aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/video-player.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-26 15:09:20 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-26 15:09:20 +0200
commitdb7f81fd08742847f3ebea061c75530b7b31b934 (patch)
tree14b308686860eea0a9c12c47c39f8a68a1bba376 /packages/meshbay-hub/src/meshbay_hub/static/video-player.js
parent7126fd3c265ba75d77b449bfd0f83f5f3e584b74 (diff)
parent59d9f50bf41b2b38b7c95f8da52b698dff923c2d (diff)
downloadmeshbay-db7f81fd08742847f3ebea061c75530b7b31b934.tar.gz
Merge branch 'debug/webrtc-lock-resume'
WebRTC transport dies silently after an extended mobile screen lock (confirmed live via client-side trace + node logs): ICE goes disconnected -> failed within ~10s of each other on both ends, but the DataChannel's readyState stays "open" throughout, so nothing failed fast — every request just sat out its own timeout, matching the reported symptom (poster spinners, blocked chat, dead new streams, stuck music). - Automatic reconnect on WebRTC "failed": capped exponential backoff, redoes the full signaling handshake, wakes immediately on visibilitychange instead of waiting out a throttled backoff timer. - Fixed two real bugs the reconnect work exposed: the signaling POST to the hub kept using the token captured at construction, never the fresh one fetched per reconnect attempt (401 loop, no possible recovery); and connect() re-armed a diagnostic listener/interval on every attempt without disposing of the previous one. - pipelinedDownload retries a lost chunk instead of aborting the whole transfer — covers Files downloads, video poster/thumbnail fetches, and music-player.js's blob-based track download. - music-player.js: don't throw "Transport not connected" while a reconnect is already landing (waitForReconnect); prefetch depth now adapts to network type (5 tracks ahead on Wi-Fi, 3 on cellular or unrecognized — Firefox/Safari included, where the detection API is simply absent). - video-player.js: onReconnected reissues the existing seek-to-current-time path, so a mid-stream reconnect looks like an ordinary seek rather than a dead player; holds a Screen Wake Lock unconditionally while open. - New opt-in (off by default) user preference: keep the screen on during audio playback, for whoever wants to trade battery for sidestepping the screen-lock gap entirely — off by default because the ordinary expectation (matching Spotify/Deezer) is that the phone locks on its own while listening. - hub: /app and / now serve Cache-Control: no-store — the SPA shell had no cache header at all, so a browser that cached it heuristically could keep re-serving an old build (old ASSET_V, old JS) through any number of reloads or pull-to-refreshes. Verified against real production use across many rounds (demo groups, actual mobile screen-lock testing) rather than synthetic reproduction alone. 430 hub tests + 650 node tests passing throughout.
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/video-player.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/video-player.js62
1 files changed, 62 insertions, 0 deletions
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..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.
@@ -529,6 +533,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;
@@ -762,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);
@@ -820,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) {
@@ -849,6 +910,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 = [];