diff --git a/Documentation.md b/Documentation.md index 8d2479d..f22835a 100644 --- a/Documentation.md +++ b/Documentation.md @@ -1,42 +1,47 @@ # TkVideoPlayer: -TkVideoPlayer inherits from `tk.Label` and display's the image on the label. +**TkVideoPlayer** inherits from `tk.Label` and display's the image on the label. Below are the methods of this library. | Methods | Parameters | Description | |------------------|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| \_\_init\_\_ | scaled(bool), consistant_frame_rate(bool)=True, keep_aspect(bool)=False | The scale parameter scales the video to the label size. The consistant_frame_rate parameter skips frames to keep the framerate consistant and keep_aspect keeps aspect ratio when resizing(note: It will not increase the size) | -| set_scaled | scaled(bool), keep_aspect(bool)=False | scales the video to the label size. | -| load | file_path(str) | starts loading the video in a thread. | -| set_size | size(Tuple[int, int]), keep_aspect(bool)=False | sets the size of the video frame. setting this will set scaled to `False` | -| current_duration | - | return video duration in seconds. | -| video_info | - | returns dictionary containing framerate, framesize, duration.| -| play | - | Plays the video. | -| pause | - | Pauses the video | -| is_paused | - | returns if the video is currently paused. -| stop | - | stops playing the file, closes the file. | -| seek | sec(int) | moves to specific time stamp. provide time stamp in seconds -| keep_aspect | keep_aspect(bool) | keeps aspect ratio when resizing -| metadata | - | returns meta information of the video if available in the form of dictionary -| set_resampling_method| method(int) | By default the resampling method while resizing is NEAREST, changing this can affect how its resampled when image is resized, refer PIL documentation to read more (note: this can also affect the framerate of the video)| +| **\_\_init\_\_** |
  • scaled(bool)
  • consistant_frame_rate(bool)
  • keep_aspect(bool)=False
  • audio(bool)=False
  • |
  • The _scaled_ parameter scales the video to the label size.
  • The _consistant_frame_rate_ parameter adds an appropriate time delay to keep the framerate consistant.
  • _keep_aspect_ keeps aspect ratio when resizing. (note: It will not increase the size)
  • _audio_ enables audio in clip (experimental)
  • | +| set_scaled |
  • scaled(bool)
  • keep_aspect(bool)=False
  • | scales the video to the label size. | +| **load** | file_path(str) | starts loading the video file in a thread. | +| set_size |
  • size(Tuple[int, int])
  • keep_aspect(bool)=False
  • | sets the size of the video frame, setting this will set _scaled_ to `False`. | +| current_duration | - | return the current video duration in seconds. | +| current_frame_number | - | get the current number of the frame. | +| **video_info** | - | returns a dictionary containing name, framerate, framesize, duration, total frames and codec of the video.| +| **play** | - | Plays the video. | +| **pause** | - | Pauses the video. | +| **is_paused** | - | returns if the video is currently paused. | +| is_stopped | - | returns if the video is currently stopped. | +| **stop** | - | stops playing the file, closes the file. | +| **seek** |
  • sec(int)
  • any_frame(bool)=False
  • pause(bool)=False
  • |
  • moves to specific time stamp. (provide time stamp in seconds only)
  • any_frame: seek to any nearest keyframe if possible.
  • The _pause_ parameter pauses the video after seeking to the given timestamp.
  • | +| **seek_frame** |
  • frame(int)
  • delay(float)=False
  • pause(bool)=True
  • |
  • moves to the specific frame number.
  • delay (0-1): add a slight delay while seeking for optimization
  • The _pause_ parameter pauses the video after seeking to the given frame.
  • +| keep_aspect | keep_aspect(bool) | keeps aspect ratio when resizing. | +| metadata | - | returns meta information of the video if available in the form of dictionary. | +| set_resampling_method | method(int) | The resampling method while resizing can be set as NEAREST, this can affect how its resampled when image is displayed, refer PIL documentation to read more. (note: this can also affect the framerate of the video) | +| current_img | - | get the current frame image. | ### Virtual events: | Virtual event | Description | |------------------------|---------------------------------------------------------------------------------------------------------------------| -| \<\\> | This event is generated when the video file is opened. | +| \<\\> | This event is generated when the video file is opened. | | \<\\> | This event is generated when the video duration is found. | | \<\\> | This event is generated whenever a second in the video passes (calculated using frame_number%frame_rate==0). | -| \<\\> | This event is generated whenever there is a new frame available. (internal use, don't use this unless you want to). | +| \<\\> | This event is generated whenever there is a new frame available. | | \<\\> | This event is generated only when the video has ended. | -note: +Note: If you would like to draw on the video etc. Copy/fork the repo and instead of inheriting from Label inherit from Canvas. -And use `image_id = self.create_image()` use the image_id to update the image. + +And add `image_id = self.create_image()` and then use the image_id to update the image. diff --git a/LICENSE b/LICENSE index aed4c30..412f374 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,25 @@ 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. + +MIT License + +Copyright (c) 2024 Akascape + +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. diff --git a/Readme.md b/Readme.md index 3b3760d..631554c 100644 --- a/Readme.md +++ b/Readme.md @@ -1,9 +1,22 @@ # TkVideoplayer -This is a simple library to play video files in tkinter. This library also provides the ability to play, pause, -skip and seek to specific timestamps. - -#### Example: +This is a simple library to **play video** files in tkinter. This library also provides the ability to play, pause, +skip and seek to specific timestamps of the video. (Audio included) + +### Installation +This is a modified version of tkVideoPlayer by PauleDemon. Download the folder tkVideoPlayer of this fork. + +Functionalities: +- load(path) : to load a video file +- stop() : stops the video +- play() : plays the video +- video_info() : return video info like framerate(fps), durartion, frame size, total frames, codec and name of the video +- current_frame_number() : returns the frame number of current location +- current_img() : return the PIL image of current frame +- seek(sec, pause) : seek to any timestamp, use pause=True to pause after seeking +- seek_frame(frame, pause) : seeks accurately to any frame number, use pause=True to pause after seeking +- mute()/unmute() : enable audio in clip (currently in beta stage) +#### Simple Usage: ```python import tkinter as tk from tkVideoPlayer import TkinterVideo @@ -18,20 +31,11 @@ videoplayer.play() # play the video root.mainloop() ``` +## If you are facing issues while playing some videos, turn off the audio. -read the documentation [here](https://github.com/PaulleDemon/tkVideoPlayer/blob/master/Documentation.md) - -> Please immediately upgrade to the latest version if you are using version 1.3 or below -#### Sample video player made using tkVideoPlayer: -![Sample player](https://github.com/PaulleDemon/tkVideoPlayer/blob/master/videoplayer_screenshot.png?raw=True) - -This example source code can be found [here](https://github.com/PaulleDemon/tkVideoPlayer/blob/master/examples/sample_player.py) - - -**Other libraries you might be interested in:** - -* [tkstylesheet](https://pypi.org/project/tkstylesheet/) - Helps you style your tkinter application using stylesheets. +### read the documentation [here](https://github.com/Akascape/tkVideoPlayer/blob/master/Documentation.md) -* [tkTimePicker](https://pypi.org/project/tkTimePicker/) - An easy-to-use timepicker. +### Sample video players made using tkVideoPlayer: + -* [PyCollision](https://pypi.org/project/PyCollision/) - Helps you draw hitboxes for 2d games. \ No newline at end of file +https://user-images.githubusercontent.com/89206401/228515652-abe137d0-6823-4c56-ba5c-8eb8292e0182.mp4 diff --git a/build/lib/tkVideoPlayer/__init__.py b/build/lib/tkVideoPlayer/__init__.py deleted file mode 100644 index 55c72e8..0000000 --- a/build/lib/tkVideoPlayer/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from tkVideoPlayer.tkvideoplayer import TkinterVideo diff --git a/build/lib/tkVideoPlayer/tkvideoplayer.py b/build/lib/tkVideoPlayer/tkvideoplayer.py deleted file mode 100644 index b687381..0000000 --- a/build/lib/tkVideoPlayer/tkvideoplayer.py +++ /dev/null @@ -1,278 +0,0 @@ -import av -import time -import threading -import logging -import tkinter as tk -from PIL import ImageTk, Image, ImageOps -from typing import Tuple, Dict - -logging.getLogger('libav').setLevel(logging.ERROR) # removes warning: deprecated pixel format used - - -class TkinterVideo(tk.Label): - - def __init__(self, master, scaled: bool = True, consistant_frame_rate: bool = True, keep_aspect: bool = False, *args, **kwargs): - super(TkinterVideo, self).__init__(master, *args, **kwargs) - - self.path = "" - self._load_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._container = None - - self._current_img = None - self._current_frame_Tk = None - self._frame_number = 0 - self._time_stamp = 0 - - self._current_frame_size = (0, 0) - - self._seek = False - self._seek_sec = 0 - - self._video_info = { - "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) - self._keep_aspect_ratio = keep_aspect - self._resampling_method: int = Image.NEAREST - - - self.bind("<>", self.stop) - self.bind("<>", self._display_frame) - - def keep_aspect(self, keep_aspect: bool): - """ keeps the aspect ratio when resizing the image """ - self._keep_aspect_ratio = keep_aspect - - def set_resampling_method(self, method: int): - """ sets the resampling method when resizing """ - self._resampling_method = method - - def set_size(self, size: Tuple[int, int], keep_aspect: bool=False): - """ sets the size of the video """ - self.set_scaled(False, self._keep_aspect_ratio) - self._current_frame_size = size - self._keep_aspect_ratio = keep_aspect - - def _resize_event(self, event): - - self._current_frame_size = event.width, event.height - - if self._paused and self._current_img and self.scaled: - if self._keep_aspect_ratio: - proxy_img = ImageOps.contain(self._current_img.copy(), self._current_frame_size) - - else: - proxy_img = self._current_img.copy().resize(self._current_frame_size) - - self._current_imgtk = ImageTk.PhotoImage(proxy_img) - self.config(image=self._current_imgtk) - - - def set_scaled(self, scaled: bool, keep_aspect: bool = False): - self.scaled = scaled - self._keep_aspect_ratio = keep_aspect - - if scaled: - self.bind("", self._resize_event) - - else: - self.unbind("") - self._current_frame_size = self.video_info()["framesize"] - - - def _set_frame_size(self, event=None): - """ sets frame size to avoid unexpected resizing """ - - self._video_info["framesize"] = (self._container.streams.video[0].width, self._container.streams.video[0].height) - - self.current_imgtk = ImageTk.PhotoImage(Image.new("RGBA", self._video_info["framesize"], (255, 0, 0, 0))) - self.config(width=150, height=100, image=self.current_imgtk) - - def _load(self, path): - """ load's file from a thread """ - - current_thread = threading.current_thread() - - 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: - - 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 - 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 - - except tk.TclError: - pass - - 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) - - self._current_img = frame.to_image() - - 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))) - - except (StopIteration, av.error.EOFError, tk.TclError): - break - - self._frame_number = 0 - self._paused = True - self._load_thread = None - - self._container = None - - try: - 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() - self.path = path - - def stop(self): - """ stops reading the file """ - self._paused = True - self._stop = True - - def pause(self): - """ pauses the video file """ - self._paused = True - - def play(self): - """ plays the video file """ - self._paused = False - self._stop = False - - if not self._load_thread: - # print("loading new thread...") - self._load_thread = threading.Thread(target=self._load, args=(self.path, ), daemon=True) - self._load_thread.start() - - def is_paused(self): - """ returns if the video is paused """ - return self._paused - - def video_info(self) -> Dict: - """ returns dict containing duration, frame_rate, file""" - return self._video_info - - def metadata(self) -> Dict: - """ returns metadata if available """ - if self._container: - return self._container.metadata - - return {} - - def current_frame_number(self) -> int: - """ return current frame number """ - return self._frame_number - - def current_duration(self) -> float: - """ returns current playing duration in sec """ - return self._time_stamp - - def current_img(self) -> Image: - """ returns current frame image """ - return self._current_img - - def _display_frame(self, event): - """ displays the frame on the label """ - - 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): - """ seeks to specific time""" - - self._seek = True - self._seek_sec = sec - \ No newline at end of file diff --git a/dist/tkvideoplayer-2.3-py3-none-any.whl b/dist/tkvideoplayer-2.3-py3-none-any.whl deleted file mode 100644 index 0a7e1da..0000000 Binary files a/dist/tkvideoplayer-2.3-py3-none-any.whl and /dev/null differ diff --git a/dist/tkvideoplayer-2.3.tar.gz b/dist/tkvideoplayer-2.3.tar.gz deleted file mode 100644 index 4aacbd4..0000000 Binary files a/dist/tkvideoplayer-2.3.tar.gz and /dev/null differ diff --git a/examples/CTkVideoPlayer.py b/examples/CTkVideoPlayer.py new file mode 100644 index 0000000..74d9711 --- /dev/null +++ b/examples/CTkVideoPlayer.py @@ -0,0 +1,85 @@ +import customtkinter +from tkinter import filedialog +from tkVideoPlayer import TkinterVideo +import os, datetime + +def open_video(): + vid_player.stop() + global video_file + new_video_file = filedialog.askopenfilename(filetypes = + [('Video', ['*.mp4','*.avi','*.mov','*.mkv']), + ('All Files', '*.*')]) + if new_video_file: + video_file = new_video_file + vid_player.load(video_file) + vid_player.play() + progress_slider.set(-1) + play_pause_btn.configure(text="Pause II") + +def seek(value): + if video_file: + vid_player.seek_frame(int(value), pause=True, delay=0.0) + play_pause_btn.configure(text="Play") + +def play_pause(): + if video_file: + if vid_player.is_paused(): + vid_player.play() + play_pause_btn.configure(text="Pause II") + else: + vid_player.pause() + play_pause_btn.configure(text="Play") + +def update_scale(event): + progress_slider.set(int(vid_player.current_frame_number())) + current_duration = datetime.timedelta(seconds=int(vid_player.current_duration())) + label_1.configure(text=str(current_duration)) + +def update_duration(event): + button_1.configure(text=os.path.basename(vid_player.video_info()["name"])) + total_frames = int(vid_player.video_info()["frames"]) + progress_slider.configure(to=total_frames, number_of_steps=total_frames) + duration = int(vid_player.video_info()["duration"]) + label_2.configure(text=str(datetime.timedelta(seconds=duration))) + +def video_ended(event): + play_pause_btn.configure(text="Play") + progress_slider.set(0) + label_1.configure(text="0:00:00") + # vid_player.play() # loop video if required + +customtkinter.set_appearance_mode("System") +customtkinter.set_default_color_theme("blue") + +app = customtkinter.CTk() +app.geometry("600x500") +app.title("CustomTkinter x TkVideoPlayer.py") + +video_file = '' +frame_1 = customtkinter.CTkFrame(master=app, corner_radius=15) +frame_1.pack(pady=20, padx=20, fill="both", expand=True) + +button_1 = customtkinter.CTkButton(master=frame_1, text="Open Video", corner_radius=8, command=open_video) +button_1.pack(pady=10, padx=10, fill="both") + +vid_player = TkinterVideo(master=frame_1, scaled=True, keep_aspect=True, consistant_frame_rate=True, bg="black") +vid_player.set_resampling_method(0) +vid_player.pack(expand=True, fill="both", padx=10, pady=10) +vid_player.bind("<>", video_ended ) +vid_player.bind("<>", update_duration) +vid_player.bind("<>", update_scale) + +progress_slider = customtkinter.CTkSlider(master=frame_1, from_=0, to=1, number_of_steps=1, command=seek) +progress_slider.set(1) +progress_slider.pack(fill="both", padx=10, pady=10) + +label_1 = customtkinter.CTkLabel(master=frame_1, text="0:00:00") +label_1.pack(side="left", padx=20, anchor="n") + +label_2 = customtkinter.CTkLabel(master=frame_1, text="0:00:00") +label_2.pack(side="right", padx=20, anchor="n") + +play_pause_btn = customtkinter.CTkButton(master=frame_1, text="Play", command=play_pause, width=10) +play_pause_btn.pack(pady=10) + +app.mainloop() diff --git a/examples/advanced_example.py b/examples/advanced_example.py new file mode 100644 index 0000000..9260ca9 --- /dev/null +++ b/examples/advanced_example.py @@ -0,0 +1,89 @@ +""" +Author: Akascape +This is an advanced example of tkvideoplayer with frame-seeking +""" + +import tkinter +from tkinter import filedialog +from tkVideoPlayer import TkinterVideo +import os, datetime + +def open_video(): + vid_player.stop() + global video_file + new_video_file = filedialog.askopenfilename(filetypes = + [('Video', ['*.mp4','*.avi','*.mov','*.mkv']), + ('All Files', '*.*')]) + if new_video_file: + video_file = new_video_file + vid_player.load(video_file) + vid_player.play() + progress_slider.set(-1) + play_pause_btn.configure(text="Pause II") + +def seek(event=None): + if video_file: + vid_player.seek_frame(int(progress_slider.get()), pause=True, delay=0.1) + play_pause_btn.configure(text="Play") + +def play_pause(): + if video_file: + if vid_player.is_paused(): + vid_player.play() + play_pause_btn.configure(text="Pause II") + else: + vid_player.pause() + play_pause_btn.configure(text="Play") + +def update_scale(event): + progress_slider.set(int(vid_player.current_frame_number())) + current_duration = datetime.timedelta(seconds=int(vid_player.current_duration())) + label_1.configure(text=str(current_duration)) + +def update_duration(event): + button_1.configure(text=os.path.basename(vid_player.video_info()["name"])) + total_frames = int(vid_player.video_info()["frames"]) + progress_slider.configure(to=total_frames) + duration = int(vid_player.video_info()["duration"]) + label_2.configure(text=str(datetime.timedelta(seconds=duration))) + +def video_ended(event): + play_pause_btn.configure(text="Play") + progress_slider.set(0) + label_1.configure(text="0:00:00") + +app = tkinter.Tk() +app.geometry("600x500") +app.title("Advanced Example.py") + +video_file = '' + +frame_1 = tkinter.Frame(master=app) +frame_1.pack(pady=20, padx=20, fill="both", expand=True) + +button_1 = tkinter.Button(master=frame_1, text="Open Video", relief="groove",command=open_video) +button_1.pack(pady=10, padx=10, fill="both") + +vid_player = TkinterVideo(master=frame_1, scaled=True, keep_aspect=True, consistant_frame_rate=True, bg="black") +vid_player.set_resampling_method(0) +vid_player.pack(expand=True, fill="both", padx=10, pady=10) +vid_player.bind("<>", video_ended ) +vid_player.bind("<>", update_duration) +vid_player.bind("<>", update_scale) + +progress_slider = tkinter.Scale(master=frame_1, from_=0, to=1, orient="horizontal") +progress_slider.set(0) +progress_slider.bind("", seek) +progress_slider.bind("", seek) +progress_slider.pack(fill="both", padx=10, pady=10) + +label_1 = tkinter.Label(master=frame_1, text="0:00:00") +label_1.pack(side="left", padx=20, anchor="n") + +label_2 = tkinter.Label(master=frame_1, text="0:00:00") +label_2.pack(side="right", padx=20, anchor="n") + +play_pause_btn = tkinter.Button(master=frame_1, text="Play", command=play_pause, width=10) +play_pause_btn.pack(pady=10) + +app.mainloop() diff --git a/requirements.txt b/requirements.txt index dd97e75..aefece4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -av~=9.2.0 -pillow~=9.0.1 +av +pillow +pyaudio diff --git a/sample_media_player.png b/sample_media_player.png deleted file mode 100644 index 9c762d5..0000000 Binary files a/sample_media_player.png and /dev/null differ diff --git a/setup.py b/setup.py deleted file mode 100644 index 4132a7e..0000000 --- a/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -from setuptools import setup - -with open("Readme.md", 'r') as f: - long_description = f.read() - -setup( - name='tkvideoplayer', - version='2.3', - description="This library helps you play video files in tkinter", - license="MIT", - long_description=long_description, - long_description_content_type="text/markdown", - author='Paul', - url="https://github.com/PaulleDemon/tkVideoPlayer", - classifiers=[ - "License :: OSI Approved :: MIT License", - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10" - ], - keywords=['tkinter', 'video', 'player', 'video player', 'tkvideoplayer', 'play video in tkinter'], - packages=["tkVideoPlayer"], - install_requires=["av", "pillow"], - include_package_data=True, - python_requires='>=3.6', -) \ No newline at end of file diff --git a/tkVideoPlayer/tkvideoplayer.py b/tkVideoPlayer/tkvideoplayer.py index b687381..eeb9606 100644 --- a/tkVideoPlayer/tkvideoplayer.py +++ b/tkVideoPlayer/tkvideoplayer.py @@ -1,4 +1,7 @@ +# Modified version of TkVideoPlayer by Akascape + import av +import pyaudio import time import threading import logging @@ -6,46 +9,46 @@ from PIL import ImageTk, Image, ImageOps from typing import Tuple, Dict -logging.getLogger('libav').setLevel(logging.ERROR) # removes warning: deprecated pixel format used - +logging.getLogger('libav').setLevel(logging.CRITICAL) # removes warning: deprecated pixel format used class TkinterVideo(tk.Label): - def __init__(self, master, scaled: bool = True, consistant_frame_rate: bool = True, keep_aspect: bool = False, *args, **kwargs): + def __init__(self, master, scaled: bool = True, consistant_frame_rate: bool = True, keep_aspect: bool = False, audio=True, *args, **kwargs): super(TkinterVideo, self).__init__(master, *args, **kwargs) self.path = "" self._load_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 = consistant_frame_rate # tries to keep the frame rate consistant by delaying between frames self._container = None - self._current_img = None self._current_frame_Tk = None + self._frame_number = 0 self._time_stamp = 0 - self._current_frame_size = (0, 0) - self._seek = False self._seek_sec = 0 - + self._seek_frame = False + self._frame = 0 + self._any_frame = False + self._seek_pause = False + self._audio = audio + self._video_info = { + "name": None, # name/path of the video "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 - + "framesize": (0, 0), # tuple containing frame height and width of the video + "frames": 0, # total frames of the video + "codec": None # codec of the file } self.set_scaled(scaled) self._keep_aspect_ratio = keep_aspect self._resampling_method: int = Image.NEAREST - self.bind("<>", self.stop) self.bind("<>", self._display_frame) @@ -64,7 +67,7 @@ def set_size(self, size: Tuple[int, int], keep_aspect: bool=False): self._keep_aspect_ratio = keep_aspect def _resize_event(self, event): - + """ scales the label frame dynamically """ self._current_frame_size = event.width, event.height if self._paused and self._current_img and self.scaled: @@ -77,11 +80,11 @@ def _resize_event(self, event): self._current_imgtk = ImageTk.PhotoImage(proxy_img) self.config(image=self._current_imgtk) - def set_scaled(self, scaled: bool, keep_aspect: bool = False): + """ set dynamic scalling for the label """ self.scaled = scaled self._keep_aspect_ratio = keep_aspect - + if scaled: self.bind("", self._resize_event) @@ -89,38 +92,79 @@ def set_scaled(self, scaled: bool, keep_aspect: bool = False): self.unbind("") self._current_frame_size = self.video_info()["framesize"] - def _set_frame_size(self, event=None): """ sets frame size to avoid unexpected resizing """ - self._video_info["framesize"] = (self._container.streams.video[0].width, self._container.streams.video[0].height) self.current_imgtk = ImageTk.PhotoImage(Image.new("RGBA", self._video_info["framesize"], (255, 0, 0, 0))) self.config(width=150, height=100, image=self.current_imgtk) + + def _display_frame(self, event): + """ displays the frame on the label """ + + self.event_generate("<>") + + 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 _load(self, path): """ load's file from a thread """ current_thread = threading.current_thread() - with av.open(path) as self._container: + with av.open(path, "r") 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] + video_stream = self._container.streams.video[0] try: - self._video_info["framerate"] = int(stream.average_rate) - - except TypeError: - raise TypeError("Not a video file") + if self._audio: + audio_stream = self._container.streams.audio[0] + + samplerate = audio_stream.rate # this will work as the video clock + channels = audio_stream.channels + + p = pyaudio.PyAudio() + audio_device = p.open(format=pyaudio.paFloat32, + channels=channels, + rate=samplerate, + output=True) + self.audio_stream_base = audio_stream.time_base + else: + audio_device = False + except: + audio_device = False + + self._video_info["framerate"] = int(video_stream.average_rate) + self._video_info["frames"] = int(video_stream.frames) + self._video_info["codec"] = str(video_stream.codec_context.name) + self._video_info["name"] = str(video_stream.container.name) + + self.video_stream_base = video_stream.time_base - try: - - self._video_info["duration"] = float(stream.duration * stream.time_base) + try: + self._video_info["duration"] = round(float(video_stream.duration * self.video_stream_base), 2) self.event_generate("<>") # duration has been found except (TypeError, tk.TclError): # the video duration cannot be found, this can happen for mkv files @@ -130,8 +174,6 @@ def _load(self, path): self._set_frame_size() - self.stream_base = stream.time_base - try: self.event_generate("<>") # generated when the video file is opened @@ -140,68 +182,173 @@ def _load(self, path): 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 - + + self.frame_buffers = [] 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 nearest timestamp (second) + self._container.seek(self._seek_sec*1000000 , whence='time', backward=True, any_frame=self._any_frame) # the seek time application is to correct the frame self._seek = False self._frame_number = self._video_info["framerate"] * self._seek_sec - self._seek_sec = 0 - + + if self._seek_pause: + self.play() + self.after(50, self.pause) + + if self._seek_frame: # seek to a specific frame + sec = int(self._frame/self._video_info["framerate"]) # get average timestamp of that required frame + self._container.seek(sec*1000000, whence='time', backward=True) # then seek to the nearest timestamp/keyframe + frame = next(self._container.decode(video=0)) # get the next frame + sec_frame = int(frame.pts * self.video_stream_base * self._video_info["framerate"]) # get that keyframe number + + try: + if self._delay: # a little bit delay can limit the cpu usage + time.sleep(self._delay) + + # seek to the required frame + for _ in range(sec_frame, self._frame): + frame = next(self._container.decode(video=0)) + + self._current_img = frame.to_image() + self._time_stamp = round(float(frame.pts * self.video_stream_base), 2) + self._frame_number = self._frame + self.event_generate("<>") + self._seek_frame = False + self._frame = 0 + + if self._seek_pause: + self.pause() + + except (StopIteration, av.error.EOFError, tk.TclError): + self._seek_frame = False + self._frame = 0 + break + 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"]) + af = 0 + dont_loop = False + try: - frame = next(self._container.decode(video=0)) - - self._time_stamp = float(frame.pts * stream.time_base) - - self._current_img = frame.to_image() - - 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))) - + if audio_device and self._audio: + last_audio_buffer = False + last_video_buffer = False + + while True: + frame = next(self._container.decode(video=0, audio=0)) + + if 'Video' in repr(frame): + if last_audio_buffer: + + if round(float(frame.pts * self.video_stream_base), 2)<=last_audio_buffer: + self.frame_buffers.append(frame) + else: + break # break if the last audio buffer pts matches the final video buffer pts + if not last_video_buffer: + break + if af<=3: + break + if af>=100: # avoid leakage + self.frame_buffers = [] + self.stop() + break + dont_loop = True + else: + self.frame_buffers.append(frame) + last_video_buffer = round(float(frame.pts * self.video_stream_base), 2) + + else: + if dont_loop: # avoid excessive buffering, can cause stutters + break + self.frame_buffers.append(frame) + last_audio_buffer = round(float(frame.pts * self.audio_stream_base), 2) + af+=1 + + # sort all the frames based on their presentation time + self.frame_buffers = sorted(self.frame_buffers, key=lambda f:f.pts*self.video_stream_base if 'Video' in repr(f) else f.pts*self.audio_stream_base) + + for i in self.frame_buffers: + if 'Video' in repr(i): + + self._current_img = i.to_image() + self._frame_number += 1 + + self.event_generate("<>") + + if self._frame_number % self._video_info["framerate"] == 0: + self.event_generate("<>") + + elif self._audio: + self._time_stamp = round(float(i.pts * self.audio_stream_base), 2) + + audio_data = i.to_ndarray().astype('float32') + interleaved_data = audio_data.T.flatten().tobytes() + audio_device.write(interleaved_data) + if self._paused: + break + if self._stop: + break + + else: + frame = next(self._container.decode(video=0)) + + self._time_stamp = round(float(frame.pts * self.video_stream_base), 2) + + self._current_img = frame.to_image() + 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)) + + self.frame_buffers = [] # flush the buffers + except (StopIteration, av.error.EOFError, tk.TclError): break + self._container.close() self._frame_number = 0 self._paused = True self._load_thread = None - self._container = None + self.frame_buffers = [] + frame = None + video_stream.close() + if audio_device: + audio_device.stop_stream() + audio_device.close() + p.terminate() + audio_stream.close() + try: 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() self.path = path - + self._load_thread = None + self._container = None + self.frame_buffers = [] + def stop(self): - """ stops reading the file """ + """ stops reading the file from reading """ self._paused = True self._stop = True @@ -213,9 +360,8 @@ def play(self): """ plays the video file """ self._paused = False self._stop = False - + if not self._load_thread: - # print("loading new thread...") self._load_thread = threading.Thread(target=self._load, args=(self.path, ), daemon=True) self._load_thread.start() @@ -223,8 +369,12 @@ def is_paused(self): """ returns if the video is paused """ return self._paused + def is_stopped(self): + """ returns if the video is stopped """ + return self._stop + def video_info(self) -> Dict: - """ returns dict containing duration, frame_rate, file""" + """ returns dict containing basic information about the video """ return self._video_info def metadata(self) -> Dict: @@ -234,6 +384,12 @@ def metadata(self) -> Dict: return {} + def mute(self) -> None: + self._audio = False + + def unmute(self) -> None: + self._audio = True + def current_frame_number(self) -> int: """ return current frame number """ return self._frame_number @@ -245,34 +401,17 @@ def current_duration(self) -> float: def current_img(self) -> Image: """ returns current frame image """ return self._current_img - - def _display_frame(self, event): - """ displays the frame on the label """ - - 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): - """ seeks to specific time""" - + + def seek(self, sec: int, any_frame: bool = False, pause: bool = False): + """ seeks to specific time (not accurate) """ self._seek = True - self._seek_sec = sec - \ No newline at end of file + self._seek_sec = sec + self._any_frame = any_frame + self._seek_pause = pause + + def seek_frame(self, frame: int, pause: bool = True, delay: float = 0.3): + """ seeks to specific frame (accurate) """ + self._seek_frame = True + self._frame = frame + self._seek_pause = pause + self._delay = delay if int(delay)<1 else 1.0 diff --git a/tkvideoplayer.egg-info/PKG-INFO b/tkvideoplayer.egg-info/PKG-INFO deleted file mode 100644 index 52225d9..0000000 --- a/tkvideoplayer.egg-info/PKG-INFO +++ /dev/null @@ -1,58 +0,0 @@ -Metadata-Version: 2.1 -Name: tkvideoplayer -Version: 2.3 -Summary: This library helps you play video files in tkinter -Home-page: https://github.com/PaulleDemon/tkVideoPlayer -Author: Paul -License: MIT -Keywords: tkinter,video,player,video player,tkvideoplayer,play video in tkinter -Platform: UNKNOWN -Classifier: License :: OSI Approved :: MIT License -Classifier: Development Status :: 5 - Production/Stable -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Requires-Python: >=3.6 -Description-Content-Type: text/markdown -License-File: LICENSE - -# TkVideoplayer - -This is a simple library to play video files in tkinter. This library also provides the ability to play, pause, -skip and seek to specific timestamps. - -#### Example: -```python -import tkinter as tk -from tkVideoPlayer import TkinterVideo - -root = tk.Tk() - -videoplayer = TkinterVideo(master=root, scaled=True) -videoplayer.load(r"samplevideo.mp4") -videoplayer.pack(expand=True, fill="both") - -videoplayer.play() # play the video - -root.mainloop() -``` - -read the documentation [here](https://github.com/PaulleDemon/tkVideoPlayer/blob/master/Documentation.md) - -> Please immediately upgrade to the latest version if you are using version 1.3 or below -#### Sample video player made using tkVideoPlayer: -![Sample player](https://github.com/PaulleDemon/tkVideoPlayer/blob/master/videoplayer_screenshot.png?raw=True) - -This example source code can be found [here](https://github.com/PaulleDemon/tkVideoPlayer/blob/master/examples/sample_player.py) - - -**Other libraries you might be interested in:** - -* [tkstylesheet](https://pypi.org/project/tkstylesheet/) - Helps you style your tkinter application using stylesheets. - -* [tkTimePicker](https://pypi.org/project/tkTimePicker/) - An easy-to-use timepicker. - -* [PyCollision](https://pypi.org/project/PyCollision/) - Helps you draw hitboxes for 2d games. - diff --git a/tkvideoplayer.egg-info/SOURCES.txt b/tkvideoplayer.egg-info/SOURCES.txt deleted file mode 100644 index d66dd04..0000000 --- a/tkvideoplayer.egg-info/SOURCES.txt +++ /dev/null @@ -1,9 +0,0 @@ -LICENSE -setup.py -tkVideoPlayer/__init__.py -tkVideoPlayer/tkvideoplayer.py -tkvideoplayer.egg-info/PKG-INFO -tkvideoplayer.egg-info/SOURCES.txt -tkvideoplayer.egg-info/dependency_links.txt -tkvideoplayer.egg-info/requires.txt -tkvideoplayer.egg-info/top_level.txt \ No newline at end of file diff --git a/tkvideoplayer.egg-info/dependency_links.txt b/tkvideoplayer.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/tkvideoplayer.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tkvideoplayer.egg-info/requires.txt b/tkvideoplayer.egg-info/requires.txt deleted file mode 100644 index 959518b..0000000 --- a/tkvideoplayer.egg-info/requires.txt +++ /dev/null @@ -1,2 +0,0 @@ -av -pillow diff --git a/tkvideoplayer.egg-info/top_level.txt b/tkvideoplayer.egg-info/top_level.txt deleted file mode 100644 index 9d7f83e..0000000 --- a/tkvideoplayer.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -tkVideoPlayer diff --git a/videoplayer_screenshot.png b/videoplayer_screenshot.png deleted file mode 100644 index 9ac49ac..0000000 Binary files a/videoplayer_screenshot.png and /dev/null differ