From 887648bf85ec0d76b59e8f3a731a6c6dc3d7ac00 Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Thu, 5 Mar 2026 14:09:58 +1100 Subject: [PATCH] Add network status/latency info --- desktop-client/main.py | 3 +++ desktop-client/room_widget.py | 27 +++++++++++++++++++++++++++ desktop-client/sync_client.py | 19 +++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/desktop-client/main.py b/desktop-client/main.py index cbba507..66d091a 100644 --- a/desktop-client/main.py +++ b/desktop-client/main.py @@ -52,7 +52,9 @@ class VlcSyncApp(QMainWindow): # Network Service self.sync_client = SyncClientThread("wss://video-sync.peterstockings.com/ws") self.sync_client.connected.connect(self.on_ws_connected) + self.sync_client.connected.connect(lambda: self.room_widget.update_connection_status(True)) self.sync_client.disconnected.connect(self.on_ws_disconnected) + self.sync_client.disconnected.connect(lambda: self.room_widget.update_connection_status(False)) self.sync_client.room_joined.connect(self.on_room_joined) self.sync_client.room_rejoined.connect(self.on_room_rejoined) self.sync_client.room_error.connect(self.on_room_error) @@ -61,6 +63,7 @@ class VlcSyncApp(QMainWindow): self.sync_client.chat_message.connect(self.room_widget.add_chat_message) self.sync_client.system_message.connect(self.room_widget.add_system_message) self.sync_client.sync_event.connect(self.room_widget.handle_sync_event) + self.sync_client.latency_updated.connect(self.room_widget.update_latency) self.apply_stylesheet() diff --git a/desktop-client/room_widget.py b/desktop-client/room_widget.py index 1a313fb..02b0328 100644 --- a/desktop-client/room_widget.py +++ b/desktop-client/room_widget.py @@ -120,6 +120,12 @@ class RoomWidget(QWidget): self.copy_code_btn.setToolTip("Copy Room Code") self.copy_code_btn.clicked.connect(self.copy_room_code) + self.status_dot = QLabel("●") + self.status_dot.setFixedWidth(20) + self.status_dot.setStyleSheet("color: #888; font-size: 14px; background: transparent;") + self.status_dot.setToolTip("Connecting...") + self._latency_ms = None + self.room_file_badge = QLabel("📄 No file") self.room_file_badge.setObjectName("fileBadge") @@ -129,6 +135,7 @@ class RoomWidget(QWidget): topbar_layout.addWidget(self.room_code_display) topbar_layout.addWidget(self.copy_code_btn) + topbar_layout.addWidget(self.status_dot) topbar_layout.addStretch() topbar_layout.addWidget(self.room_file_badge) topbar_layout.addWidget(self.leave_btn) @@ -345,6 +352,26 @@ class RoomWidget(QWidget): def set_room_code_display(self, text: str): self.room_code_display.setText(f"Room: {text}") + def update_connection_status(self, connected: bool): + if connected: + self.status_dot.setStyleSheet("color: #4BB543; font-size: 14px; background: transparent;") + self.status_dot.setToolTip("Connected") + else: + self.status_dot.setStyleSheet("color: #ff4e45; font-size: 14px; background: transparent;") + self.status_dot.setToolTip("Disconnected") + self._latency_ms = None + + def update_latency(self, latency_ms: int): + self._latency_ms = latency_ms + if latency_ms < 100: + color = "#4BB543" # green + elif latency_ms < 250: + color = "#f0ad4e" # yellow + else: + color = "#ff4e45" # red + self.status_dot.setStyleSheet(f"color: {color}; font-size: 14px; background: transparent;") + self.status_dot.setToolTip(f"Latency: {latency_ms}ms") + def toggle_tags_panel(self): if self.tags_list.isHidden(): self.tags_list.show() diff --git a/desktop-client/sync_client.py b/desktop-client/sync_client.py index eee4014..095ff6b 100644 --- a/desktop-client/sync_client.py +++ b/desktop-client/sync_client.py @@ -1,5 +1,6 @@ import asyncio import json +import time import websockets from PyQt6.QtCore import QThread, pyqtSignal @@ -15,6 +16,7 @@ class SyncClientThread(QThread): chat_message = pyqtSignal(str, str, int) # author, text, timestamp system_message = pyqtSignal(str) sync_event = pyqtSignal(dict) + latency_updated = pyqtSignal(int) # latency in ms def __init__(self, url="ws://localhost:3000/ws"): super().__init__() @@ -22,6 +24,7 @@ class SyncClientThread(QThread): self.ws = None self.loop = None self.running = False + self._ping_sent_at = 0 def run(self): """Runs strictly within the newly created QThread""" @@ -44,6 +47,19 @@ class SyncClientThread(QThread): json_str = json.dumps(message) asyncio.run_coroutine_threadsafe(self.ws.send(json_str), self.loop) + async def _ping_loop(self, ws): + """Sends WebSocket protocol-level pings every 5s to measure latency.""" + while self.running: + try: + pong = await ws.ping() + sent_at = time.time() + await asyncio.wait_for(pong, timeout=5) + latency_ms = int((time.time() - sent_at) * 1000) + self.latency_updated.emit(latency_ms) + except Exception: + break + await asyncio.sleep(5) + async def _connect_and_listen(self): while self.running: try: @@ -51,6 +67,7 @@ class SyncClientThread(QThread): self.ws = ws self.connected.emit() + ping_task = asyncio.create_task(self._ping_loop(ws)) try: async for message in ws: if not self.running: @@ -58,6 +75,8 @@ class SyncClientThread(QThread): self._handle_message(json.loads(message)) except websockets.ConnectionClosed: pass + finally: + ping_task.cancel() except Exception as e: print(f"WebSocket Error: {e}")