108 lines
3.8 KiB
Python
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()
|