206 lines
6.6 KiB
Python
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()
|