diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2992dbe..0114cf1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,9 +4,18 @@ All notable changes to the KoalaSync browser extension and relay server. --- -## [v2.2.5] — Unreleased +## [v2.3.0] — Unreleased + +### Added +- **Extension: New Interactive Onboarding Tour**: A fully redesigned, interactive step-by-step onboarding experience. +- **Extension: Auto-Switch to Sync Tab**: The UI now intelligently switches to the Sync tab when you join a room to guide video selection. ### Fixed +- **Extension: Infinite Seek Loop Prevention**: Replaced the fragile time-based seek suppression with an exact target-time verification mechanism, entirely eliminating infinite seek loops on slow buffers. +- **Extension: Zombie Connections Resolved**: Implemented a forced disconnect upon ping timeouts, ensuring the extension reliably auto-reconnects when the WebSocket hangs in a half-open state. +- **Extension: Room Switching Architecture**: Joining a new room while already connected now explicitly severs the old connection first, preventing state cross-contamination. +- **Extension: Join/Leave Race Conditions**: Added UI locks to prevent users from accidentally sending conflicting connection commands via rapid double-clicking. +- **Extension: Same-Room Invite Bypass**: Clicking an invite link for the room you are currently in no longer triggers a redundant reconnect, instead instantly confirming the join. - **Extension: Audio settings now propagate immediately to video tabs**: Changes made in the audio options page are now instantly applied to the active video tab. Previously, settings saved to `chrome.storage.local` were not picked up by the background listener, which only watched `chrome.storage.sync`. - **Extension: Audio compressor now logs enable/disable state and resume failures**: The compressor reports when it is activated or bypassed, and warns if the `AudioContext` cannot be resumed (e.g. browser autoplay policy requires a user gesture on the page first). - **Extension: Video heartbeat no longer sent when alone in a room**: The full media metadata `PEER_STATUS` is now only emitted when other peers are present. The session keepalive (background heartbeat) continues to run unaffected, preventing the server reaper from disconnecting idle peers. diff --git a/extension/background.js b/extension/background.js index 04f1f12..f773c91 100644 --- a/extension/background.js +++ b/extension/background.js @@ -708,7 +708,9 @@ function sendPing() { if (pingTimeout) clearTimeout(pingTimeout); pingTimeout = setTimeout(() => { if (pendingPingT === t) { + addLog('Ping timeout reached, force disconnecting to trigger reconnect', 'warn'); pendingPingT = null; + forceDisconnect(); } pingTimeout = null; }, 5000); @@ -1333,9 +1335,7 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { function leaveOldRoomIfSwitching(newRoomId) { if (currentRoom && currentRoom.roomId !== newRoomId) { addLog(`Switching rooms: leaving ${currentRoom.roomId} to join ${newRoomId}`, 'info'); - if (socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined) { - emit(EVENTS.LEAVE_ROOM, { peerId }); - } + forceDisconnect(); currentRoom = null; if (storageInitialized) chrome.storage.session.set({ currentRoom: null }); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {}); @@ -1389,15 +1389,27 @@ async function handleAsyncMessage(message, sender, sendResponse) { await ensureState(); if (message.type === 'CONNECT') { + const settings = await getSettings(); + const desiredUrl = resolveServerUrl(settings); + + if (settings.roomId && currentRoom && currentRoom.roomId === settings.roomId && socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined && desiredUrl === currentServerUrl) { + broadcastConnectionStatus('connected'); + const tabs = await new Promise(resolve => chrome.tabs.query({}, resolve)); + for (const tab of tabs) { + chrome.tabs.sendMessage(tab.id, { type: 'JOIN_STATUS', success: true, message: 'Already in room' }).catch(() => {}); + } + if (typeof sendResponse === 'function') sendResponse({ status: 'ok' }); + return; + } + reconnectFailed = false; reconnectStartTime = null; reconnectAttempts = 0; chrome.storage.session.set({ reconnectFailed: false, reconnectAttempts: 0, reconnectStartTime: null }); - const settings = await getSettings(); + if (settings.roomId) { leaveOldRoomIfSwitching(settings.roomId); } - const desiredUrl = resolveServerUrl(settings); if (desiredUrl !== currentServerUrl || !socket || socket.readyState !== WebSocket.OPEN || !isNamespaceJoined) { if (desiredUrl !== currentServerUrl) forceDisconnect(); connect(); @@ -1493,14 +1505,25 @@ async function handleAsyncMessage(message, sender, sendResponse) { useCustomServer: !!useCustomServer, serverUrl: serverUrl || '' }, async () => { + const settings = await getSettings(); + const desiredUrl = resolveServerUrl(settings); + + if (roomId && currentRoom && currentRoom.roomId === roomId && socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined && desiredUrl === currentServerUrl) { + broadcastConnectionStatus('connected'); + const tabs = await new Promise(resolve => chrome.tabs.query({}, resolve)); + for (const tab of tabs) { + chrome.tabs.sendMessage(tab.id, { type: 'JOIN_STATUS', success: true, message: 'Already in room' }).catch(() => {}); + } + return; + } + reconnectFailed = false; reconnectStartTime = null; reconnectAttempts = 0; chrome.storage.session.set({ reconnectFailed: false, reconnectAttempts: 0, reconnectStartTime: null }); broadcastConnectionStatus('connecting'); leaveOldRoomIfSwitching(roomId); - const settings = await getSettings(); - const desiredUrl = resolveServerUrl(settings); + if (desiredUrl !== currentServerUrl || !socket || socket.readyState !== WebSocket.OPEN || !isNamespaceJoined) { if (desiredUrl !== currentServerUrl) forceDisconnect(); connect(); diff --git a/extension/content.js b/extension/content.js index b986aa5..b91c6c2 100644 --- a/extension/content.js +++ b/extension/content.js @@ -55,6 +55,7 @@ const MIN_SEEK_DELTA = 2.0; let lastReportedSeekTime = null; // last currentTime we relayed as a SEEK let seekDebounceTimer = null; // debounce timer for rapid seek events + let expectedSeekTime = null; // strictly track programmatic seeks // --- Episode Auto-Sync State --- let lastKnownMediaTitle = null; @@ -438,7 +439,7 @@ ytButton.click(); } if (action === EVENTS.SEEK) { - _setSuppress('seek'); + expectedSeekTime = data.targetTime; video.currentTime = data.targetTime; } return; @@ -454,7 +455,7 @@ twitchButton.click(); } if (action === EVENTS.SEEK) { - _setSuppress('seek'); + expectedSeekTime = data.targetTime; video.currentTime = data.targetTime; } return; @@ -472,7 +473,7 @@ _setSuppress('paused'); video.pause(); } else if (action === EVENTS.SEEK) { - _setSuppress('seek'); + expectedSeekTime = data.targetTime; video.currentTime = data.targetTime; } } catch (e) { @@ -575,7 +576,7 @@ return; } _setSuppress('paused'); - _setSuppress('seek'); + expectedSeekTime = payload.targetTime; video.pause(); try { video.currentTime = payload.targetTime; @@ -865,11 +866,17 @@ const current = video.currentTime; if (!Number.isFinite(current)) return; - // Step 1: Check _suppressTimers (programmatic seek from remote peer) - if (_suppressTimers['seek']) { - _clearSuppress('seek'); - lastReportedSeekTime = current; - return; + // Step 1: Check expectedSeekTime (programmatic seek from remote peer) + if (expectedSeekTime !== null) { + if (Math.abs(current - expectedSeekTime) < 1.0) { + // Video arrived at expected time. Safely clear and ignore. + expectedSeekTime = null; + lastReportedSeekTime = current; + return; + } else { + // User manually scrubbed to a DIFFERENT time while we were buffering + expectedSeekTime = null; + } } // Step 2: Suppress during visibility grace period (tab re-focus ghost events) diff --git a/extension/locales/de.json b/extension/locales/de.json index 0d9e91e..42c1b11 100644 --- a/extension/locales/de.json +++ b/extension/locales/de.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "Equalizer", - "AUDIO_COMING_SOON": "Demnächst" + "AUDIO_COMING_SOON": "Demnächst", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/locales/en.json b/extension/locales/en.json index 9434d5b..b1ccc42 100644 --- a/extension/locales/en.json +++ b/extension/locales/en.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "Equalizer", - "AUDIO_COMING_SOON": "Coming soon" + "AUDIO_COMING_SOON": "Coming soon", + "BTN_RESTART_TOUR": "Restart Tutorial", + "BTN_RESTART_TOUR_TOOLTIP": "Restart the onboarding tutorial", + "HINT_SELECT_VIDEO": "Select your video here!" } diff --git a/extension/locales/es.json b/extension/locales/es.json index 64525e1..97b4495 100644 --- a/extension/locales/es.json +++ b/extension/locales/es.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "Ecualizador", - "AUDIO_COMING_SOON": "Próximamente" + "AUDIO_COMING_SOON": "Próximamente", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/locales/fr.json b/extension/locales/fr.json index e7a8105..83ad29c 100644 --- a/extension/locales/fr.json +++ b/extension/locales/fr.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "Égaliseur", - "AUDIO_COMING_SOON": "Bientôt disponible" + "AUDIO_COMING_SOON": "Bientôt disponible", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/locales/it.json b/extension/locales/it.json index 62d45de..c1af1cc 100644 --- a/extension/locales/it.json +++ b/extension/locales/it.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "Equalizzatore", - "AUDIO_COMING_SOON": "Prossimamente" + "AUDIO_COMING_SOON": "Prossimamente", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/locales/ja.json b/extension/locales/ja.json index 4cafb04..bbcff90 100644 --- a/extension/locales/ja.json +++ b/extension/locales/ja.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "イコライザー", - "AUDIO_COMING_SOON": "近日公開" + "AUDIO_COMING_SOON": "近日公開", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/locales/ko.json b/extension/locales/ko.json index 60a30f9..fe61d49 100644 --- a/extension/locales/ko.json +++ b/extension/locales/ko.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "이퀄라이저", - "AUDIO_COMING_SOON": "출시 예정" + "AUDIO_COMING_SOON": "출시 예정", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/locales/nl.json b/extension/locales/nl.json index 9039166..7ea5786 100644 --- a/extension/locales/nl.json +++ b/extension/locales/nl.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "Equalizer", - "AUDIO_COMING_SOON": "Binnenkort beschikbaar" + "AUDIO_COMING_SOON": "Binnenkort beschikbaar", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/locales/pl.json b/extension/locales/pl.json index 4f141e4..24f0eb2 100644 --- a/extension/locales/pl.json +++ b/extension/locales/pl.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "Korektor", - "AUDIO_COMING_SOON": "Wkrótce" + "AUDIO_COMING_SOON": "Wkrótce", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/locales/pt-BR.json b/extension/locales/pt-BR.json index 0bedc1d..f15f2d6 100644 --- a/extension/locales/pt-BR.json +++ b/extension/locales/pt-BR.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "Equalizador", - "AUDIO_COMING_SOON": "Em breve" + "AUDIO_COMING_SOON": "Em breve", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/locales/pt.json b/extension/locales/pt.json index d791af6..8250a52 100644 --- a/extension/locales/pt.json +++ b/extension/locales/pt.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "Equalizador", - "AUDIO_COMING_SOON": "Em breve" + "AUDIO_COMING_SOON": "Em breve", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/locales/ru.json b/extension/locales/ru.json index 880e6c7..b7ed9f2 100644 --- a/extension/locales/ru.json +++ b/extension/locales/ru.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "Эквалайзер", - "AUDIO_COMING_SOON": "Скоро" + "AUDIO_COMING_SOON": "Скоро", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/locales/tr.json b/extension/locales/tr.json index 3288431..0529502 100644 --- a/extension/locales/tr.json +++ b/extension/locales/tr.json @@ -205,5 +205,8 @@ "AUDIO_PARAM_ATTACK": "Attack", "AUDIO_PARAM_RELEASE": "Release", "AUDIO_EQUALIZER": "Ekolayzer", - "AUDIO_COMING_SOON": "Çok yakında" + "AUDIO_COMING_SOON": "Çok yakında", + "BTN_RESTART_TOUR": "", + "BTN_RESTART_TOUR_TOOLTIP": "", + "HINT_SELECT_VIDEO": "" } diff --git a/extension/popup.html b/extension/popup.html index 44f967f..8f31a03 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -364,6 +364,14 @@ 0%, 100% { opacity: 0.4; transform: scale(1); } 50% { opacity: 1; transform: scale(1.3); } } + @keyframes spotlightPulse { + 0%, 100% { box-shadow: 0 0 0 9999px rgba(15, 23, 42, 0.65), 0 0 0 0px var(--accent); } + 50% { box-shadow: 0 0 0 9999px rgba(15, 23, 42, 0.65), 0 0 0 6px var(--accent); } + } + @keyframes floatHint { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-4px); } + }
@@ -457,11 +465,16 @@Use this if you see "Duplicate Identity" errors.