feat: fix looping
This commit is contained in:
315
vlchan/player.py
315
vlchan/player.py
@@ -10,64 +10,140 @@ from .synchan import SynchanState, SynchanController, create_synchan
|
|||||||
|
|
||||||
class VLChanPlayer:
|
class VLChanPlayer:
|
||||||
"""VLC video player synchronized with synchan server."""
|
"""VLC video player synchronized with synchan server."""
|
||||||
|
|
||||||
def __init__(self, synchan_url: str = "http://localhost:3000",
|
def __init__(
|
||||||
video_path: Optional[str] = None):
|
self,
|
||||||
|
synchan_url: str = "http://localhost:3000",
|
||||||
|
video_path: Optional[str] = None,
|
||||||
|
):
|
||||||
"""Initialize the VLC player with synchan synchronization.
|
"""Initialize the VLC player with synchan synchronization.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
synchan_url: URL of the synchan server
|
synchan_url: URL of the synchan server
|
||||||
video_path: Path to video file to play
|
video_path: Path to video file to play
|
||||||
"""
|
"""
|
||||||
self.synchan_url = synchan_url
|
self.synchan_url = synchan_url
|
||||||
self.video_path = video_path
|
self.video_path = video_path
|
||||||
|
self.loop = True
|
||||||
|
|
||||||
# Initialize VLC
|
# Initialize VLC
|
||||||
self.instance = vlc.Instance()
|
self.instance = vlc.Instance()
|
||||||
self.player = self.instance.media_player_new()
|
self.player = self.instance.media_player_new()
|
||||||
|
|
||||||
|
# Set up VLC event manager for looping and debugging
|
||||||
|
self.event_manager = self.player.event_manager()
|
||||||
|
self.event_manager.event_attach(
|
||||||
|
vlc.EventType.MediaPlayerEndReached, self._on_end_reached
|
||||||
|
)
|
||||||
|
# Add more events for debugging
|
||||||
|
self.event_manager.event_attach(
|
||||||
|
vlc.EventType.MediaPlayerStopped, self._on_stopped
|
||||||
|
)
|
||||||
|
self.event_manager.event_attach(
|
||||||
|
vlc.EventType.MediaPlayerPlaying, self._on_playing
|
||||||
|
)
|
||||||
|
self.event_manager.event_attach(
|
||||||
|
vlc.EventType.MediaPlayerPaused, self._on_paused
|
||||||
|
)
|
||||||
|
|
||||||
# Initialize synchan
|
# Initialize synchan
|
||||||
self.synchan_controller = SynchanController(synchan_url)
|
self.synchan_controller = SynchanController(synchan_url)
|
||||||
self.synchan_observable: Optional[Observable[SynchanState]] = None
|
self.synchan_observable: Optional[Observable[SynchanState]] = None
|
||||||
self.synchan_subscription = None
|
self.synchan_subscription = None
|
||||||
|
|
||||||
# State tracking
|
# State tracking
|
||||||
self._is_playing = False
|
self._is_playing = False
|
||||||
self.last_sync_time = 0
|
self.last_sync_time = 0
|
||||||
self.sync_threshold = 0.1 # 100ms threshold for sync
|
self.sync_threshold = 0.1 # 100ms threshold for sync
|
||||||
|
|
||||||
# Playback rate adjustment constants (from VideoPlayer.tsx)
|
# Playback rate adjustment constants (from VideoPlayer.tsx)
|
||||||
self.rate_k = 0.5 # K constant
|
self.rate_k = 0.5 # K constant
|
||||||
self.rate_range = 0.05 # Range limit
|
self.rate_range = 0.05 # Range limit
|
||||||
|
|
||||||
# Callbacks
|
# Callbacks
|
||||||
self.on_state_change: Optional[Callable[[SynchanState], None]] = None
|
self.on_state_change: Optional[Callable[[SynchanState], None]] = None
|
||||||
|
|
||||||
|
# Position-based loop detection as fallback
|
||||||
|
self._last_position = 0.0
|
||||||
|
self._position_check_thread = None
|
||||||
|
self._position_monitor_running = False
|
||||||
|
|
||||||
# Load video if provided
|
# Load video if provided
|
||||||
if video_path:
|
if video_path:
|
||||||
self.load_video(video_path)
|
self.load_video(video_path)
|
||||||
|
|
||||||
|
def _on_end_reached(self, event):
|
||||||
|
"""Handle video end reached event for looping."""
|
||||||
|
print(
|
||||||
|
f"[EVENT] MediaPlayerEndReached fired - loop={self.loop}, was_playing={self._is_playing}"
|
||||||
|
)
|
||||||
|
if self.loop and self.video_path:
|
||||||
|
print("[LOOP] Attempting to loop back to beginning")
|
||||||
|
|
||||||
|
def restart_video():
|
||||||
|
try:
|
||||||
|
print("[LOOP] Stopping player...")
|
||||||
|
self.player.stop()
|
||||||
|
time.sleep(0.05) # Shorter delay
|
||||||
|
|
||||||
|
print("[LOOP] Reloading media...")
|
||||||
|
media = self.instance.media_new(self.video_path)
|
||||||
|
self.player.set_media(media)
|
||||||
|
|
||||||
|
print("[LOOP] Setting position to 0...")
|
||||||
|
self.player.set_position(0.0)
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
if self._is_playing:
|
||||||
|
print("[LOOP] Restarting playback...")
|
||||||
|
self.player.play()
|
||||||
|
else:
|
||||||
|
print("[LOOP] Not restarting - was not playing")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[LOOP ERROR] Failed to restart: {e}")
|
||||||
|
|
||||||
|
# Use a timer to restart after a short delay
|
||||||
|
threading.Timer(0.05, restart_video).start()
|
||||||
|
else:
|
||||||
|
print("[EVENT] Video ended - no looping enabled")
|
||||||
|
self._is_playing = False
|
||||||
|
|
||||||
|
def _on_stopped(self, event):
|
||||||
|
"""Handle player stopped event."""
|
||||||
|
print("[EVENT] MediaPlayerStopped")
|
||||||
|
|
||||||
|
def _on_playing(self, event):
|
||||||
|
"""Handle player playing event."""
|
||||||
|
print("[EVENT] MediaPlayerPlaying")
|
||||||
|
self._is_playing = True
|
||||||
|
|
||||||
|
def _on_paused(self, event):
|
||||||
|
"""Handle player paused event."""
|
||||||
|
print("[EVENT] MediaPlayerPaused")
|
||||||
|
self._is_playing = False
|
||||||
|
|
||||||
def load_video(self, video_path: str):
|
def load_video(self, video_path: str):
|
||||||
"""Load a video file."""
|
"""Load a video file."""
|
||||||
self.video_path = video_path
|
self.video_path = video_path
|
||||||
media = self.instance.media_new(video_path)
|
media = self.instance.media_new(video_path)
|
||||||
self.player.set_media(media)
|
self.player.set_media(media)
|
||||||
print(f"Loaded video: {video_path}")
|
print(f"Loaded video: {video_path}")
|
||||||
|
|
||||||
def start_sync(self):
|
def start_sync(self):
|
||||||
"""Start synchronization with synchan server."""
|
"""Start synchronization with synchan server."""
|
||||||
if self.synchan_observable is not None:
|
if self.synchan_observable is not None:
|
||||||
print("Sync already started")
|
print("Sync already started")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.synchan_observable = create_synchan(self.synchan_url)
|
self.synchan_observable = create_synchan(self.synchan_url)
|
||||||
self.synchan_subscription = self.synchan_observable.subscribe(
|
self.synchan_subscription = self.synchan_observable.subscribe(
|
||||||
on_next=self._handle_synchan_state,
|
on_next=self._handle_synchan_state,
|
||||||
on_error=self._handle_synchan_error,
|
on_error=self._handle_synchan_error,
|
||||||
on_completed=self._handle_synchan_completed
|
on_completed=self._handle_synchan_completed,
|
||||||
)
|
)
|
||||||
print("Started synchan synchronization")
|
print("Started synchan synchronization")
|
||||||
|
|
||||||
def stop_sync(self):
|
def stop_sync(self):
|
||||||
"""Stop synchronization with synchan server."""
|
"""Stop synchronization with synchan server."""
|
||||||
if self.synchan_subscription:
|
if self.synchan_subscription:
|
||||||
@@ -75,38 +151,42 @@ class VLChanPlayer:
|
|||||||
self.synchan_subscription = None
|
self.synchan_subscription = None
|
||||||
self.synchan_observable = None
|
self.synchan_observable = None
|
||||||
print("Stopped synchan synchronization")
|
print("Stopped synchan synchronization")
|
||||||
|
|
||||||
def _handle_synchan_state(self, state: SynchanState):
|
def _handle_synchan_state(self, state: SynchanState):
|
||||||
"""Handle state updates from synchan server."""
|
"""Handle state updates from synchan server."""
|
||||||
if self.on_state_change:
|
if self.on_state_change:
|
||||||
self.on_state_change(state)
|
self.on_state_change(state)
|
||||||
|
|
||||||
# Get current local time
|
# Get current local time
|
||||||
local_time = self.player.get_time() / 1000.0 # Convert to seconds
|
local_time = self.player.get_time() / 1000.0 # Convert to seconds
|
||||||
|
|
||||||
# Apply latency compensation (similar to VideoPlayer.tsx)
|
# Apply latency compensation (similar to VideoPlayer.tsx)
|
||||||
# currentTime = newState.currentTime + newState.latency / 1000
|
# currentTime = newState.currentTime + newState.latency / 1000
|
||||||
compensated_time = state.currentTime + state.latency / 1000
|
compensated_time = state.currentTime + state.latency / 1000
|
||||||
|
|
||||||
print(f"Control: playing={state.playing}, server_time={state.currentTime:.2f}s, "
|
print(
|
||||||
f"latency={state.latency:.0f}ms, compensated={compensated_time:.2f}s, "
|
f"Control: playing={state.playing}, server_time={state.currentTime:.2f}s, "
|
||||||
f"local={local_time:.2f}s")
|
f"latency={state.latency:.0f}ms, compensated={compensated_time:.2f}s, "
|
||||||
|
f"local={local_time:.2f}s"
|
||||||
|
)
|
||||||
|
|
||||||
if state.playing:
|
if state.playing:
|
||||||
# Calculate time difference
|
# Calculate time difference
|
||||||
diff = compensated_time - local_time
|
diff = compensated_time - local_time
|
||||||
|
|
||||||
# If difference is large (>1s), seek to correct position
|
# If difference is large (>1s), seek to correct position
|
||||||
if diff > 1.0:
|
if diff > 1.0:
|
||||||
print(f"Large diff detected ({diff:.2f}s), seeking to {compensated_time:.2f}s")
|
print(
|
||||||
|
f"Large diff detected ({diff:.2f}s), seeking to {compensated_time:.2f}s"
|
||||||
|
)
|
||||||
seek_time = int(compensated_time * 1000) # Convert to milliseconds
|
seek_time = int(compensated_time * 1000) # Convert to milliseconds
|
||||||
self.player.set_time(seek_time)
|
self.player.set_time(seek_time)
|
||||||
local_time = compensated_time # Update local time after seek
|
local_time = compensated_time # Update local time after seek
|
||||||
diff = 0 # Reset diff after seek
|
diff = 0 # Reset diff after seek
|
||||||
|
|
||||||
# Calculate playback rate adjustment (similar to getPlaybackRate)
|
# Calculate playback rate adjustment (similar to getPlaybackRate)
|
||||||
new_rate = self._get_playback_rate(diff)
|
new_rate = self._get_playback_rate(diff)
|
||||||
|
|
||||||
# Apply playback rate and play
|
# Apply playback rate and play
|
||||||
self.player.set_rate(new_rate)
|
self.player.set_rate(new_rate)
|
||||||
if not self._is_playing:
|
if not self._is_playing:
|
||||||
@@ -121,31 +201,31 @@ class VLChanPlayer:
|
|||||||
self.player.pause()
|
self.player.pause()
|
||||||
self._is_playing = False
|
self._is_playing = False
|
||||||
print("Paused playback")
|
print("Paused playback")
|
||||||
|
|
||||||
# Set exact time when paused
|
# Set exact time when paused
|
||||||
seek_time = int(state.currentTime * 1000)
|
seek_time = int(state.currentTime * 1000)
|
||||||
self.player.set_time(seek_time)
|
self.player.set_time(seek_time)
|
||||||
print(f"Set paused time to {state.currentTime:.2f}s")
|
print(f"Set paused time to {state.currentTime:.2f}s")
|
||||||
|
|
||||||
def _handle_synchan_error(self, error):
|
def _handle_synchan_error(self, error):
|
||||||
"""Handle synchan connection errors."""
|
"""Handle synchan connection errors."""
|
||||||
print(f"Synchan error: {error}")
|
print(f"Synchan error: {error}")
|
||||||
# Optionally attempt to reconnect
|
# Optionally attempt to reconnect
|
||||||
threading.Timer(5.0, self.start_sync).start()
|
threading.Timer(5.0, self.start_sync).start()
|
||||||
|
|
||||||
def _handle_synchan_completed(self):
|
def _handle_synchan_completed(self):
|
||||||
"""Handle synchan connection completion."""
|
"""Handle synchan connection completion."""
|
||||||
print("Synchan connection completed")
|
print("Synchan connection completed")
|
||||||
|
|
||||||
def _get_playback_rate(self, diff: float) -> float:
|
def _get_playback_rate(self, diff: float) -> float:
|
||||||
"""Calculate playback rate adjustment based on time difference.
|
"""Calculate playback rate adjustment based on time difference.
|
||||||
|
|
||||||
This implements the same logic as getPlaybackRate in VideoPlayer.tsx:
|
This implements the same logic as getPlaybackRate in VideoPlayer.tsx:
|
||||||
return 1 + Math.min(Math.max(diff * 0.5, -range), range);
|
return 1 + Math.min(Math.max(diff * 0.5, -range), range);
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
diff: Time difference in seconds (positive = ahead, negative = behind)
|
diff: Time difference in seconds (positive = ahead, negative = behind)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Adjusted playback rate (1.0 = normal speed)
|
Adjusted playback rate (1.0 = normal speed)
|
||||||
"""
|
"""
|
||||||
@@ -154,81 +234,186 @@ class VLChanPlayer:
|
|||||||
rate_adjustment = max(rate_adjustment, -self.rate_range)
|
rate_adjustment = max(rate_adjustment, -self.rate_range)
|
||||||
rate_adjustment = min(rate_adjustment, self.rate_range)
|
rate_adjustment = min(rate_adjustment, self.rate_range)
|
||||||
return 1.0 + rate_adjustment
|
return 1.0 + rate_adjustment
|
||||||
|
|
||||||
def play(self):
|
def play(self):
|
||||||
"""Start video playback."""
|
"""Start video playback."""
|
||||||
if self.player.get_media():
|
if self.player.get_media():
|
||||||
|
print("Starting playback...")
|
||||||
self.player.play()
|
self.player.play()
|
||||||
self._is_playing = True
|
self._is_playing = True
|
||||||
|
# Start position monitoring for loop detection
|
||||||
|
if self.loop:
|
||||||
|
self._start_position_monitoring()
|
||||||
print("Started playback")
|
print("Started playback")
|
||||||
else:
|
else:
|
||||||
print("No media loaded")
|
print("No media loaded")
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
"""Pause video playback."""
|
"""Pause video playback."""
|
||||||
self.player.pause()
|
self.player.pause()
|
||||||
self._is_playing = False
|
self._is_playing = False
|
||||||
print("Paused playback")
|
print("Paused playback")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop video playback."""
|
"""Stop video playback."""
|
||||||
self.player.stop()
|
self.player.stop()
|
||||||
self._is_playing = False
|
self._is_playing = False
|
||||||
|
self._stop_position_monitoring()
|
||||||
print("Stopped playback")
|
print("Stopped playback")
|
||||||
|
|
||||||
def seek(self, time_seconds: float):
|
def seek(self, time_seconds: float):
|
||||||
"""Seek to a specific time position."""
|
"""Seek to a specific time position."""
|
||||||
time_ms = int(time_seconds * 1000)
|
time_ms = int(time_seconds * 1000)
|
||||||
self.player.set_time(time_ms)
|
self.player.set_time(time_ms)
|
||||||
print(f"Seeked to {time_seconds:.2f}s")
|
print(f"Seeked to {time_seconds:.2f}s")
|
||||||
|
|
||||||
def get_position(self) -> float:
|
def get_position(self) -> float:
|
||||||
"""Get current playback position in seconds."""
|
"""Get current playback position in seconds."""
|
||||||
return self.player.get_time() / 1000.0
|
return self.player.get_time() / 1000.0
|
||||||
|
|
||||||
def get_compensated_position(self, latency: float) -> float:
|
def get_compensated_position(self, latency: float) -> float:
|
||||||
"""Get latency-compensated position for synchronization.
|
"""Get latency-compensated position for synchronization.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
latency: Network latency in milliseconds
|
latency: Network latency in milliseconds
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Position adjusted for latency
|
Position adjusted for latency
|
||||||
"""
|
"""
|
||||||
return self.get_position() + latency / 1000.0
|
return self.get_position() + latency / 1000.0
|
||||||
|
|
||||||
def get_duration(self) -> float:
|
def get_duration(self) -> float:
|
||||||
"""Get video duration in seconds."""
|
"""Get video duration in seconds."""
|
||||||
return self.player.get_length() / 1000.0
|
return self.player.get_length() / 1000.0
|
||||||
|
|
||||||
def is_playing(self) -> bool:
|
def is_playing(self) -> bool:
|
||||||
"""Check if video is currently playing."""
|
"""Check if video is currently playing."""
|
||||||
return self.player.is_playing()
|
return self.player.is_playing()
|
||||||
|
|
||||||
def set_volume(self, volume: int):
|
def set_volume(self, volume: int):
|
||||||
"""Set volume (0-100)."""
|
"""Set volume (0-100)."""
|
||||||
self.player.audio_set_volume(int(volume))
|
self.player.audio_set_volume(int(volume))
|
||||||
|
|
||||||
def get_volume(self) -> int:
|
def get_volume(self) -> int:
|
||||||
"""Get current volume (0-100)."""
|
"""Get current volume (0-100)."""
|
||||||
return self.player.audio_get_volume()
|
return self.player.audio_get_volume()
|
||||||
|
|
||||||
def get_rate(self) -> float:
|
def get_rate(self) -> float:
|
||||||
"""Get current playback rate."""
|
"""Get current playback rate."""
|
||||||
return self.player.get_rate()
|
return self.player.get_rate()
|
||||||
|
|
||||||
def set_rate(self, rate: float):
|
def set_rate(self, rate: float):
|
||||||
"""Set playback rate (1.0 = normal speed)."""
|
"""Set playback rate (1.0 = normal speed)."""
|
||||||
self.player.set_rate(rate)
|
self.player.set_rate(rate)
|
||||||
|
|
||||||
def set_fullscreen(self, fullscreen: bool):
|
def set_fullscreen(self, fullscreen: bool):
|
||||||
"""Set fullscreen mode."""
|
"""Set fullscreen mode."""
|
||||||
self.player.set_fullscreen(fullscreen)
|
self.player.set_fullscreen(fullscreen)
|
||||||
|
|
||||||
|
def set_loop(self, loop: bool):
|
||||||
|
"""Enable or disable video looping."""
|
||||||
|
self.loop = loop
|
||||||
|
print(f"Looping {'enabled' if loop else 'disabled'}")
|
||||||
|
|
||||||
|
def is_looping(self) -> bool:
|
||||||
|
"""Check if looping is enabled."""
|
||||||
|
return self.loop
|
||||||
|
|
||||||
|
def trigger_loop(self):
|
||||||
|
"""Manually trigger a loop back to the beginning (for testing)."""
|
||||||
|
if self.video_path:
|
||||||
|
print("Manually triggering loop")
|
||||||
|
self._on_end_reached(None)
|
||||||
|
|
||||||
|
def _start_position_monitoring(self):
|
||||||
|
"""Start monitoring position for loop detection."""
|
||||||
|
if not self._position_monitor_running:
|
||||||
|
self._position_monitor_running = True
|
||||||
|
self._position_check_thread = threading.Thread(
|
||||||
|
target=self._position_monitor_loop, daemon=True
|
||||||
|
)
|
||||||
|
self._position_check_thread.start()
|
||||||
|
print("[MONITOR] Started position monitoring")
|
||||||
|
|
||||||
|
def _stop_position_monitoring(self):
|
||||||
|
"""Stop monitoring position."""
|
||||||
|
self._position_monitor_running = False
|
||||||
|
if self._position_check_thread:
|
||||||
|
self._position_check_thread.join(timeout=1.0)
|
||||||
|
print("[MONITOR] Stopped position monitoring")
|
||||||
|
|
||||||
|
def _position_monitor_loop(self):
|
||||||
|
"""Monitor position for loop detection as fallback."""
|
||||||
|
consecutive_same_position = 0
|
||||||
|
while self._position_monitor_running:
|
||||||
|
try:
|
||||||
|
if self._is_playing and self.loop:
|
||||||
|
current_pos = self.get_position()
|
||||||
|
duration = self.get_duration()
|
||||||
|
|
||||||
|
# Check if we're stuck at the end
|
||||||
|
if duration > 0 and current_pos >= duration - 0.5:
|
||||||
|
consecutive_same_position += 1
|
||||||
|
if consecutive_same_position >= 3: # 3 consecutive checks
|
||||||
|
print(
|
||||||
|
f"[MONITOR] Detected stuck at end: pos={current_pos:.2f}, dur={duration:.2f}"
|
||||||
|
)
|
||||||
|
self._force_loop()
|
||||||
|
consecutive_same_position = 0
|
||||||
|
else:
|
||||||
|
consecutive_same_position = 0
|
||||||
|
|
||||||
|
# Also check if position hasn't changed for too long near the end
|
||||||
|
if (
|
||||||
|
duration > 0
|
||||||
|
and current_pos > duration * 0.95
|
||||||
|
and abs(current_pos - self._last_position) < 0.1
|
||||||
|
):
|
||||||
|
consecutive_same_position += 1
|
||||||
|
else:
|
||||||
|
consecutive_same_position = 0
|
||||||
|
|
||||||
|
self._last_position = current_pos
|
||||||
|
|
||||||
|
time.sleep(1) # Check every second
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[MONITOR ERROR] {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
def _force_loop(self):
|
||||||
|
"""Force a loop when position monitoring detects we're stuck."""
|
||||||
|
print("[FORCE LOOP] Forcing loop due to position monitoring")
|
||||||
|
try:
|
||||||
|
# Stop and restart completely
|
||||||
|
self.player.stop()
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Reload media
|
||||||
|
media = self.instance.media_new(self.video_path)
|
||||||
|
self.player.set_media(media)
|
||||||
|
|
||||||
|
# Start from beginning
|
||||||
|
self.player.set_time(0)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Resume if we were playing
|
||||||
|
if self._is_playing:
|
||||||
|
self.player.play()
|
||||||
|
print("[FORCE LOOP] Resumed playback from beginning")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[FORCE LOOP ERROR] {e}")
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""Clean up resources."""
|
"""Clean up resources."""
|
||||||
self.stop_sync()
|
self.stop_sync()
|
||||||
self.stop()
|
self.stop()
|
||||||
|
self._stop_position_monitoring()
|
||||||
|
# Detach events before cleanup
|
||||||
|
if hasattr(self, "event_manager"):
|
||||||
|
self.event_manager.event_detach(vlc.EventType.MediaPlayerEndReached)
|
||||||
|
self.event_manager.event_detach(vlc.EventType.MediaPlayerStopped)
|
||||||
|
self.event_manager.event_detach(vlc.EventType.MediaPlayerPlaying)
|
||||||
|
self.event_manager.event_detach(vlc.EventType.MediaPlayerPaused)
|
||||||
self.player.release()
|
self.player.release()
|
||||||
self.instance.release()
|
self.instance.release()
|
||||||
print("Cleaned up VLC player")
|
print("Cleaned up VLC player")
|
||||||
@@ -237,34 +422,40 @@ class VLChanPlayer:
|
|||||||
def main():
|
def main():
|
||||||
"""Example usage of VLChanPlayer."""
|
"""Example usage of VLChanPlayer."""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print("Usage: python -m vlchan.player <video_path> [synchan_url]")
|
print("Usage: python -m vlchan.player <video_path> [synchan_url] [--loop]")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
video_path = sys.argv[1]
|
video_path = sys.argv[1]
|
||||||
synchan_url = sys.argv[2] if len(sys.argv) > 2 else "http://localhost:3000"
|
synchan_url = (
|
||||||
|
sys.argv[2]
|
||||||
# Create player
|
if len(sys.argv) > 2 and not sys.argv[2].startswith("--")
|
||||||
|
else "http://localhost:3000"
|
||||||
|
)
|
||||||
|
loop = "--loop" in sys.argv
|
||||||
|
|
||||||
|
# Create player with looping if specified
|
||||||
player = VLChanPlayer(synchan_url, video_path)
|
player = VLChanPlayer(synchan_url, video_path)
|
||||||
|
print(f"Created player with looping {'enabled' if loop else 'disabled'}")
|
||||||
|
|
||||||
# Set up state change callback
|
# Set up state change callback
|
||||||
def on_state_change(state: SynchanState):
|
def on_state_change(state: SynchanState):
|
||||||
print(f"Synchan state: playing={state.playing}, time={state.currentTime:.2f}s")
|
print(f"Synchan state: playing={state.playing}, time={state.currentTime:.2f}s")
|
||||||
|
|
||||||
player.on_state_change = on_state_change
|
player.on_state_change = on_state_change
|
||||||
|
|
||||||
# Start synchronization
|
# Start synchronization
|
||||||
player.start_sync()
|
player.start_sync()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Start playback
|
# Start playback
|
||||||
player.play()
|
player.play()
|
||||||
|
|
||||||
# Keep running until interrupted
|
# Keep running until interrupted
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nShutting down...")
|
print("\nShutting down...")
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
Reference in New Issue
Block a user