This commit is contained in:
Rich
2023-12-07 15:22:21 +00:00
parent a36e9d92ed
commit e446f9cce4
50 changed files with 766 additions and 360 deletions

299
pyvidplayer2/video.py Normal file
View File

@@ -0,0 +1,299 @@
import cv2
import subprocess
import os
from typing import Tuple
from threading import Thread
from .pyaudio_handler import PyaudioHandler
try:
import pygame
except ImportError:
pass
else:
from .mixer_handler import MixerHandler
class Video:
def __init__(self, path: str, chunk_size, max_threads, max_chunks, subs, post_process, interp, use_pygame_audio) -> None:
self.path = path
self.name, self.ext = os.path.splitext(os.path.basename(self.path))
self._vid = cv2.VideoCapture(self.path)
if not self._vid.isOpened():
raise FileNotFoundError(f'Could not find "{self.path}"')
# file information
self.frame_count = int(self._vid.get(cv2.CAP_PROP_FRAME_COUNT))
self.frame_rate = self._vid.get(cv2.CAP_PROP_FPS)
self.frame_delay = 1 / self.frame_rate
self.duration = self.frame_count / self.frame_rate
self.original_size = (int(self._vid.get(cv2.CAP_PROP_FRAME_WIDTH)), int(self._vid.get(cv2.CAP_PROP_FRAME_HEIGHT)))
self.current_size = self.original_size
self.aspect_ratio = self.original_size[0] / self.original_size[1]
self.chunk_size = chunk_size
self.max_chunks = max_chunks
self.max_threads = max_threads
self._chunks = []
self._threads = []
self._starting_time = 0
self._chunks_claimed = 0
self._chunks_played = 0
self._stop_loading = False
self.frame = 0
self.frame_data = None
self.frame_surf = None
self.active = False
self.buffering = False
self.paused = False
self.muted = False
self.subs = subs
self.post_func = post_process
self.interp = interp
self.use_pygame_audio = use_pygame_audio
if use_pygame_audio:
try:
self._audio = MixerHandler()
except NameError:
raise ModuleNotFoundError("Unable to use Pygame audio because Pygame is not installed.")
else:
self._audio = PyaudioHandler()
self.speed = 1
self._missing_ffmpeg = False # for throwing errors
self.play()
def _chunks_len(self) -> int:
i = 0
for c in self._chunks:
if c is not None:
i += 1
return i
def _convert_seconds(self, seconds: float) -> str:
h = int(seconds // 3600)
seconds = seconds % 3600
m = int(seconds // 60)
s = int(seconds % 60)
d = round(seconds % 1, 1)
return f"{h}:{m}:{s}.{int(d * 10)}"
def _threaded_load(self, index) -> None:
i = index # assigned to variable so another thread does not change it
self._chunks.append(None)
s = self._convert_seconds((self._starting_time + (self._chunks_claimed - 1) * self.chunk_size) * (1 / self.speed))
command = [
"ffmpeg",
"-i",
self.path,
"-ss",
str(s),
"-t",
str(self._convert_seconds(self.chunk_size)),
"-vn",
"-f",
"wav",
"-loglevel",
"quiet",
"-"
]
filters = []
if self.speed != 1:
filters += ["-filter:a", f"atempo={self.speed}"]
command = command[:7] + filters + command[7:]
try:
p = subprocess.run(command, capture_output=True)
except FileNotFoundError:
self._missing_ffmpeg = True
self._chunks[i - self._chunks_played - 1] = p.stdout
def _update_threads(self) -> None:
for t in self._threads:
if not t.is_alive():
self._threads.remove(t)
self._stop_loading = self._starting_time + self._chunks_claimed * self.chunk_size >= self.duration
if not self._stop_loading and len(self._threads) < self.max_threads and self._chunks_len() + len(self._threads) < self.max_chunks:
self._chunks_claimed += 1
self._threads.append(Thread(target=self._threaded_load, args=(self._chunks_claimed,)))
self._threads[-1].start()
def _write_subs(self) -> None:
p = self.get_pos()
if p >= self.subs.start:
if p > self.subs.end:
if self.subs._get_next():
self._write_subs()
else:
self.subs._write_subs(self.frame_surf)
def _update(self) -> bool:
if self._missing_ffmpeg:
raise FileNotFoundError("Could not find FFMPEG. Make sure it's downloaded and accessible via $PATH.")
self._update_threads()
n = False
self.buffering = False
if self._audio.get_busy() or self.paused:
while self.get_pos() > self.frame * self.frame_delay:
has_frame, data = self._vid.read()
self.frame += 1
if has_frame:
if self.original_size != self.current_size:
data = cv2.resize(data, dsize=self.current_size, interpolation=self.interp)
data = self.post_func(data)
self.frame_data = data
self.frame_surf = self._create_frame(data)
if self.subs is not None:
self._write_subs()
n = True
else:
break
elif self.active:
if self._chunks and self._chunks[0] is not None:
self._chunks_played += 1
self._audio.load(self._chunks.pop(0))
self._audio.play()
elif self._stop_loading and self._chunks_played == self._chunks_claimed:
self.stop()
else:
self.buffering = True
return n
def mute(self) -> None:
self.muted = True
self._audio.mute()
def unmute(self) -> None:
self.muted = False
self._audio.unmute()
def set_speed(self, speed: float) -> None:
speed = max(0.5, min(10, speed))
if speed != self.speed:
self.speed = speed
self.seek(0) # must reload audio chunks
def get_speed(self) -> float:
return self.speed
def play(self) -> None:
self.active = True
def stop(self) -> None:
self.restart()
self.active = False
self.frame_data = None
self.frame_surf = None
self.paused = False
def resize(self, size: Tuple[int, int]) -> None:
self.current_size = size
def change_resolution(self, height: int) -> None:
self.current_size = (int(height * self.aspect_ratio), height)
def close(self) -> None:
self.stop()
self._vid.release()
self._audio.unload()
for t in self._threads:
t.join()
if not self.use_pygame_audio:
self._audio.close()
def restart(self) -> None:
self.seek(0, relative=False)
self.play()
def set_volume(self, vol: float) -> None:
self._audio.set_volume(vol)
def get_volume(self) -> float:
return self._audio.get_volume()
def get_paused(self) -> bool:
# here because the original pyvidplayer had get_paused
return self.paused
def toggle_pause(self) -> None:
self.resume() if self.paused else self.pause()
def toggle_mute(self) -> None:
self.unmute() if self.muted else self.mute()
def pause(self) -> None:
if self.active:
self.paused = True
self._audio.pause()
def resume(self) -> None:
if self.active:
self.paused = False
self._audio.unpause()
def get_pos(self) -> float:
return self._starting_time + max(0, self._chunks_played - 1) * self.chunk_size + self._audio.get_pos() * self.speed
def seek(self, time: float, relative=True) -> None:
# seeking accurate to 1 tenth of a second
self._starting_time = (self.get_pos() + time) if relative else time
self._starting_time = round(min(max(0, self._starting_time), self.duration), 1)
for t in self._threads:
t.join()
self._chunks = []
self._threads = []
self._chunks_claimed = 0
self._chunks_played = 0
self._audio.unload()
self._vid.set(cv2.CAP_PROP_POS_FRAMES, self._starting_time * self.frame_rate)
self.frame = int(self._vid.get(cv2.CAP_PROP_POS_FRAMES))
if self.subs is not None:
self.subs._seek(self._starting_time)
def draw(self, surf, pos: Tuple[int, int], force_draw=True) -> bool:
if (self._update() or force_draw) and self.frame_surf is not None:
self._render_frame(surf, pos)
return True
return False
def _create_frame(self):
pass
def _render_frame(self):
pass
def preview(self):
pass