Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed the seek function #57

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions tkVideoPlayer/TickSystem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import time



# this is a small Module that allowes us to run our any while loop
# exactly set amount of times we want it without worring about other
# lines of the code effecting how many times we run it like in the
# the only limmiting factor becomes if your cpu can handle all the
# proccessing in the while loop that you put in it

class FpsController: # this is a tick system that will help you block the
# the main loop up until a certain time

def __init__(self, DesiredFps:int = 60): # we take the DesierdFps
self.fpsCount:int = 0 # this is for us to keep track of fps
# if we want to view it at real time

self.Tick:float = 1/DesiredFps # intialise the time for the tick
# we are aiming for


#private Variables
self.FpsTimer:float = time.time() + 1 # we create a timer if we want
# this will help us view the
# images our selfs

self.TickTimer:float = time.time() # this help us keep track of how
# much time has passed by adding
# the tick we made


def BlockUntilNextFrame(self):
while True: # this blockes the functions when you run it through a while loop

if self.Tick <= (time.time() - self.TickTimer): # this will check if more
# time has passed than our
# tick if that is the case
# add the tick to the
# ticktimer and unblock the
# function we blocked by
# running this function
self.TickTimer += self.Tick
return

time.sleep(1/1000) # sleep for one milliseconds
# we do this so we dont over
# stress the cpu by the while
# loop


def ShowFps(self): # this function will simply show the fps
self.fpsCount += 1 # this will keep count of the fps
if time.time() >= self.FpsTimer: # check if it hase been
# 1 sec
print(self.fpsCount) # print out the fps

self.fpsCount = 0 # reset the fps
self.FpsTimer = time.time() + 1 # reset the timer
1 change: 1 addition & 0 deletions tkVideoPlayer/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from tkVideoPlayer.tkvideoplayer import TkinterVideo
from tkVideoPlayer.TickSystem import FpsController
224 changes: 129 additions & 95 deletions tkVideoPlayer/tkvideoplayer.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import gc
import av
import time
import threading
import logging
import tkinter as tk
from PIL import ImageTk, Image, ImageOps
from typing import Tuple, Dict
from TickSystem import FpsController # this library is going to be used to controll the fps
# this takes count of the time that the other lines is
# going to take


logging.getLogger('libav').setLevel(logging.ERROR) # removes warning: deprecated pixel format used

Expand All @@ -16,12 +19,17 @@ def __init__(self, master, scaled: bool = True, consistant_frame_rate: bool = Tr
super(TkinterVideo, self).__init__(master, *args, **kwargs)

self.path = ""
self._load_thread = None

self._load_thread:threading.Thread = None
self._paused = True
self._stop = True

self.consistant_frame_rate = consistant_frame_rate # tries to keep the frame rate consistant by skipping over a few frames
self.consistant_frame_rate:bool = consistant_frame_rate # tries to keep the frame rate consistant by skipping over a few frames

self.Frame_Rate_Controller:FpsController = None # this will be used to block the thread until the next frame it utlise a
# tick system so the time it takes to run your code is accounted for
# refare to the file TickSystem.py for further explnation

self.Frame_Rate_Scaler:float = 1.0 # this var is to contral how slow or fast the frame rate is for speeding or slowing down video

self._container = None

Expand All @@ -39,7 +47,6 @@ def __init__(self, master, scaled: bool = True, consistant_frame_rate: bool = Tr
"duration": 0, # duration of the video
"framerate": 0, # frame rate of the video
"framesize": (0, 0) # tuple containing frame height and width of the video

}

self.set_scaled(scaled)
Expand Down Expand Up @@ -104,126 +111,139 @@ def _load(self, path):

current_thread = threading.current_thread()

try:
with av.open(path) as self._container:
with av.open(path) as self._container:

self._container.streams.video[0].thread_type = "AUTO"
self._container.fast_seek = True
self._container.discard_corrupt = True
self._container.streams.video[0].thread_type = "AUTO"

self._container.fast_seek = True
self._container.discard_corrupt = True

stream = self._container.streams.video[0]
stream = self._container.streams.video[0]

try:
self._video_info["framerate"] = int(stream.average_rate)
try:
self._video_info["framerate"] = int(stream.average_rate * self.Frame_Rate_Scaler) # this file has been edited the the var can speed or slow down the video
self.Frame_Rate_Controller = FpsController(DesiredFps=(stream.average_rate * self.Frame_Rate_Scaler))

except TypeError:
raise TypeError("Not a video file")

try:
except TypeError:
raise TypeError("Not a video file")

self._video_info["duration"] = float(stream.duration * stream.time_base)
self.event_generate("<<Duration>>") # duration has been found
try:

except (TypeError, tk.TclError): # the video duration cannot be found, this can happen for mkv files
pass
self._video_info["duration"] = float(stream.duration * stream.time_base)
self.event_generate("<<Duration>>") # duration has been found

self._frame_number = 0
except (TypeError, tk.TclError): # the video duration cannot be found, this can happen for mkv files
pass

self._set_frame_size()
self._frame_number = 0

self.stream_base = stream.time_base
self._set_frame_size()

try:
self.event_generate("<<Loaded>>") # generated when the video file is opened

except tk.TclError:
pass
self.stream_base = stream.time_base

now = time.time_ns() // 1_000_000 # time in milliseconds
then = now
try:
self.event_generate("<<Loaded>>") # generated when the video file is opened

except tk.TclError:
pass

time_in_frame = (1/self._video_info["framerate"])*1000 # second it should play each frame
now = time.time_ns() // 1_000_000 # time in milliseconds
then = now

time_in_frame = (1/self._video_info["framerate"])*1000 # second it should play each frame

while self._load_thread == current_thread and not self._stop:
if self._seek: # seek to specific second
self._container.seek(self._seek_sec*1000000 , whence='time', backward=True, any_frame=False) # the seek time is given in av.time_base, the multiplication is to correct the frame
self._seek = False
self._frame_number = self._video_info["framerate"] * self._seek_sec

self._seek_sec = 0

if self._paused:
time.sleep(0.0001) # to allow other threads to function better when its paused
continue

now = time.time_ns() // 1_000_000 # time in milliseconds
delta = now - then # time difference between current frame and previous frame
then = now

# print("Frame: ", frame.time, frame.index, self._video_info["framerate"])
try:
frame = next(self._container.decode(video=0))

self._time_stamp = float(frame.pts * stream.time_base)
while self._load_thread == current_thread and not self._stop:

width = self._current_frame_size[0]
height = self._current_frame_size[1]
if self._keep_aspect_ratio:
im_ratio = frame.width / frame.height
dest_ratio = width / height
if im_ratio != dest_ratio:
if im_ratio > dest_ratio:
new_height = round(frame.height / frame.width * width)
height = new_height
else:
new_width = round(frame.width / frame.height * height)
width = new_width
if self._seek and not self._stop: # seek to specific second
# the seek time is given in av.time_base, the multiplication is to correct the frame
self._container.seek(self._seek_sec*1000000)
frame = next(self._container.decode(video=0)) # grab the next frame
CurrentFrame = float(frame.pts * stream.time_base) # calclate the time of the frame

self._current_img = frame.to_image(width=width, height=height, interpolation="FAST_BILINEAR")

self._frame_number += 1

self.event_generate("<<FrameGenerated>>")
if CurrentFrame >= self._seek_sec and not self._stop: # check if the frame time is before
try: # we have to try because this is running on a thread
self._container.seek((self._seek_sec - 5)*1000000) # if it is then seek the before 5 sec
except:
pass

self._seek = False # we set the seek to false so it dose not correct where we are
# as we are seeking it from a seek bar this is mostly for gui

# we can loop through the frames until we get to the desired frame
while CurrentFrame <= self._seek_sec and not self._seek and not self._stop:

try:
frame = next(self._container.decode(video=0)) # keep getting the next frame
CurrentFrame = float(frame.pts * stream.time_base) # update the current frame time so we know where we are at so far
except:
pass

if self._frame_number % self._video_info["framerate"] == 0:
self.event_generate("<<SecondChanged>>")

if self.consistant_frame_rate:
time.sleep(max((time_in_frame - delta)/1000, 0))

# time.sleep(abs((1 / self._video_info["framerate"]) - (delta / 1000)))

except (StopIteration, av.error.EOFError, tk.TclError):
break

self._container.close()
self._frame_number = self._video_info["framerate"] * self._seek_sec

self._seek_sec = 0

if self._paused:
time.sleep(0.0001) # to allow other threads to function better when its paused

self.Frame_Rate_Controller.TickTimer = time.time() # reset the TickTimer this will help
# tell our code that we are not behind on
# fps
continue

now = time.time_ns() // 1_000_000 # time in milliseconds
delta = now - then # time difference between current frame and previous frame
then = now

# print("Frame: ", frame.time, frame.index, self._video_info["framerate"])
try:
frame = next(self._container.decode(video=0))

# print("Container: ", self._container.c)
if self._container:
self._container.close()
self._container = None
self._time_stamp = float(frame.pts * stream.time_base)

self._current_img = frame.to_image()

self._frame_number += 1

finally:
self._cleanup()
gc.collect()
self.event_generate("<<FrameGenerated>>")

if self._frame_number % self._video_info["framerate"] == 0:
self.event_generate("<<SecondChanged>>")

if self.consistant_frame_rate:
#time.sleep(max((time_in_frame - delta)/1000, 0))
self.Frame_Rate_Controller.BlockUntilNextFrame() # this is much better methode than the time.sleep()
# this utlise the simple mousle that will achieve
# the Desired Frame rate through keeping track of a tick
# system i still left the code for the previous methode
# so you can use that if you dont care about playing the
# video faster or slower

# time.sleep(abs((1 / self._video_info["framerate"]) - (delta / 1000)))

except (StopIteration, av.error.EOFError, tk.TclError):
break

def _cleanup(self):
self._frame_number = 0
self._paused = True
self._stop = True
if self._load_thread:
self._load_thread = None
if self._container:
self._container.close()
self._container = None
self._load_thread = None

self._container = None

try:
self.event_generate("<<Ended>>")
self.event_generate("<<Ended>>") # this is generated when the video ends

except tk.TclError:
pass


def load(self, path: str):
""" loads the file from the given path """
self.stop()
Expand All @@ -233,7 +253,6 @@ def stop(self):
""" stops reading the file """
self._paused = True
self._stop = True
self._cleanup()

def pause(self):
""" pauses the video file """
Expand Down Expand Up @@ -279,10 +298,25 @@ def current_img(self) -> Image:
def _display_frame(self, event):
""" displays the frame on the label """

if self.current_imgtk.width() == self._current_img.width and self.current_imgtk.height() == self._current_img.height:
self.current_imgtk.paste(self._current_img)
else:
self.current_imgtk = ImageTk.PhotoImage(self._current_img)
if self.scaled or (len(self._current_frame_size) == 2 and all(self._current_frame_size)):

if self._keep_aspect_ratio:
self._current_img = ImageOps.contain(self._current_img, self._current_frame_size, self._resampling_method)

else:
self._current_img = self._current_img.resize(self._current_frame_size, self._resampling_method)

else:
self._current_frame_size = self.video_info()["framesize"] if all(self.video_info()["framesize"]) else (1, 1)

if self._keep_aspect_ratio:
self._current_img = ImageOps.contain(self._current_img, self._current_frame_size, self._resampling_method)

else:
self._current_img = self._current_img.resize(self._current_frame_size, self._resampling_method)


self.current_imgtk = ImageTk.PhotoImage(self._current_img)
self.config(image=self.current_imgtk)

def seek(self, sec: int):
Expand Down