Use state machine for syncing playback
This commit is contained in:
@@ -105,8 +105,8 @@ class VlcSyncApp(QMainWindow):
|
|||||||
self.local_file_path = None
|
self.local_file_path = None
|
||||||
self.lobby_widget.clear_file()
|
self.lobby_widget.clear_file()
|
||||||
|
|
||||||
def _on_room_sync_action(self, action, position_s):
|
def _on_room_sync_action(self, action, position_s, req_id):
|
||||||
self.sync_client.send_message({"type": "sync", "action": action, "position": position_s})
|
self.sync_client.send_message({"type": "sync", "action": action, "position": position_s, "req_id": req_id})
|
||||||
|
|
||||||
def _on_room_chat(self, text):
|
def _on_room_chat(self, text):
|
||||||
self.sync_client.send_message({"type": "chat", "message": text})
|
self.sync_client.send_message({"type": "chat", "message": text})
|
||||||
|
|||||||
@@ -6,12 +6,20 @@ from PyQt6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
|
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
|
||||||
from PyQt6.QtGui import QIcon
|
from PyQt6.QtGui import QIcon
|
||||||
|
import uuid
|
||||||
|
|
||||||
from vlc_player import VLCSyncPlayer
|
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):
|
class RoomWidget(QWidget):
|
||||||
leave_requested = pyqtSignal()
|
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
|
chat_message_ready = pyqtSignal(str) # raw message text
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -19,7 +27,7 @@ class RoomWidget(QWidget):
|
|||||||
|
|
||||||
self.username = ""
|
self.username = ""
|
||||||
self.room_code = ""
|
self.room_code = ""
|
||||||
self.ignore_vlc_events = False
|
self.expected_vlc_events = []
|
||||||
self.last_reported_time_ms = 0
|
self.last_reported_time_ms = 0
|
||||||
|
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
@@ -210,16 +218,38 @@ class RoomWidget(QWidget):
|
|||||||
toast.deleteLater()
|
toast.deleteLater()
|
||||||
QTimer.singleShot(1500, reset)
|
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):
|
def toggle_playback(self):
|
||||||
position_s = self.vlc_player.current_time_ms / 1000.0
|
position_s = self.vlc_player.current_time_ms / 1000.0
|
||||||
if self.vlc_player.is_playing:
|
if self.vlc_player.is_playing:
|
||||||
self.vlc_player.pause()
|
req = self._tell_vlc_and_expect("pause", position_s)
|
||||||
self.play_btn.setText("▶")
|
self.sync_action_requested.emit("pause", position_s, req)
|
||||||
self.sync_action_requested.emit("pause", position_s)
|
|
||||||
else:
|
else:
|
||||||
self.vlc_player.play()
|
req = self._tell_vlc_and_expect("play", position_s)
|
||||||
self.play_btn.setText("⏸")
|
self.sync_action_requested.emit("play", position_s, req)
|
||||||
self.sync_action_requested.emit("play", position_s)
|
|
||||||
|
|
||||||
def on_vlc_time(self, time_ms: int):
|
def on_vlc_time(self, time_ms: int):
|
||||||
length_ms = self.vlc_player.get_length()
|
length_ms = self.vlc_player.get_length()
|
||||||
@@ -235,17 +265,39 @@ class RoomWidget(QWidget):
|
|||||||
self.seekbar.setValue(progress)
|
self.seekbar.setValue(progress)
|
||||||
self.seekbar.blockSignals(False)
|
self.seekbar.blockSignals(False)
|
||||||
|
|
||||||
if self.last_reported_time_ms is not None:
|
if self.last_reported_time_ms is not None:
|
||||||
diff = abs(time_ms - self.last_reported_time_ms)
|
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)
|
if diff > 1500:
|
||||||
self.last_reported_time_ms = time_ms
|
# 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):
|
def on_vlc_state(self, playing: bool, time_ms: int):
|
||||||
if self.ignore_vlc_events:
|
|
||||||
return
|
|
||||||
action = "play" if playing else "pause"
|
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):
|
def on_seekbar_dragged(self, value):
|
||||||
length_ms = self.vlc_player.get_length()
|
length_ms = self.vlc_player.get_length()
|
||||||
@@ -260,38 +312,23 @@ class RoomWidget(QWidget):
|
|||||||
length_ms = self.vlc_player.get_length()
|
length_ms = self.vlc_player.get_length()
|
||||||
if length_ms > 0:
|
if length_ms > 0:
|
||||||
target_ms = int((self.seekbar.value() / 1000.0) * length_ms)
|
target_ms = int((self.seekbar.value() / 1000.0) * length_ms)
|
||||||
self.vlc_player.seek(target_ms)
|
req = self._tell_vlc_and_expect("seek", target_ms / 1000.0)
|
||||||
self.sync_action_requested.emit("seek", target_ms / 1000.0)
|
self.sync_action_requested.emit("seek", target_ms / 1000.0, req)
|
||||||
|
|
||||||
def on_volume_changed(self, value):
|
def on_volume_changed(self, value):
|
||||||
self.vlc_player.set_volume(value)
|
self.vlc_player.set_volume(value)
|
||||||
|
|
||||||
# --- Incoming Sync Logic ---
|
# --- Incoming Sync Logic ---
|
||||||
def handle_sync_event(self, msg: dict):
|
def handle_sync_event(self, msg: dict):
|
||||||
self.ignore_vlc_events = True
|
|
||||||
|
|
||||||
action = msg.get("action")
|
action = msg.get("action")
|
||||||
if not action:
|
if not action:
|
||||||
if msg.get("playing", False): action = "play"
|
if msg.get("playing", False): action = "play"
|
||||||
elif msg.get("playing") is False: action = "pause"
|
elif msg.get("playing") is False: action = "pause"
|
||||||
|
|
||||||
position_s = msg.get("position", 0)
|
position_s = msg.get("position", 0)
|
||||||
position_ms = int(position_s * 1000)
|
|
||||||
|
|
||||||
if action == "play":
|
if action in ["play", "pause", "seek"]:
|
||||||
self.vlc_player.seek(position_ms)
|
self._tell_vlc_and_expect(action, position_s)
|
||||||
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)
|
|
||||||
|
|
||||||
username = msg.get("username")
|
username = msg.get("username")
|
||||||
if username and username != self.username:
|
if username and username != self.username:
|
||||||
@@ -385,8 +422,8 @@ class RoomWidget(QWidget):
|
|||||||
else: target_ms = float(arg) * 1000
|
else: target_ms = float(arg) * 1000
|
||||||
|
|
||||||
target_ms = max(0, min(target_ms, length_ms))
|
target_ms = max(0, min(target_ms, length_ms))
|
||||||
self.vlc_player.seek(int(target_ms))
|
req = self._tell_vlc_and_expect("seek", target_ms / 1000.0)
|
||||||
self.sync_action_requested.emit("seek", target_ms / 1000.0)
|
self.sync_action_requested.emit("seek", target_ms / 1000.0, req)
|
||||||
return True
|
return True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user