From e65f80ce7dd667f6d202830e86f290577f589d9f Mon Sep 17 00:00:00 2001 From: Kadir Aksoy Date: Sun, 13 Jun 2021 15:31:41 +0300 Subject: [PATCH] 1.0.0 --- .gitignore | 6 + README.md | 194 +++++++++++++++++++++++++++++- pygamevideo.py | 314 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 512 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 pygamevideo.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efee2f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +pycache/ +dev/ +dist/ +setup.py +requirements.txt +pygamevideo.egg-info/ diff --git a/README.md b/README.md index 9834ee0..d722886 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,192 @@ -# pygame-video - Video player for Pygame +# Pygame Video Player +`pygamevideo` module helps developer to embed videos into their Pygame display. Audio playback doesn't use `pygame.mixer`. + +## Installing +``` +pip install pygamevideo +``` +or just copy-paste `pygamevideo.py` to your working directory + +## Usage +```py +import pygame +from pygamevideo import Video + +window = pygame.display.set_mode() + +video = Video("video.mp4") + +# start video +video.play() + +# main loop +while True: + ... + + # draw video to display surface + # this function must be called every tick + 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) +``` + +## 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/) + +## Reference + +## `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 diff --git a/pygamevideo.py b/pygamevideo.py new file mode 100644 index 0000000..84e4cb9 --- /dev/null +++ b/pygamevideo.py @@ -0,0 +1,314 @@ +# Pygame Video Player # +# LGPL 3.0 - Kadir Aksoy # +# https://github.com/kadir014/pygamevideo # + + +import time +import os +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()) + + 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 + + s = mr//1000 + sr = mr%1000 + + return cls(hour=h, minute=m, second=s, millisecond=sr) + + + +class Video: + def __init__(self, filepath): + self.is_ready = False + self.load(filepath) + + def __repr__(self): + return f"" + + def load(self, filepath): + filepath = str(filepath) + if not os.path.exists(filepath): + raise FileNotFoundError(f"No such file or directory: '{filepath}'") + + self.filepath = filepath + + self.is_playing = False + self.is_ended = True + self.is_paused = False + self.is_looped = False + + self.draw_frame = 0 + self.start_time = 0 + self.ostart_time = 0 + + self.volume = 1 + self.is_muted = False + + self.vidcap = cv2.VideoCapture(self.filepath) + self.ff = MediaPlayer(self.filepath) + + self.fps = self.vidcap.get(cv2.CAP_PROP_FPS) + + 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.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.is_ready = True + + def release(self): + 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) + + self.is_playing = True + self.is_looped = loop + + self.start_time = time.time() + self.ostart_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.draw_frame = 0 + self.is_paused = False + + self.start_time = time.time() + self.ostart_time = time.time() + + 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.frame_surf = pygame.Surface((self.frame_width, self.frame_height)) + + self.release() + + def pause(self): + self.is_paused = True + sel.ff.set_pause(True) + + def resume(self): + self.is_paused = False + self.ff.set_pause(False) + + # Audio methods + + def mute(self): + self.is_muted = True + self.ff.set_mute(True) + + def unmute(self): + self.is_muted = False + self.ff.set_mute(False) + + def has_audio(self): + pass + + def set_volume(self, volume): + self.volume = volume + self.ff.set_volume(volume) + + # Duration methods & properties + + @property + def duration(self): + return Time.from_millisecond((self.total_frames/self.fps)*1000) + + @property + def current_time(self): + return Time.from_millisecond(self.vidcap.get(cv2.CAP_PROP_POS_MSEC)) + + @property + def remaining_time(self): + return self.duration - self.current_time + + @property + def current_frame(self): + return self.vidcap.get(cv2.CAP_PROP_POS_FRAMES) + + @property + def remaining_frames(self): + 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:]) + + _t = Time(hour=h, minute=m, second=s, millisecond=f) + self.seek_time(_t.to_millisecond()) + + 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) + + else: + raise ValueError("Time can only be represented in Time, str, int or float") + + def seek_frame(self, frame): + 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): + if not self.is_playing: + return self._scaled_frame() + + elapsed_frames = int((time.time() - self.start_time) * self.fps) + + if self.draw_frame >= elapsed_frames: + return self._scaled_frame() + + else: + target_frames = elapsed_frames - self.draw_frame + self.draw_frame += target_frames + + if not self.is_paused: + for _ in range(target_frames): + success, frame = self.vidcap.read() + audio_frame, val = self.ff.get_frame() + + if not success: + if self.is_looped: + self.restart() + return + else: + self.stop() + return + + pygame.pixelcopy.array_to_surface(self.frame_surf, numpy.flip(numpy.rot90(frame[::-1]))) + + return self._scaled_frame() + + def draw_to(self, surface, pos): + frame = self.get_frame() + + surface.blit(frame, pos)