Skip to content
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
139 changes: 139 additions & 0 deletions examples/sample_player2.py
Original file line number Diff line number Diff line change
@@ -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("<ButtonRelease-1>", 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("<<Duration>>", update_duration)
vid_player.bind("<<SecondChanged>>", update_scale)
vid_player.bind("<<Ended>>", 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("<<ComboboxSelected>>", set_speed)

root.mainloop()
75 changes: 43 additions & 32 deletions tkVideoPlayer/tkvideoplayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -50,6 +51,10 @@ def __init__(self, master, scaled: bool = True, consistant_frame_rate: bool = Tr
self.bind("<<Destroy>>", self.stop)
self.bind("<<FrameGenerated>>", 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
Expand Down Expand Up @@ -108,64 +113,69 @@ 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

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

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>>") # 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("<<Loaded>>") # generated when the video file is opened

self.event_generate("<<Loaded>>") # 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]
Expand All @@ -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("<<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))

# 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
Expand Down