From 27d59cbb2d11d863273e2282257d81ac9911ab97 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 26 Aug 2026 11:17:37 +0200 Subject: fix(transport): auto-reconnect after WebRTC failure, without dropping in-flight streams/downloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed live (client trace + node logs, mobile screen-lock ~5min): ICE goes disconnected -> failed within ~10s on both ends, but the DataChannel's readyState stays "open" throughout, so nothing failed fast — every request just sat out its own 8s/30s timeout, matching the reported symptom (poster spinners, blocked chat, dead new streams). transport.js: on connectionState "failed", reject pending requests immediately (TransportLostError) and start a self-contained reconnect loop (capped exponential backoff, redoes the full signaling handshake — the node already discards the old session on its own "failed"/"closed", so there is nothing lower-level to resume). New hooks: onNeedToken (fetch a fresh JWT, since the captured one may have expired during the outage) and onReconnected (let a consumer resume something that was mid-flight). file-utils.js: pipelinedDownload retries a lost chunk instead of aborting the whole transfer — covers Files downloads, poster/thumbnail fetches, and music-player.js's blob-based track download, all of which go through it. video-player.js: onReconnected reissues the existing seek-to-current-time path, which already knows how to land a new stream_init on the live SourceBuffer without resetting playback. Playing audio is unaffected either way — musicbay.md's design downloads a track to a blob before playing it, so a dead transport was never a network dependency for what is already playing. Stays on this branch until confirmed by real-device testing. --- .../src/meshbay_hub/static/video-player.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/video-player.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-player.js b/packages/meshbay-hub/src/meshbay_hub/static/video-player.js index 82e1116..78760d8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/video-player.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/video-player.js @@ -529,6 +529,24 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) { setPhase('error'); }; + // The old stream died with the connection (the node retires it the + // moment its session goes away — see webrtc_server.py's + // on_state_change), so there is nothing to resume on the wire, only a + // reason to ask again. requestSeek already knows how to land a new + // stream_init on the live SourceBuffer without resetting playback — + // exactly what dragging the scrubber does — so reusing it here means a + // screen-lock reconnect looks like a seek to where the film already + // was, not a reload. + transport.onReconnected = () => { + if (cancelled) return; + const v = videoRef.current; + const seek = requestSeekRef.current; + if (!v || !seek) return; + console.log('[MeshBay] transport reconnected — resuming stream at', + v.currentTime.toFixed(1)); + seek(v.currentTime); + }; + transport.onStreamInit = (msg) => { if (cancelled) return; if (msg.file_id && msg.file_id !== entry.id) return; @@ -849,6 +867,7 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) { transport.onStreamData = null; transport.onStreamEnd = null; transport.onStreamError = null; + transport.onReconnected = null; } // The queue can hold several megabytes of decrypted video. queueRef.current = []; -- cgit v1.2.3 From ab96d5821fe9cda4f31b3389e1c6fb3e6010682d Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 26 Aug 2026 11:35:31 +0200 Subject: feat(video): hold a Screen Wake Lock while a video is open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Purely additive and isolated from the streaming/transport code: touches no DataChannel, SourceBuffer, or playback state, so it cannot itself cause a stall or regress existing playback (PC/Electron included) — feature-detected and try/caught, a no-op wherever unsupported or refused. Sidesteps the commonest real-world trigger for the WebRTC-drop recovery (the phone auto-locking on its own idle timer while someone just watches). Does nothing for a deliberate power-button lock or backgrounding the tab — released automatically in both cases per spec — so the reconnect path is still what handles those. --- .../src/meshbay_hub/static/video-player.js | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/video-player.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-player.js b/packages/meshbay-hub/src/meshbay_hub/static/video-player.js index 78760d8..0c0c6c7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/video-player.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/video-player.js @@ -164,6 +164,10 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) { const castDeviceRef = useRef(null); const castRestartGenRef = useRef(0); const landingPlayheadRef = useRef(false); + // The current Screen Wake Lock sentinel, if the browser granted one — see + // the effect below. Null on any platform/context that does not support it, + // which playback has never depended on. + const wakeLockRef = useRef(null); /** * The buffered range the playhead is actually in, or null. @@ -780,11 +784,49 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) { if (t && t.connected) t.stopStream(); }; const onPageHide = () => leave('pagehide'); + + // Screen Wake Lock: keeps the display on while this page is open and + // visible, purely so the phone stops auto-locking mid-film on its own + // idle timer — the commonest real-world trigger for the WebRTC-drop + // recovery above, and the one case it can sidestep entirely rather than + // recover from. Unrelated to streaming/transport in every direction: + // requesting, holding, or losing this lock touches no DataChannel, no + // SourceBuffer, no playback state, so it cannot itself cause a stall or + // a regression in the existing pipeline. It also does nothing at all on + // a phone the user locks with the power button, or once the tab is + // backgrounded (the spec releases it automatically) — the reconnect path + // above is still the one that has to handle those. + const releaseWakeLock = () => { + const wl = wakeLockRef.current; + wakeLockRef.current = null; + if (wl) { try { wl.release(); } catch { /* already released */ } } + }; + const acquireWakeLock = async () => { + if (!('wakeLock' in navigator)) return; + try { + const wl = await navigator.wakeLock.request('screen'); + // The effect may have torn down while this was in flight. + if (cancelled) { try { wl.release(); } catch { /* ignore */ } return; } + wakeLockRef.current = wl; + wl.addEventListener('release', () => { wakeLockRef.current = null; }); + } catch (e) { + // Battery saver, no permission, an insecure context — playback has + // never depended on this, so there is nothing to fall back to. + console.warn('[MeshBay] Wake lock request failed:', e.message); + } + }; + acquireWakeLock(); + // NOT wired to stopStream. Android fires visibilitychange when a video goes // fullscreen, so cutting the stream here killed the film the moment it was // watched properly. Logged only, until that is confirmed or ruled out. const onVisibility = () => { console.log('[MeshBay] visibilitychange:', document.visibilityState); + // The lock is released automatically the moment the page goes hidden + // (spec behaviour, not something to undo) — re-requesting it here is + // what makes it hold again once the film is actually back on screen, + // including the fullscreen transition this handler already exists for. + if (document.visibilityState === 'visible') acquireWakeLock(); }; window.addEventListener('pagehide', onPageHide); document.addEventListener('visibilitychange', onVisibility); @@ -838,6 +880,7 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) { clearInterval(pumpTimer); clearInterval(diagTimer); clearTimeout(seekTimerRef.current); + releaseWakeLock(); // Closing the player is the commonest way to stop watching, so this is // the write that matters most. if (videoRef.current) { -- cgit v1.2.3