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 # --no-keyboard stops VLC from capturing and swallowing keyboard events self.instance = vlc.Instance("--no-xlib", "--drop-late-frames", "--no-keyboard") 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, start_time_s: float = 0.0): media = self.instance.media_new(path) if start_time_s > 0: media.add_option(f"start-time={start_time_s}") 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()