diff --git a/tkVideoPlayer/TickSystem.py b/tkVideoPlayer/TickSystem.py new file mode 100644 index 0000000..cddb49d --- /dev/null +++ b/tkVideoPlayer/TickSystem.py @@ -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 \ No newline at end of file diff --git a/tkVideoPlayer/__init__.py b/tkVideoPlayer/__init__.py index 55c72e8..6b7eb2f 100644 --- a/tkVideoPlayer/__init__.py +++ b/tkVideoPlayer/__init__.py @@ -1 +1,2 @@ from tkVideoPlayer.tkvideoplayer import TkinterVideo +from tkVideoPlayer.TickSystem import FpsController diff --git a/tkVideoPlayer/tkvideoplayer.py b/tkVideoPlayer/tkvideoplayer.py index f2c8a9f..ff692fb 100644 --- a/tkVideoPlayer/tkvideoplayer.py +++ b/tkVideoPlayer/tkvideoplayer.py @@ -1,4 +1,3 @@ -import gc import av import time import threading @@ -6,6 +5,10 @@ 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 @@ -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 @@ -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) @@ -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 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 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("<>") # 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("<>") # 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("<>") + 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("<>") - 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("<>") + + if self._frame_number % self._video_info["framerate"] == 0: + self.event_generate("<>") + + 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("<>") + self.event_generate("<>") # 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() @@ -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 """ @@ -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):