Files
video-sync/desktop-client/vlc_player.py
2026-03-02 21:54:22 +11:00

108 lines
3.8 KiB
Python

import vlc
import sys
from PyQt6.QtWidgets import QFrame
from PyQt6.QtCore import Qt, pyqtSignal, QObject
import threading
class VLCSignals(QObject):
state_changed = pyqtSignal(bool, int) # is_playing, time_ms
time_changed = pyqtSignal(int) # time_ms
class VLCSyncPlayer:
def __init__(self, frame: QFrame):
self.frame = frame
self.signals = VLCSignals()
# Initialize VLC instance
# --no-xlib prevents crashes on Linux
# --drop-late-frames improves sync by not delaying playback when CPU is slow
self.instance = vlc.Instance("--no-xlib", "--drop-late-frames")
self.media_player = self.instance.media_player_new()
# Embed the VLC player into the provided PyQt QFrame
# On Windows, PyQt6 widgets don't have a native handle by default
self.frame.setAttribute(Qt.WidgetAttribute.WA_NativeWindow)
if sys.platform.startswith('linux'):
self.media_player.set_xwindow(int(self.frame.winId()))
elif sys.platform == "win32":
# For Windows, we must explicitly cast winId() to int
self.media_player.set_hwnd(int(self.frame.winId()))
elif sys.platform == "darwin":
self.media_player.set_nsobject(int(self.frame.winId()))
# Register Event Callbacks
self.events = self.media_player.event_manager()
self.events.event_attach(vlc.EventType.MediaPlayerPlaying, self._on_playing)
self.events.event_attach(vlc.EventType.MediaPlayerPaused, self._on_paused)
self.events.event_attach(vlc.EventType.MediaPlayerTimeChanged, self._on_time_changed)
# Local State
self.is_playing = False
self.current_time_ms = 0
self.ignore_next_event = False
self.lock = threading.Lock()
def load_media(self, path: str):
media = self.instance.media_new(path)
self.media_player.set_media(media)
def play(self):
with self.lock:
self.ignore_next_event = True
self.media_player.play()
def pause(self):
with self.lock:
self.ignore_next_event = True
self.media_player.set_pause(1)
def seek(self, position_ms: int):
with self.lock:
self.ignore_next_event = True
self.media_player.set_time(position_ms)
def set_volume(self, volume: int):
self.media_player.audio_set_volume(volume)
def get_volume(self) -> int:
return self.media_player.audio_get_volume()
# --- Internal VLC Callbacks ---
@vlc.callbackmethod
def _on_playing(self, event):
self.is_playing = True
with self.lock:
if self.ignore_next_event:
self.ignore_next_event = False
return
time_ms = self.media_player.get_time()
# Fire signal to PyQt thread
self.signals.state_changed.emit(True, time_ms)
@vlc.callbackmethod
def _on_paused(self, event):
self.is_playing = False
with self.lock:
if self.ignore_next_event:
self.ignore_next_event = False
return
time_ms = self.media_player.get_time()
self.signals.state_changed.emit(False, time_ms)
@vlc.callbackmethod
def _on_time_changed(self, event):
# Emitted constantly during playback
self.current_time_ms = event.u.new_time
# We also want to fire this signal so the UI scrubber/time label can update
self.signals.time_changed.emit(self.current_time_ms)
def get_length(self):
return self.media_player.get_length()
def stop(self):
self.media_player.stop()