diff --git a/examples/sample_player2.py b/examples/sample_player2.py new file mode 100644 index 0000000..2c5989e --- /dev/null +++ b/examples/sample_player2.py @@ -0,0 +1,139 @@ +# Author Paul +# Edit Victor + +# MIT License +# +# Copyright (c) 2021 Paul +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# This is same as sample_player1 example but adds speed control + +# import sys +# sys.path.append('./') + +import datetime +import tkinter as tk +from tkinter.ttk import Combobox +from tkinter import filedialog +from tkvideoplayer import TkinterVideo + + +def update_duration(event): + """ updates the duration after finding the duration """ + duration = vid_player.video_info()["duration"] + end_time["text"] = str(datetime.timedelta(seconds=duration)) + progress_slider["to"] = duration + + +def update_scale(event): + """ updates the scale value """ + progress_value.set(vid_player.current_duration()) + + +def load_video(): + """ loads the video """ + file_path = filedialog.askopenfilename() + + if file_path: + vid_player.load(file_path) + + progress_slider.config(to=0, from_=0) + play_pause_btn["text"] = "Play" + progress_value.set(0) + + +def seek(value): + """ used to seek a specific timeframe """ + vid_player.seek(int(value)) + + +def skip(value: int): + """ skip seconds """ + vid_player.seek(int(progress_slider.get())+value) + progress_value.set(progress_slider.get() + value) + +def set_speed(event): + """ sets the playback speed """ + selected_speed = speed_combobox.get() + if selected_speed: + vid_player.set_speed(float(selected_speed)) + +def play_pause(): + """ pauses and plays """ + if vid_player.is_paused(): + vid_player.play() + play_pause_btn["text"] = "Pause" + + else: + vid_player.pause() + play_pause_btn["text"] = "Play" + + +def video_ended(event): + """ handle video ended """ + progress_slider.set(progress_slider["to"]) + play_pause_btn["text"] = "Play" + progress_slider.set(0) + + +root = tk.Tk() +root.title("Tkinter media") + +load_btn = tk.Button(root, text="Load", command=load_video) +load_btn.pack() + +vid_player = TkinterVideo(scaled=True, master=root) +vid_player.pack(expand=True, fill="both") + +play_pause_btn = tk.Button(root, text="Play", command=play_pause) +play_pause_btn.pack() + +skip_plus_5sec = tk.Button(root, text="Skip -5 sec", command=lambda: skip(-5)) +skip_plus_5sec.pack(side="left") + +start_time = tk.Label(root, text=str(datetime.timedelta(seconds=0))) +start_time.pack(side="left") + +progress_value = tk.IntVar(root) + +progress_slider = tk.Scale(root, variable=progress_value, from_=0, to=0, orient="horizontal", command=seek) +# progress_slider.bind("", seek) +progress_slider.pack(side="left", fill="x", expand=True) + +end_time = tk.Label(root, text=str(datetime.timedelta(seconds=0))) +end_time.pack(side="left") + +vid_player.bind("<>", update_duration) +vid_player.bind("<>", update_scale) +vid_player.bind("<>", video_ended ) + +skip_plus_5sec = tk.Button(root, text="Skip +5 sec", command=lambda: skip(5)) +skip_plus_5sec.pack(side="left") + +# Speed Selection Combobox +speed_label = tk.Label(root, text="Speed:") +speed_label.pack(side="left", padx=5) + +speed_combobox = Combobox(root, values=["0.1","0.5", "1.0", "1.5", "2.0", "5.0"], state="readonly") +speed_combobox.set("1.0") # Default speed +speed_combobox.pack(side="left", padx=5) +speed_combobox.bind("<>", set_speed) + +root.mainloop() diff --git a/tkVideoPlayer/tkvideoplayer.py b/tkVideoPlayer/tkvideoplayer.py index f2c8a9f..3c52c1e 100644 --- a/tkVideoPlayer/tkvideoplayer.py +++ b/tkVideoPlayer/tkvideoplayer.py @@ -29,6 +29,7 @@ def __init__(self, master, scaled: bool = True, consistant_frame_rate: bool = Tr self._current_frame_Tk = None self._frame_number = 0 self._time_stamp = 0 + self._playback_speed = 1.0 # Default to normal speed self._current_frame_size = (0, 0) @@ -50,6 +51,10 @@ def __init__(self, master, scaled: bool = True, consistant_frame_rate: bool = Tr self.bind("<>", self.stop) self.bind("<>", self._display_frame) + def set_speed(self, speed: float): + """Set playback speed.""" + self._playback_speed = speed + def keep_aspect(self, keep_aspect: bool): """ keeps the aspect ratio when resizing the image """ self._keep_aspect_ratio = keep_aspect @@ -108,7 +113,6 @@ def _load(self, path): 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 @@ -116,56 +120,62 @@ def _load(self, path): try: self._video_info["framerate"] = int(stream.average_rate) - except TypeError: raise TypeError("Not a video file") - - try: + try: self._video_info["duration"] = float(stream.duration * stream.time_base) self.event_generate("<>") # duration has been found - - except (TypeError, tk.TclError): # the video duration cannot be found, this can happen for mkv files + except (TypeError, tk.TclError): pass self._frame_number = 0 - self._set_frame_size() - self.stream_base = stream.time_base try: - self.event_generate("<>") # generated when the video file is opened - + self.event_generate("<>") # Generated when the video file is opened except tk.TclError: pass - now = time.time_ns() // 1_000_000 # time in milliseconds + 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 + if self._seek: # Seek to a specific second + self._container.seek( + self._seek_sec * 1_000_000, + whence="time", + backward=True, + any_frame=False + ) 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 + time.sleep(0.0001) # To allow other threads to function better when paused continue - now = time.time_ns() // 1_000_000 # time in milliseconds - delta = now - then # time difference between current frame and previous frame + # Adjust logic for playback speed + speed_multiplier = self._playback_speed + frames_to_skip = int(speed_multiplier) if speed_multiplier >= 1.0 else 0 # Skip frames only for speeds >= 1.0 + + 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)) + for _ in range(frames_to_skip): # Skip frames based on playback speed + frame = next(self._container.decode(video=0)) + + # Slow-motion playback: Increase sleep duration dynamically + if speed_multiplier < 1.0: + time_in_frame = (1 / self._video_info["framerate"]) * 1000 / speed_multiplier + else: + time_in_frame = (1 / self._video_info["framerate"]) * 1000 + frame = next(self._container.decode(video=0)) self._time_stamp = float(frame.pts * stream.time_base) width = self._current_frame_size[0] @@ -181,34 +191,35 @@ def _load(self, path): new_width = round(frame.width / frame.height * height) width = new_width - self._current_img = frame.to_image(width=width, height=height, interpolation="FAST_BILINEAR") - + self._current_img = frame.to_image( + width=width, + height=height, + interpolation="FAST_BILINEAR" + ) self._frame_number += 1 - + 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)) - - # time.sleep(abs((1 / self._video_info["framerate"]) - (delta / 1000))) + time.sleep(max((time_in_frame - delta) / 1000, 0)) except (StopIteration, av.error.EOFError, tk.TclError): break - + self._container.close() - # print("Container: ", self._container.c) if self._container: self._container.close() self._container = None - + finally: self._cleanup() gc.collect() + def _cleanup(self): self._frame_number = 0 self._paused = True