diff --git a/desktop-client/main.py b/desktop-client/main.py index cb9414a..fdf3509 100644 --- a/desktop-client/main.py +++ b/desktop-client/main.py @@ -105,8 +105,8 @@ class VlcSyncApp(QMainWindow): self.local_file_path = None self.lobby_widget.clear_file() - def _on_room_sync_action(self, action, position_s): - self.sync_client.send_message({"type": "sync", "action": action, "position": 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, "req_id": req_id}) def _on_room_chat(self, text): self.sync_client.send_message({"type": "chat", "message": text}) diff --git a/desktop-client/room_widget.py b/desktop-client/room_widget.py index 724a59e..c8fc389 100644 --- a/desktop-client/room_widget.py +++ b/desktop-client/room_widget.py @@ -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