Initial Commit
This commit is contained in:
205
v2a-player/v2a_player/player.py
Normal file
205
v2a-player/v2a_player/player.py
Normal 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()
|
||||
Reference in New Issue
Block a user