Files
v2a/v2a-player/v2a_player/player.py
Exil Productions 0cb370f8ec Initial Commit
2025-12-19 20:18:56 +01:00

206 lines
6.6 KiB
Python

import sys
import time
import threading
import select
import tty
import termios
from typing import Optional
from .reader import V2AReader
from .terminal import TerminalRenderer
from .audio_player import create_audio_player
class V2APlayer:
def __init__(self, filepath: str):
self.filepath = filepath
self.reader: Optional[V2AReader] = None
self.renderer: Optional[TerminalRenderer] = None
self.audio_player = None
self.playing = False
self.paused = False
self.current_frame = 0
self.frame_delay = 0.0
self.control_thread: Optional[threading.Thread] = None
self.stop_event = threading.Event()
self.paused_event = threading.Event()
self.paused_event.set()
self.original_termios = None
def load(self):
self.reader = V2AReader(self.filepath)
first_frame = self.reader.read_frame()
if first_frame is None:
raise ValueError("No frames in V2A file")
self.reader.reset()
self.renderer = TerminalRenderer()
self.renderer.update_layout(first_frame.width, first_frame.height)
if self.reader.audio:
self.audio_player = create_audio_player(self.reader.audio)
self.frame_delay = 1.0 / self.reader.frame_rate if self.reader.frame_rate > 0 else 0.1
print(f"Loaded: {self.reader.header.frame_count} frames, "
f"{self.reader.frame_rate:.1f} fps, "
f"{first_frame.width}x{first_frame.height} chars")
if self.audio_player and self.audio_player.is_valid():
print("Audio: Available")
else:
print("Audio: Not available (install pygame for audio)")
def _setup_terminal(self):
if not sys.stdin.isatty():
return
self.original_termios = termios.tcgetattr(sys.stdin)
tty.setraw(sys.stdin.fileno())
def _restore_terminal(self):
if self.original_termios:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.original_termios)
self.original_termios = None
def _handle_input(self):
while not self.stop_event.is_set():
ready, _, _ = select.select([sys.stdin], [], [], 0.1)
if ready:
ch = sys.stdin.read(1)
self._process_key(ch)
def _process_key(self, key: str):
if key == ' ':
self.toggle_pause()
elif key == 'q' or key == '\x03':
self.stop()
elif key == 'f':
if self.reader:
first_frame = None
pos = self.reader.file.tell()
self.reader.reset()
first_frame = self.reader.read_frame()
self.reader.reset()
if first_frame:
self.renderer.update_layout(first_frame.width, first_frame.height)
print(f"\rResized to: {self.renderer.scaled_width}x{self.renderer.scaled_height}")
def toggle_pause(self):
self.paused = not self.paused
if self.paused:
self.paused_event.clear()
if self.audio_player:
self.audio_player.pause()
print("\rPaused", end='')
else:
self.paused_event.set()
if self.audio_player:
self.audio_player.resume()
print("\rPlaying", end='')
sys.stdout.flush()
def _playback_loop(self):
frame_count = self.reader.header.frame_count
start_time = time.time()
expected_frame = 0
while (not self.stop_event.is_set() and
self.current_frame < frame_count):
self.paused_event.wait()
if self.stop_event.is_set():
break
elapsed = time.time() - start_time
expected_frame = int(elapsed / self.frame_delay)
if self.current_frame > expected_frame:
time.sleep(0.001)
continue
while self.current_frame < expected_frame and self.current_frame < frame_count - 1:
frame = self.reader.read_frame()
if frame is None:
break
self.current_frame += 1
if self.renderer.check_resize():
sys.stdout.write(self.renderer.prepare_display())
sys.stdout.flush()
frame = self.reader.read_frame()
if frame is None:
break
output = self.renderer.render_frame(frame.pixel_pairs, frame.width, frame.height)
sys.stdout.write(self.renderer.clear_video_area() + self.renderer.frame_prefix() + output)
sys.stdout.flush()
self.current_frame += 1
target_time = start_time + (self.current_frame * self.frame_delay)
sleep_time = target_time - time.time()
if sleep_time > 0:
time.sleep(sleep_time)
def play(self):
if self.playing:
return
self.playing = True
self.stop_event.clear()
self.paused_event.set()
try:
self._setup_terminal()
if self.audio_player and self.audio_player.is_valid():
self.audio_player.start()
sys.stdout.write(self.renderer.prepare_display())
sys.stdout.flush()
if sys.stdin.isatty():
self.control_thread = threading.Thread(target=self._handle_input)
self.control_thread.start()
else:
self.control_thread = None
self._playback_loop()
except KeyboardInterrupt:
pass
finally:
self.stop()
def stop(self):
if not self.playing:
return
self.stop_event.set()
self.paused_event.set()
if self.audio_player:
self.audio_player.stop()
if self.control_thread:
self.control_thread.join(timeout=0.5)
self._restore_terminal()
if self.renderer:
sys.stdout.write(self.renderer.restore_display())
sys.stdout.flush()
self.playing = False
print(f"\nPlayback stopped at frame {self.current_frame}/{self.reader.header.frame_count}")
def close(self):
self.stop()
if self.reader:
self.reader.close()