Initial Commit

This commit is contained in:
Exil Productions
2025-12-19 20:18:56 +01:00
commit 0cb370f8ec
22 changed files with 1804 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
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()