diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/music-player.js | 63 |
1 files changed, 60 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js index 154d761..f0f7c47 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js @@ -217,6 +217,13 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs, onSaveQueue // one of them being off is a property of that group rather than of each of // its tracks in turn โ see advancePastFailure below (docs/playlists.md ยง11.3). const downGroupsRef = useRef(new Set()); + // When the page last came back from hidden. A phone with its screen off + // drops every connection at once, so a track that fails for lack of one + // while the page sleeps, or just after it wakes, says nothing about its + // group: it is retried rather than skipped (see the load effect below). + const wokeAtRef = useRef(0); + const [retryNonce, setRetryNonce] = useState(0); + const WAKE_GRACE_MS = 30000; const currentTrack = tracks[order[pos]] || null; @@ -345,7 +352,11 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs, onSaveQueue } if (!transport) throw groupDown(); if (!transport.connected) await transport.waitForReconnect(); - if (!transport.connected) throw groupDown(); + if (!transport.connected) { + const e = groupDown(); + e.transport = transport; + throw e; + } let downloadId = entry.id; let downloadSize = entry.size; @@ -441,8 +452,46 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs, onSaveQueue // effect above (pos/order/tracks all just changed together) and also // after skipNext/skipPrev/onEnded update `pos` alone. useEffect(() => { + const onVisibility = () => { + if (document.visibilityState === 'visible') wokeAtRef.current = Date.now(); + }; + document.addEventListener('visibilitychange', onVisibility); + return () => document.removeEventListener('visibilitychange', onVisibility); + }, []); + + const asleepOrJustWoke = () => document.visibilityState === 'hidden' + || Date.now() - wokeAtRef.current < WAKE_GRACE_MS; + + // Retries the current track once the page is back and a connection may be: + // the screen coming on, the network coming back, or the track's own + // transport finishing its reconnect, whichever comes first. A visible page + // also retries on a timer, so a failure that outlives the grace window + // ends on the ordinary error path instead of a spinner nobody can clear. + const retryWhenAwake = (myToken, transport) => { + let stop = () => {}; + let timer = null; + const fire = () => { + stop(); + if (loadTokenRef.current === myToken) setRetryNonce((n) => n + 1); + }; + const onVisibility = () => { if (document.visibilityState === 'visible') fire(); }; + document.addEventListener('visibilitychange', onVisibility); + window.addEventListener('online', fire); + const unsubscribe = transport ? transport.addReconnectListener(fire) : () => {}; + if (document.visibilityState === 'visible') timer = setTimeout(fire, 10000); + stop = () => { + clearTimeout(timer); + document.removeEventListener('visibilitychange', onVisibility); + window.removeEventListener('online', fire); + unsubscribe(); + }; + if (transport && transport.connected) fire(); + }; + + useEffect(() => { if (!currentTrack) return; const myToken = ++loadTokenRef.current; + let waiting = false; setLoading(true); setError(''); (async () => { @@ -462,17 +511,25 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs, onSaveQueue // Autoplay refused (no recent tap): the track is loaded and fine, it // waits for the play button. Not an error, and no reason to skip it. if (err && err.name === 'NotAllowedError') { setPlaying(false); prefetchNext(pos); return; } + if (err && err.isGroupDown && asleepOrJustWoke()) { + // Our own connection is what went, not the group: keep the spinner + // and this track, and try it again once we are back. + waiting = true; + setPlaying(false); + retryWhenAwake(myToken, err.transport); + return; + } setError(err.message || String(err)); setPlaying(false); // `fetchTrackBlob` throws `err_transport` when no node answered for // this track's group; anything else is about the file itself. advancePastFailure(err.isGroupDown ? currentTrack.groupId : null); } finally { - if (loadTokenRef.current === myToken) setLoading(false); + if (loadTokenRef.current === myToken && !waiting) setLoading(false); } })(); // eslint-disable-next-line - }, [currentTrack && currentTrack.id]); + }, [currentTrack && currentTrack.id, retryNonce]); // Belt and suspenders alongside the catch block above: `.play()` rejecting // is the common path for a source the browser can't decode at all, but a |