diff --git a/.gitignore b/.gitignore
index efee2f9..28c038d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
-pycache/
+__pycache__/
dev/
dist/
setup.py
-requirements.txt
pygamevideo.egg-info/
diff --git a/LICENSE b/LICENSE
index 8805d0d..bbc4370 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,165 +1,21 @@
-GNU LESSER GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-
- This version of the GNU Lesser General Public License incorporates
-the terms and conditions of version 3 of the GNU General Public
-License, supplemented by the additional permissions listed below.
-
- 0. Additional Definitions.
-
- As used herein, "this License" refers to version 3 of the GNU Lesser
-General Public License, and the "GNU GPL" refers to version 3 of the GNU
-General Public License.
-
- "The Library" refers to a covered work governed by this License,
-other than an Application or a Combined Work as defined below.
-
- An "Application" is any work that makes use of an interface provided
-by the Library, but which is not otherwise based on the Library.
-Defining a subclass of a class defined by the Library is deemed a mode
-of using an interface provided by the Library.
-
- A "Combined Work" is a work produced by combining or linking an
-Application with the Library. The particular version of the Library
-with which the Combined Work was made is also called the "Linked
-Version".
-
- The "Minimal Corresponding Source" for a Combined Work means the
-Corresponding Source for the Combined Work, excluding any source code
-for portions of the Combined Work that, considered in isolation, are
-based on the Application, and not on the Linked Version.
-
- The "Corresponding Application Code" for a Combined Work means the
-object code and/or source code for the Application, including any data
-and utility programs needed for reproducing the Combined Work from the
-Application, but excluding the System Libraries of the Combined Work.
-
- 1. Exception to Section 3 of the GNU GPL.
-
- You may convey a covered work under sections 3 and 4 of this License
-without being bound by section 3 of the GNU GPL.
-
- 2. Conveying Modified Versions.
-
- If you modify a copy of the Library, and, in your modifications, a
-facility refers to a function or data to be supplied by an Application
-that uses the facility (other than as an argument passed when the
-facility is invoked), then you may convey a copy of the modified
-version:
-
- a) under this License, provided that you make a good faith effort to
- ensure that, in the event an Application does not supply the
- function or data, the facility still operates, and performs
- whatever part of its purpose remains meaningful, or
-
- b) under the GNU GPL, with none of the additional permissions of
- this License applicable to that copy.
-
- 3. Object Code Incorporating Material from Library Header Files.
-
- The object code form of an Application may incorporate material from
-a header file that is part of the Library. You may convey such object
-code under terms of your choice, provided that, if the incorporated
-material is not limited to numerical parameters, data structure
-layouts and accessors, or small macros, inline functions and templates
-(ten or fewer lines in length), you do both of the following:
-
- a) Give prominent notice with each copy of the object code that the
- Library is used in it and that the Library and its use are
- covered by this License.
-
- b) Accompany the object code with a copy of the GNU GPL and this license
- document.
-
- 4. Combined Works.
-
- You may convey a Combined Work under terms of your choice that,
-taken together, effectively do not restrict modification of the
-portions of the Library contained in the Combined Work and reverse
-engineering for debugging such modifications, if you also do each of
-the following:
-
- a) Give prominent notice with each copy of the Combined Work that
- the Library is used in it and that the Library and its use are
- covered by this License.
-
- b) Accompany the Combined Work with a copy of the GNU GPL and this license
- document.
-
- c) For a Combined Work that displays copyright notices during
- execution, include the copyright notice for the Library among
- these notices, as well as a reference directing the user to the
- copies of the GNU GPL and this license document.
-
- d) Do one of the following:
-
- 0) Convey the Minimal Corresponding Source under the terms of this
- License, and the Corresponding Application Code in a form
- suitable for, and under terms that permit, the user to
- recombine or relink the Application with a modified version of
- the Linked Version to produce a modified Combined Work, in the
- manner specified by section 6 of the GNU GPL for conveying
- Corresponding Source.
-
- 1) Use a suitable shared library mechanism for linking with the
- Library. A suitable mechanism is one that (a) uses at run time
- a copy of the Library already present on the user's computer
- system, and (b) will operate properly with a modified version
- of the Library that is interface-compatible with the Linked
- Version.
-
- e) Provide Installation Information, but only if you would otherwise
- be required to provide such information under section 6 of the
- GNU GPL, and only to the extent that such information is
- necessary to install and execute a modified version of the
- Combined Work produced by recombining or relinking the
- Application with a modified version of the Linked Version. (If
- you use option 4d0, the Installation Information must accompany
- the Minimal Corresponding Source and Corresponding Application
- Code. If you use option 4d1, you must provide the Installation
- Information in the manner specified by section 6 of the GNU GPL
- for conveying Corresponding Source.)
-
- 5. Combined Libraries.
-
- You may place library facilities that are a work based on the
-Library side by side in a single library together with other library
-facilities that are not Applications and are not covered by this
-License, and convey such a combined library under terms of your
-choice, if you do both of the following:
-
- a) Accompany the combined library with a copy of the same work based
- on the Library, uncombined with any other library facilities,
- conveyed under the terms of this License.
-
- b) Give prominent notice with the combined library that part of it
- is a work based on the Library, and explaining where to find the
- accompanying uncombined form of the same work.
-
- 6. Revised Versions of the GNU Lesser General Public License.
-
- The Free Software Foundation may publish revised and/or new versions
-of the GNU Lesser General Public License from time to time. Such new
-versions will be similar in spirit to the present version, but may
-differ in detail to address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Library as you received it specifies that a certain numbered version
-of the GNU Lesser General Public License "or any later version"
-applies to it, you have the option of following the terms and
-conditions either of that published version or of any later version
-published by the Free Software Foundation. If the Library as you
-received it does not specify a version number of the GNU Lesser
-General Public License, you may choose any version of the GNU Lesser
-General Public License ever published by the Free Software Foundation.
-
- If the Library as you received it specifies that a proxy can decide
-whether future versions of the GNU Lesser General Public License shall
-apply, that proxy's public statement of acceptance of any version is
-permanent authorization for you to choose that version for the
-Library.
+MIT License
+
+Copyright (c) 2023 Kadir Aksoy
+
+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.
\ No newline at end of file
diff --git a/README.md b/README.md
index d722886..5a91183 100644
--- a/README.md
+++ b/README.md
@@ -1,192 +1,92 @@
-# Pygame Video Player
-`pygamevideo` module helps developer to embed videos into their Pygame display. Audio playback doesn't use `pygame.mixer`.
+# Pygame Video Player 📺
+
+
+
+
+
+This module provides a simple API to let developers use videos in their Pygame apps. Audio playback doesn't use `pygame.mixer`.
## Installing
```
pip install pygamevideo
```
-or just copy-paste `pygamevideo.py` to your working directory
+or just copy-paste `pygamevideo.py` to your working directory.
## Usage
```py
import pygame
from pygamevideo import Video
-window = pygame.display.set_mode()
+pygame.init()
+window = pygame.display.set_mode(...)
+# Load the video from the specified dir
video = Video("video.mp4")
-# start video
+# Start the video
video.play()
-# main loop
+# Main loop
while True:
...
- # draw video to display surface
- # this function must be called every tick
+ # Draw video to display surface
+ # this function should be called every frame
video.draw_to(window, (0, 0))
- # set window title to current duration of video as hour:minute:second
- t = video.current_time.format("%h:%m:%s")
- pygame.display.set_caption(t)
+ # Update pygame display
+ pygame.display.flip()
```
## Dependencies
-- [pygame](https://pypi.org/project/pygame/)
-- [numpy](https://pypi.org/project/numpy/)
-- [opencv-python](https://pypi.org/project/opencv-python/)
-- [ffpyplayer](https://pypi.org/project/ffpyplayer/)
+- [Python](https://www.python.org/downloads/) 3.9+
+- [Pygame Community Edition](https://github.com/pygame-community/pygame-ce) 2.20.0+
+- [NumPy](https://pypi.org/project/numpy/)
+- [OpenCV](https://pypi.org/project/opencv-python/)
+- [FFPyPlayer](https://pypi.org/project/ffpyplayer/)
-## Reference
+# API Reference
+You can just use the docstrings as well.
## `class Video(filepath)`
-Pygame video player class
-
-### Parameters
-`filepath` : Filepath of the video source
-
-### Methods & Attributes
-`load(filepath)` : Load another video source
-
-
-
-`release()` : Release resources
-
-#### Related to playback control
-`play(loop=True)` : Starts video playback
- - `loop` : Is video looped or not
-
-
-
-`restart()` : Restarts already playing video
-
-
-
-`stop()` : Stops video
-
-
-
-`pause()` : Pauses video
-
-
-
-`resume()` : Resumes video
-
-
-
-`is_playing` : Whether the video is playing or not (bool)
-
-
-
-`is_ended` : Whether the video has ended or not (bool)
-
-
-
-`is_paused` : Whether the video is paused or not (bool)
-
-
-
-`is_ready` : Whether the resources and video is ready to play (bool)
-
-### Related to audio control
-`mute()` : Mutes audio
-
-
-
-`unmute()` : Unmutes audio
-
-
-
-`has_audio()` : **NOT IMPLEMENTED**
-
-
-
-`set_volume(volume)` : Sets audio volume
- - `volume` : Floating number between 0.0 and 1.0
-
-
-
-`is_muted` : Whether the audio is muted or not (bool)
-
-
-
-`volume` : Audio volume (float)
-
-### Related to timing control
-`duration` : Length of the video as [Time](#Time) object
-
-
-
-`current_time` : Current time of the video as [Time](#Time) object
-
-
-
-`remaining_time` : Remaining time till the end of the video as [Time](#Time) object
-
-
-
-`total_frames` : Length of the video as frames (int)
-
-
-
-`current_frame` : Current frame of the video (int)
-
-
-
-`remaining_frames` : Remaining frames till the end of the video (int)
-
-
-
-`seek_time(t)` : Jump into a specific time of the video
- - `t` : [Time](#Time) object
- - `t` : Representation of time in string, eg: "00:01:05:200" meaning 1 minute, 5 seconds and 200 milliseconds (str)
- - `t` : Milliseconds (int)
-
-
-
-`seek_frame(frame)` : Jump into a specific frame of the video
-- `frame` : Frame number (int)
-
-### Related to resizing & frame dimensions
-`get_size()` : Returns video size (tuple)
-
-
-
-`get_width()` : Returns video width (int)
-
-
-
-`get_height()` : Returns video height (int)
-
-
-
-`set_size(size)` : Resizes video
- - `size` : New size (tuple)
-
-
-
-`set_width(width)` : Resizes video
- - `width` : New width (int)
-
-
-
-`set_height(height)` : Resizes video
- - `height` : New height (int)
-
-
-
-`keep_aspect_ratio` : Keeps original aspect ratio while resizing the video (bool)
-
-### Drawing the video
-`draw_to(surface, pos)` : Draws the video onto the surface. This functions must be called every tick.
- - `surface` : Destination surface (pygame.Surface)
- - `pos` : Blitting position (tuple)
-
-
-
-`get_frame()` : Returns the current video frame as pygame.Surface. This function is used by `draw_to` function, so use only one of both each tick
-
-
-## `class Time`
-Data class used to represent duration and such things by `Video` class
+Pygame video player class.
+
+- ### Parameters
+ `filepath` : Filepath of the video source.
+
+- ### Attributes & Properties
+ - `is_ready` : Is the video source loaded and ready to play?
+ - `frame_width` : Default frame width in pixels.
+ - `frame_height` : Default frame height in pixels.
+ - `is_playing` : Is the video currently playing?
+ - `is_paused` : Is the video currently paused?
+ - `is_looped` : Is looping enabled?
+ - `volume` : Volume of the audio.
+ - `is_muted` : Is the audio muted?
+ - `fps` : Framerate of the video.
+ - `total_frames` : Total amount of frames of the video.
+ - `duration` : Total duration of the video in milliseconds.
+ - `current_time` : Current time into the video in milliseconds.
+ - `remaining_time` : Remaining time left in the video in milliseconds.
+ - `current_frame` : Current frame into the video.
+ - `remaining_frames` : Remaining frames left in the video.
+ - `current_time` : Current time into the video in milliseconds.
+
+- ### Methods
+ - `load(filepath: Union[str, os.PathLike])` : Load a video from file path. This method is also called implicitly when instantiated.
+ - `reload()` : Reload the video from the same filepath.
+ - `release()` : Release the resources used by the video player.
+ - `play(loop: bool = False)` : Start playing the video.
+ - `stop()` : Stop playing the video.
+ - `pause()` : Pause the video.
+ - `resume()` : Resume the video.
+ - `toggle_pause()` : Switch between paused states.
+ - `mute()` : Mute audio playback.
+ - `unmute()` : Unmute audio playback.
+ - `seek_time(timepoint: float)` : Seek into desired timepoint.
+ - `seek_frame(frame: int)` : Seek into desired frame.
+ - `get_frame()` : Advance the video and return the current frame. Must be called once per frame.
+ - `draw_to(dest_surface: pygame.Surface, position: Coordinate)` : Blit the current video frame to the surface.
+
+# License
+[MIT](LICENSE) © Kadir Aksoy
\ No newline at end of file
diff --git a/examples/bunny.mp4 b/examples/bunny.mp4
new file mode 100644
index 0000000..139380e
Binary files /dev/null and b/examples/bunny.mp4 differ
diff --git a/examples/example.py b/examples/example.py
new file mode 100644
index 0000000..fc8d41b
--- /dev/null
+++ b/examples/example.py
@@ -0,0 +1,68 @@
+from time import perf_counter
+from datetime import timedelta
+
+import pygame
+
+from pygamevideo import Video
+
+
+pygame.init()
+window = pygame.display.set_mode((1280, 720))
+clock = pygame.time.Clock()
+
+font18 = pygame.font.SysFont("Arial", 18)
+font14 = pygame.font.SysFont("Arial", 14)
+
+
+start = perf_counter()
+video = Video("bunny.mp4")
+elapsed = perf_counter() - start
+
+video.play(loop=True)
+
+
+titlebar_surf = pygame.Surface((1280, 35)).convert()
+titlebar_surf.set_alpha(180)
+
+control_surf = pygame.Surface((1280, 50)).convert()
+control_surf.set_alpha(180)
+
+pygame.draw.rect(control_surf, (100, 100, 100), (153, 22, 1100, 4), 0)
+
+
+while True:
+ clock.tick(0)
+ pygame.display.set_caption(f"Pygame Video Player @{round(clock.get_fps())}FPS")
+
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ raise SystemExit(0)
+
+ window.fill((25, 18, 41))
+
+ video.draw_to(window, (0, 0))
+
+ t = timedelta(milliseconds=video.current_time)
+ h, rem = divmod(t.seconds, 3600)
+ m, s = divmod(rem, 60)
+ time_formatted = f"{str(h).zfill(2)}:{str(m).zfill(2)}:{str(s).zfill(2)}"
+
+ t = timedelta(milliseconds=video.duration)
+ h, rem = divmod(t.seconds, 3600)
+ m, s = divmod(rem, 60)
+ total_time = f"{str(h).zfill(2)}:{str(m).zfill(2)}:{str(s).zfill(2)}"
+
+ window.blit(titlebar_surf, (0, 0))
+ window.blit(font18.render(f"{video.filepath} {round(video.fps, 2)}FPS {video.total_frames} frames {video.frame_width}x{video.frame_height} Loaded in {round(elapsed * 1000, 1)}ms", True, (255, 255, 255)), (10, 7))
+
+ window.blit(control_surf, (0, 720 - 50))
+ window.blit(font14.render(f"{time_formatted} / {total_time}", True, (255, 255, 255)), (5, 720 - 32))
+
+ percentage = video.current_frame / video.total_frames
+ x = percentage * 1100
+
+ pygame.draw.rect(window, (156, 135, 250), (153, 720 - 50 + 22, x, 4), 0)
+
+ pygame.draw.circle(window, (255, 255, 255), (153 + x, 720 - 50 + 24), 7, 0)
+
+ pygame.display.flip()
\ No newline at end of file
diff --git a/pygamevideo.py b/pygamevideo.py
index efa2966..014a927 100644
--- a/pygamevideo.py
+++ b/pygamevideo.py
@@ -1,81 +1,52 @@
-# Pygame Video Player #
-# LGPL 3.0 - Kadir Aksoy #
-# https://github.com/kadir014/pygamevideo #
-
+from typing import Union
import time
import os
+import atexit
+
import pygame
import numpy
import cv2
from ffpyplayer.player import MediaPlayer
-__version__ = "1.0.0"
-
-
-
-class Time:
- def __init__(self, hour=0, minute=0, second=0, millisecond=0):
- self.hour, self.minute, self.second, self.millisecond = hour, minute, second, millisecond
-
- def __repr__(self):
- return self.format("")
-
- def __add__(self, other):
- return Time.from_millisecond(self.to_millisecond() + other.to_millisecond())
-
- def __sub__(self, other):
- return Time.from_millisecond(self.to_millisecond() - other.to_millisecond())
+__version__ = "2.0.0"
- def format(self, format_string):
- if "%H" in format_string: format_string = format_string.replace("%H", str(self.hour).zfill(2))
- if "%M" in format_string: format_string = format_string.replace("%M", str(self.minute).zfill(2))
- if "%S" in format_string: format_string = format_string.replace("%S", str(self.second).zfill(2))
- if "%F" in format_string: format_string = format_string.replace("%F", str(self.millisecond).zfill(2))
- if "%h" in format_string: format_string = format_string.replace("%h", str(int(self.hour)).zfill(2))
- if "%m" in format_string: format_string = format_string.replace("%m", str(int(self.minute)).zfill(2))
- if "%s" in format_string: format_string = format_string.replace("%s", str(int(self.second)).zfill(2))
- if "%f" in format_string: format_string = format_string.replace("%f", str(int(self.millisecond)).zfill(2))
- return format_string
-
- def to_hour(self):
- return self.hour + self.minute/60 + self.second/3600 + self.millisecond/3600000
-
- def to_minute(self):
- return self.hour*60 + self.minute + self.second/60 + self.millisecond/60000
-
- def to_second(self):
- return self.hour*3600 + self.minute*60 + self.second + self.millisecond/1000
-
- def to_millisecond(self):
- return self.hour*3600000 + self.minute*60000 + self.second*1000 + self.millisecond
-
- @classmethod
- def from_millisecond(cls, ms):
- h = ms//3600000
- hr = ms%3600000
-
- m = hr//60000
- mr = hr%60000
+class Video:
+ """
+ Pygame video player.
+ """
- s = mr//1000
- sr = mr%1000
+ def __init__(self, filepath: Union[str, os.PathLike]) -> None:
+ """
+ Parameters
+ ----------
+ @param filepath File path to the video to load.
+ """
- return cls(hour=h, minute=m, second=s, millisecond=sr)
+ self.is_ready = False
+ self.load(filepath)
+ # Release before exiting interpreter to prevent segfault
+ atexit.register(self.release)
+ def __repr__(self) -> str:
+ return f"<{__name__}.{self.__class__.__name__}(frame#{self.current_frame})>"
+
+ def __del__(self) -> None:
+ self.release()
-class Video:
- def __init__(self, filepath):
- self.is_ready = False
- self.load(filepath)
+ def load(self, filepath: Union[str, os.PathLike]) -> None:
+ """
+ Load a video from file path.
+ This method is also called implicitly when instantiated.
- def __repr__(self):
- return f""
+ Parameters
+ ----------
+ @param filepath File path to the video to load.
+ """
- def load(self, filepath):
filepath = str(filepath)
if not os.path.exists(filepath):
raise FileNotFoundError(f"No such file or directory: '{filepath}'")
@@ -83,209 +54,190 @@ def load(self, filepath):
self.filepath = filepath
self.is_playing = False
- self.is_ended = True
- self.is_paused = False
- self.is_looped = False
+ self.is_paused = False
+ self.is_looped = False
- self.draw_frame = 0
- self.start_time = 0
- self.ostart_time = 0
+ self.draw_frame = 0
+ self.start_time = 0
+ self.__start_time = 0
- self.volume = 1
+ self.__volume = 1.0
self.is_muted = False
+ self.__volume_before_mute = 1.0
- self.vidcap = cv2.VideoCapture(self.filepath)
- self.ff = MediaPlayer(self.filepath)
+ self.__vidcap = cv2.VideoCapture(self.filepath)
+ ff_opts = {"fast": True, "framedrop": True, "paused": True}
+ self.__ff = MediaPlayer(self.filepath, ff_opts=ff_opts)
- self.fps = self.vidcap.get(cv2.CAP_PROP_FPS)
+ self.fps = self.__vidcap.get(cv2.CAP_PROP_FPS)
- self.total_frames = int(self.vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
+ self.total_frames = int(self.__vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
- self.frame_width = int(self.vidcap.get(cv2.CAP_PROP_FRAME_WIDTH))
- self.frame_height = int(self.vidcap.get(cv2.CAP_PROP_FRAME_HEIGHT))
+ self.frame_width = int(self.__vidcap.get(cv2.CAP_PROP_FRAME_WIDTH))
+ self.frame_height = int(self.__vidcap.get(cv2.CAP_PROP_FRAME_HEIGHT))
- self.aspect_ratio = self.frame_width / self.frame_height
- self.aspect_ratio2 = self.frame_height / self.frame_width
- self.keep_aspect_ratio = False
-
- self.frame_surf = pygame.Surface((self.frame_width, self.frame_height))
- self._aspect_surf = pygame.Surface((self.frame_width, self.frame_height))
+ self.frame_surf = pygame.Surface((self.frame_width, self.frame_height)).convert()
self.is_ready = True
- def release(self):
- self.vidcap.release()
- self.ff.close_player()
- self.is_ready = False
+ def reload(self) -> None:
+ """ Reload the video from the same filepath. """
+
+ self.release()
+ self.load(self.filepath)
+
+ def release(self) -> None:
+ """ Release the resources used by the video player. """
+
+ if self.is_ready:
+ self.__vidcap.release()
+ self.__ff.close_player()
+ self.is_ready = False
# Control methods
- def play(self, loop=False):
- if not self.is_playing:
- if not self.is_ready: self.load(self.filepath)
+ def play(self, loop: bool = False) -> None:
+ """
+ Start playing the video.
+
+ Parameters
+ ----------
+ @param loop Whether to loop or not when the video playback ends.
+ """
+ if self.is_ready and not self.is_playing:
self.is_playing = True
self.is_looped = loop
+ self.seek_frame(0)
+ self.draw_frame = 0
self.start_time = time.time()
- self.ostart_time = time.time()
+ self.__start_time = time.time()
- def restart(self):
- if self.is_playing:
- self.release()
- self.vidcap = cv2.VideoCapture(self.filepath)
- self.ff = MediaPlayer(self.filepath)
- self.is_ready = True
+ self.__ff.set_pause(False)
- self.draw_frame = 0
- self.is_paused = False
+ def stop(self) -> None:
+ """
+ Stop playing the video.
+ """
- self.start_time = time.time()
- self.ostart_time = time.time()
+ self.is_playing = False
+ self.is_paused = False
+ self.__ff.set_pause(True)
+
+ def pause(self) -> None:
+ """ Pause the video. """
- def stop(self):
if self.is_playing:
- self.is_playing = False
- self.is_paused = False
- self.is_ended = True
- self.is_looped = False
- self.draw_frame = 0
+ self.is_paused = True
+ self.__ff.set_pause(True)
- self.frame_surf = pygame.Surface((self.frame_width, self.frame_height))
+ def resume(self) -> None:
+ """ Resume the video. """
- self.release()
+ if self.is_playing:
+ self.is_paused = False
+ self.__ff.set_pause(False)
- def pause(self):
- self.is_paused = True
- self.ff.set_pause(True)
+ def toggle_pause(self) -> None:
+ """ Switch between paused states. """
- def resume(self):
- self.is_paused = False
- self.ff.set_pause(False)
+ if self.is_playing:
+ if self.is_paused: self.resume()
+ else: self.pause()
# Audio methods
- def mute(self):
+ def mute(self) -> None:
+ """ Mute audio playback. """
+ # MediaPlayer.set_mute doesn't work!
self.is_muted = True
- self.ff.set_mute(True)
+ self.__volume_before_mute = self.volume
+ self.__ff.set_volume(0.0)
- def unmute(self):
+ def unmute(self) -> None:
+ """ Unmute audio playback. """
self.is_muted = False
- self.ff.set_mute(False)
-
- def has_audio(self):
- pass
+ self.__ff.set_volume(self.__volume_before_mute)
- def set_volume(self, volume):
- self.volume = volume
- self.ff.set_volume(volume)
+ @property
+ def volume(self) -> float:
+ """ Volume of the audio playback. """
+ return self.__volume
+
+ @volume.setter
+ def volume(self, value: float) -> None:
+ self.__volume = value
+ if not self.is_muted:
+ self.__ff.set_volume(value)
# Duration methods & properties
@property
- def duration(self):
- return Time.from_millisecond((self.total_frames/self.fps)*1000)
+ def duration(self) -> float:
+ """ Total duration of the video in milliseconds. """
+ return (self.total_frames / self.fps) * 1000
@property
- def current_time(self):
- return Time.from_millisecond(self.vidcap.get(cv2.CAP_PROP_POS_MSEC))
+ def current_time(self) -> float:
+ """ Current time into the video in milliseconds. """
+ if not self.is_ready or not self.is_playing: return 0
+ return self.__vidcap.get(cv2.CAP_PROP_POS_MSEC)
@property
- def remaining_time(self):
+ def remaining_time(self) -> float:
+ """ Remaining time left in the video in milliseconds. """
return self.duration - self.current_time
@property
- def current_frame(self):
- return self.vidcap.get(cv2.CAP_PROP_POS_FRAMES)
+ def current_frame(self) -> int:
+ """ Current frame into the video. """
+ return self.__vidcap.get(cv2.CAP_PROP_POS_FRAMES)
@property
- def remaining_frames(self):
+ def remaining_frames(self) -> int:
+ """ Remaining frames left in the video. """
return self.frame_count - self.current_frame
- def seek_time(self, t):
- if isinstance(t, Time):
- _t = t.to_millisecond()
- self.seek_time(_t)
-
- elif isinstance(t, str):
- h = float(t[:2])
- m = float(t[3:5])
- s = float(t[6:8])
- f = float(t[9:])
+ def seek_time(self, timepoint: float) -> None:
+ """
+ Seek into desired timepoint.
- _t = Time(hour=h, minute=m, second=s, millisecond=f)
- self.seek_time(_t.to_millisecond())
+ Parameters
+ ----------
+ @param time Time in milliseconds to seek to.
+ """
- elif isinstance(t, (int, float)):
- self.start_time = self.ostart_time + t/1000
- self.draw_frame = int((time.time() - self.start_time) * self.fps)
- self.vidcap.set(cv2.CAP_PROP_POS_MSEC, t)
- self.ff.seek(t/1000, relative=False)
+ self.start_time = self.__start_time + timepoint / 1000
+ self.draw_frame = int((time.time() - self.start_time) * self.fps)
+ self.__vidcap.set(cv2.CAP_PROP_POS_MSEC, timepoint)
+ self.__ff.seek(timepoint / 1000, relative=False)
- else:
- raise ValueError("Time can only be represented in Time, str, int or float")
+ def seek_frame(self, frame: int) -> None:
+ """
+ Seek into desired frame.
- def seek_frame(self, frame):
+ Parameters
+ ----------
+ @param frame Frame number to seek to.
+ """
self.seek_time((frame / self.fps) * 1000)
- # Dimension methods
-
- def get_size(self):
- return (self.frame_width, self.frame_height)
-
- def get_width(self):
- return self.frame_width
-
- def get_height(self):
- return self.frame_height
-
- def set_size(self, size):
- self.frame_width, self.frame_height = size
- self._aspect_surf = pygame.transform.scale(self._aspect_surf, (self.frame_width, self.frame_height))
-
- if not (self.frame_width > 0 and self.frame_height > 0):
- raise ValueError(f"Size must be positive")
-
- def set_width(self, width):
- self.frame_width = width
- self._aspect_surf = pygame.transform.scale(self._aspect_surf, (self.frame_width, self.frame_height))
-
- if self.frame_width <= 0:
- raise ValueError(f"Width must be positive")
-
- def set_height(self, height):
- self.frame_height = height
- self._aspect_surf = pygame.transform.scale(self._aspect_surf, (self.frame_width, self.frame_height))
-
- if self.frame_height <= 0:
- raise ValueError(f"Height must be positive")
-
# Process & draw video
- def _scaled_frame(self):
- if self.keep_aspect_ratio:
- if self.frame_width < self.frame_height:
- self._aspect_surf.fill((0, 0, 0))
- frame_surf = pygame.transform.scale(self.frame_surf, (self.frame_width, int(self.frame_width/self.aspect_ratio)))
- self._aspect_surf.blit(frame_surf, (0, self.frame_height/2-frame_surf.get_height()/2))
- return self._aspect_surf
-
- else:
- self._aspect_surf.fill((0, 0, 0))
- frame_surf = pygame.transform.scale(self.frame_surf, (int(self.frame_height/self.aspect_ratio2), self.frame_height))
- self._aspect_surf.blit(frame_surf, (self.frame_width/2-frame_surf.get_width()/2, 0))
- return self._aspect_surf
- else:
- return pygame.transform.scale(self.frame_surf, (self.frame_width, self.frame_height))
+ def get_frame(self) -> pygame.Surface:
+ """
+ Advance the video and return the current frame.
+ Must be called once per frame.
+ """
- def get_frame(self):
- if not self.is_playing:
- return self._scaled_frame()
+ if not self.is_playing or not self.is_ready:
+ return self.frame_surf
elapsed_frames = int((time.time() - self.start_time) * self.fps)
if self.draw_frame >= elapsed_frames:
- return self._scaled_frame()
+ return self.frame_surf
else:
target_frames = elapsed_frames - self.draw_frame
@@ -293,22 +245,37 @@ def get_frame(self):
if not self.is_paused:
for _ in range(target_frames):
- success, frame = self.vidcap.read()
- audio_frame, val = self.ff.get_frame()
+ success, frame = self.__vidcap.read()
if not success:
if self.is_looped:
- self.restart()
- return
+ self.seek_frame(0)
+ return self.frame_surf
+
else:
self.stop()
- return
+ return self.frame_surf
+
+ pygame.pixelcopy.array_to_surface(
+ self.frame_surf,
+ numpy.flip(numpy.rot90(frame[::-1]))
+ )
+
+ return self.frame_surf
- pygame.pixelcopy.array_to_surface(self.frame_surf, numpy.flip(numpy.rot90(frame[::-1])))
+ def draw_to(self,
+ dest_surface: pygame.Surface,
+ position: Union[tuple[float, float], pygame.Vector2, pygame.Rect]
+ ) -> None:
+ """
+ Blit the current video frame to the surface.
- return self._scaled_frame()
+ Parameters
+ ----------
+ @param dest_surface Destination surface to draw the video on.
+ @param position Position to draw the video frame at.
+ """
- def draw_to(self, surface, pos):
frame = self.get_frame()
- surface.blit(frame, pos)
+ dest_surface.blit(frame, position)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..a37e846
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+pygame-ce
+numpy
+opencv-python
+ffpyplayer