Use state machine for syncing playback
This commit is contained in:
@@ -6,12 +6,20 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
|
||||
from PyQt6.QtGui import QIcon
|
||||
import uuid
|
||||
|
||||
from vlc_player import VLCSyncPlayer
|
||||
|
||||
class ExpectedVlcEvent:
|
||||
def __init__(self, action: str, req_id: str, target_val=None):
|
||||
self.action = action
|
||||
self.req_id = req_id
|
||||
self.target_val = target_val
|
||||
self.timestamp = datetime.datetime.now()
|
||||
|
||||
class RoomWidget(QWidget):
|
||||
leave_requested = pyqtSignal()
|
||||
sync_action_requested = pyqtSignal(str, float) # action, position_s
|
||||
sync_action_requested = pyqtSignal(str, float, str) # action, position_s, req_id
|
||||
chat_message_ready = pyqtSignal(str) # raw message text
|
||||
|
||||
def __init__(self):
|
||||
@@ -19,7 +27,7 @@ class RoomWidget(QWidget):
|
||||
|
||||
self.username = ""
|
||||
self.room_code = ""
|
||||
self.ignore_vlc_events = False
|
||||
self.expected_vlc_events = []
|
||||
self.last_reported_time_ms = 0
|
||||
|
||||
self._setup_ui()
|
||||
@@ -210,16 +218,38 @@ class RoomWidget(QWidget):
|
||||
toast.deleteLater()
|
||||
QTimer.singleShot(1500, reset)
|
||||
|
||||
def _tell_vlc_and_expect(self, action: str, position_s: float):
|
||||
req_id = str(uuid.uuid4())[:8]
|
||||
target_ms = int(position_s * 1000)
|
||||
|
||||
# Clean up old expectations (e.g. VLC dropped the event or we missed it)
|
||||
now = datetime.datetime.now()
|
||||
self.expected_vlc_events = [e for e in self.expected_vlc_events
|
||||
if (now - e.timestamp).total_seconds() < 3.0]
|
||||
|
||||
self.expected_vlc_events.append(ExpectedVlcEvent(action, req_id, target_ms))
|
||||
|
||||
if action == "play":
|
||||
self.vlc_player.seek(target_ms)
|
||||
self.vlc_player.play()
|
||||
self.play_btn.setText("⏸")
|
||||
elif action == "pause":
|
||||
self.vlc_player.seek(target_ms)
|
||||
self.vlc_player.pause()
|
||||
self.play_btn.setText("▶")
|
||||
elif action == "seek":
|
||||
self.vlc_player.seek(target_ms)
|
||||
|
||||
return req_id
|
||||
|
||||
def toggle_playback(self):
|
||||
position_s = self.vlc_player.current_time_ms / 1000.0
|
||||
if self.vlc_player.is_playing:
|
||||
self.vlc_player.pause()
|
||||
self.play_btn.setText("▶")
|
||||
self.sync_action_requested.emit("pause", position_s)
|
||||
req = self._tell_vlc_and_expect("pause", position_s)
|
||||
self.sync_action_requested.emit("pause", position_s, req)
|
||||
else:
|
||||
self.vlc_player.play()
|
||||
self.play_btn.setText("⏸")
|
||||
self.sync_action_requested.emit("play", position_s)
|
||||
req = self._tell_vlc_and_expect("play", position_s)
|
||||
self.sync_action_requested.emit("play", position_s, req)
|
||||
|
||||
def on_vlc_time(self, time_ms: int):
|
||||
length_ms = self.vlc_player.get_length()
|
||||
@@ -235,17 +265,39 @@ class RoomWidget(QWidget):
|
||||
self.seekbar.setValue(progress)
|
||||
self.seekbar.blockSignals(False)
|
||||
|
||||
if self.last_reported_time_ms is not None:
|
||||
diff = abs(time_ms - self.last_reported_time_ms)
|
||||
if diff > 2500 and not self.ignore_vlc_events:
|
||||
self.sync_action_requested.emit("seek", time_ms / 1000.0)
|
||||
self.last_reported_time_ms = time_ms
|
||||
if self.last_reported_time_ms is not None:
|
||||
diff = abs(time_ms - self.last_reported_time_ms)
|
||||
|
||||
if diff > 1500:
|
||||
# Look for a pending seek expectation
|
||||
matched = False
|
||||
for i, expected in enumerate(self.expected_vlc_events):
|
||||
if expected.action == "seek" and (expected.target_val is None or abs(expected.target_val - time_ms) < 2000):
|
||||
matched = True
|
||||
self.expected_vlc_events.pop(i)
|
||||
break
|
||||
|
||||
if not matched:
|
||||
# Genuine user scrub!
|
||||
req = str(uuid.uuid4())[:8]
|
||||
self.sync_action_requested.emit("seek", time_ms / 1000.0, req)
|
||||
|
||||
self.last_reported_time_ms = time_ms
|
||||
|
||||
def on_vlc_state(self, playing: bool, time_ms: int):
|
||||
if self.ignore_vlc_events:
|
||||
return
|
||||
action = "play" if playing else "pause"
|
||||
self.sync_action_requested.emit(action, time_ms / 1000.0)
|
||||
|
||||
# Look for a pending state change expectation
|
||||
matched = False
|
||||
for i, expected in enumerate(self.expected_vlc_events):
|
||||
if expected.action == action:
|
||||
matched = True
|
||||
self.expected_vlc_events.pop(i)
|
||||
break
|
||||
|
||||
if not matched:
|
||||
req = str(uuid.uuid4())[:8]
|
||||
self.sync_action_requested.emit(action, time_ms / 1000.0, req)
|
||||
|
||||
def on_seekbar_dragged(self, value):
|
||||
length_ms = self.vlc_player.get_length()
|
||||
@@ -260,38 +312,23 @@ class RoomWidget(QWidget):
|
||||
length_ms = self.vlc_player.get_length()
|
||||
if length_ms > 0:
|
||||
target_ms = int((self.seekbar.value() / 1000.0) * length_ms)
|
||||
self.vlc_player.seek(target_ms)
|
||||
self.sync_action_requested.emit("seek", target_ms / 1000.0)
|
||||
req = self._tell_vlc_and_expect("seek", target_ms / 1000.0)
|
||||
self.sync_action_requested.emit("seek", target_ms / 1000.0, req)
|
||||
|
||||
def on_volume_changed(self, value):
|
||||
self.vlc_player.set_volume(value)
|
||||
|
||||
# --- Incoming Sync Logic ---
|
||||
def handle_sync_event(self, msg: dict):
|
||||
self.ignore_vlc_events = True
|
||||
|
||||
action = msg.get("action")
|
||||
if not action:
|
||||
if msg.get("playing", False): action = "play"
|
||||
elif msg.get("playing") is False: action = "pause"
|
||||
|
||||
position_s = msg.get("position", 0)
|
||||
position_ms = int(position_s * 1000)
|
||||
|
||||
if action == "play":
|
||||
self.vlc_player.seek(position_ms)
|
||||
self.vlc_player.play()
|
||||
self.play_btn.setText("⏸")
|
||||
elif action == "pause":
|
||||
self.vlc_player.seek(position_ms)
|
||||
self.vlc_player.pause()
|
||||
self.play_btn.setText("▶")
|
||||
elif action == "seek":
|
||||
self.vlc_player.seek(position_ms)
|
||||
|
||||
def clear_ignore():
|
||||
self.ignore_vlc_events = False
|
||||
QTimer.singleShot(1500, clear_ignore)
|
||||
if action in ["play", "pause", "seek"]:
|
||||
self._tell_vlc_and_expect(action, position_s)
|
||||
|
||||
username = msg.get("username")
|
||||
if username and username != self.username:
|
||||
@@ -385,8 +422,8 @@ class RoomWidget(QWidget):
|
||||
else: target_ms = float(arg) * 1000
|
||||
|
||||
target_ms = max(0, min(target_ms, length_ms))
|
||||
self.vlc_player.seek(int(target_ms))
|
||||
self.sync_action_requested.emit("seek", target_ms / 1000.0)
|
||||
req = self._tell_vlc_and_expect("seek", target_ms / 1000.0)
|
||||
self.sync_action_requested.emit("seek", target_ms / 1000.0, req)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user