diff --git a/README.md b/README.md index 6f7154d..14a171d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@
KoalaSync is a lightweight Browser Extension and Relay Server for synchronized video playback on almost any website with a video element—YouTube, Twitch, Netflix, Emby, Jellyfin, and beyond. Built with a focus on Data Sovereignty and Performance.
-New v2.2.0 Release! — See what's changed
+New v2.2.1 Release! — See what's changed
### 🌟 Why KoalaSync? diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5be03da..24a7d77 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to the KoalaSync browser extension and relay server. --- +## [v2.2.1] — 2026-06-09 + +### Added +- **Server Ping Display**: Measures round-trip latency to the relay server via application-level ping/pong events. The extension sends `PING { t }` every 15 seconds; the server responds with `PONG { t }`. Round-trip time is calculated client-side and displayed in the Status tab, color-coded (<50ms green, 50–150ms yellow, >150ms red). No ping value is shown when disconnected or if the server does not respond within 5 seconds. +- **Peer Ping Response (Future-Proof)**: The extension can now respond to incoming `PING { t, sender }` events from other peers by sending back `PONG { t, target: sender }`. The relay server forwards `PING` to the target peer and routes `PONG` back to the original sender. Both client and server validate that peers are in the same room before forwarding/routing. Peer-to-peer ping initiation will be activated in a future extension update without requiring a server restart. + +--- + ## [v2.2.0] — 2026-06-08 ### Added diff --git a/extension/background.js b/extension/background.js index eff941f..e03b585 100644 --- a/extension/background.js +++ b/extension/background.js @@ -23,6 +23,12 @@ const lastSeqBySender = {}; // senderId → last received seq (sta const activePorts = new Set(); // New: track active content ports for keep-alive let expectedAcksCount = 0; // Snapshot of peerCount when initiating Force Sync +// --- Ping / Latency --- +let pingInterval = null; +let pingTimeout = null; +let pendingPingT = null; +let currentPingMs = null; + // --- Keep-Alive Port Listener --- chrome.runtime.onConnect.addListener((port) => { if (port.name === 'keepAlive') { @@ -299,6 +305,7 @@ function forceDisconnect() { clearTimeout(forceSyncTimeout); forceSyncTimeout = null; } + stopPing(); if (socket) { socket.onopen = null; socket.onmessage = null; @@ -492,6 +499,7 @@ async function connect() { isConnecting = false; isNamespaceJoined = true; broadcastConnectionStatus('connected'); + startPing(); addLog('Joined Namespace /', 'success'); const settings = await getSettings(); if (settings.roomId) { @@ -527,6 +535,7 @@ async function connect() { socket.onclose = () => { isConnecting = false; isNamespaceJoined = false; + stopPing(); isForceSyncInitiator = false; forceSyncAcks.clear(); @@ -691,6 +700,42 @@ function addToHistory(action, senderId) { chrome.runtime.sendMessage({ type: 'HISTORY_UPDATE', history }).catch(() => {}); } +// --- Ping / Latency --- +function sendPing() { + const t = Date.now(); + pendingPingT = t; + emit(EVENTS.PING, { t }); + if (pingTimeout) clearTimeout(pingTimeout); + pingTimeout = setTimeout(() => { + if (pendingPingT === t) { + pendingPingT = null; + } + pingTimeout = null; + }, 5000); +} + +function startPing() { + if (pingInterval) clearInterval(pingInterval); + if (pingTimeout) { clearTimeout(pingTimeout); pingTimeout = null; } + currentPingMs = null; + pendingPingT = null; + pingInterval = setInterval(sendPing, 15000); + sendPing(); +} + +function stopPing() { + if (pingInterval) { + clearInterval(pingInterval); + pingInterval = null; + } + if (pingTimeout) { + clearTimeout(pingTimeout); + pingTimeout = null; + } + currentPingMs = null; + pendingPingT = null; +} + // --- Event Handlers --- function handleServerEvent(event, data) { if (!data) { @@ -875,6 +920,11 @@ function handleServerEvent(event, data) { routeToContent(event, data); break; + case EVENTS.PING: + if (data && typeof data.t === 'number' && Number.isFinite(data.t) && data.sender) { + emit(EVENTS.PONG, { t: data.t, target: data.sender }); + } + break; case EVENTS.EVENT_ACK: if (lastActionState && lastActionState.action && data?.senderId) { // Correlation Check: Only accept ACK if it matches our current action's timestamp @@ -1011,6 +1061,20 @@ function handleServerEvent(event, data) { addLog(`Episode lobby for "${title}" cancelled by ${data.senderId || 'peer'}`, 'warn'); } break; + case EVENTS.PONG: + if (data && typeof data.t === 'number' && Number.isFinite(data.t)) { + if (pendingPingT === data.t) { + pendingPingT = null; + if (pingTimeout) { + clearTimeout(pingTimeout); + pingTimeout = null; + } + const rtt = Date.now() - data.t; + currentPingMs = (rtt >= 0 && rtt < 30000) ? rtt : null; + chrome.runtime.sendMessage({ type: 'PING_UPDATE', ping: currentPingMs }).catch(() => {}); + } + } + break; default: addLog(`Received unknown event from server: ${event}`, 'warn'); break; @@ -1372,7 +1436,8 @@ async function handleAsyncMessage(message, sender, sendResponse) { serverUrl: currentServerUrl, version: chrome.runtime.getManifest().version, protocolVersion: PROTOCOL_VERSION, - roomPassword: currentRoom ? currentRoom.password : null + roomPassword: currentRoom ? currentRoom.password : null, + ping: currentPingMs }); } else if (message.type === 'LEAVE_ROOM') { resetAudioProcessingInTab(currentTabId); diff --git a/extension/popup.html b/extension/popup.html index 34f0faa..a7c78fa 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -598,6 +598,7 @@