diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-24 20:30:12 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-24 20:30:12 +0200 |
| commit | ed97f138cfc0aee5ac04e4d12b9e392277cbcc40 (patch) | |
| tree | 9206bf295ebbb98a2805b82a5732b4a8401277b4 /packages/meshbay-hub/src/meshbay_hub/static/music-player.js | |
| parent | 28be8eead316f5026e1c0c8ffd46b873d479091d (diff) | |
| download | meshbay-ed97f138cfc0aee5ac04e4d12b9e392277cbcc40.tar.gz | |
fix(hub): auto-skip a track that fails to load instead of stalling the queue
A genuinely corrupt source file (found via a real "1 track, fails to play"
report) would stop a "play all" queue dead with no way forward except
manually picking the next track. The player now counts consecutive load
failures and advances past them on its own, capped so a pathological queue
(everything broken) doesn't spin forever; the existing Promise-rejection
catch and a new <audio> onerror handler both feed the same counter, since a
decode failure can surface either way depending on the browser.
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/music-player.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/music-player.js | 36 |
1 files changed, 35 insertions, 1 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 eac011a..690efc9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js @@ -136,9 +136,25 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) { const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [showQueue, setShowQueue] = useState(false); + // A broken source file (truncated download, a format the browser has no + // decoder for) must not stall a "play all" queue on the one track that + // failed — found live against a real library: a corrupt few-hundred-byte + // file with no audio stream at all, sitting between two good tracks. + // Bounded so a queue that turns out to be *entirely* bad (every file the + // same unsupported format) fails once, visibly, rather than burning + // through the whole list in an instant. + const consecutiveFailuresRef = useRef(0); + const MAX_CONSECUTIVE_FAILURES = 5; const currentTrack = tracks[order[pos]] || null; + const advancePastFailure = useCallback(() => { + consecutiveFailuresRef.current += 1; + if (consecutiveFailuresRef.current > MAX_CONSECUTIVE_FAILURES || order.length <= 1) return; + if (pos + 1 < order.length) setPos(pos + 1); + else if (repeat === 'all') setPos(0); + }, [order.length, pos, repeat]); + // Stops playback the moment this bar goes away for any reason -- the // close button below, or the shell tearing it down on its own (leaving // the group, switching to a different one). A component removed from the @@ -200,6 +216,7 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) { setOrder(initialOrder); setPos(startPos); setError(''); + consecutiveFailuresRef.current = 0; // Playback itself starts from the effect below, keyed on [tracks, order, pos]. }, [queue]); @@ -220,12 +237,14 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) { audio.src = url; audio.currentTime = 0; await audio.play(); + consecutiveFailuresRef.current = 0; setPlaying(true); prefetchNext(pos); } catch (err) { if (loadTokenRef.current !== myToken) return; setError(err.message || String(err)); setPlaying(false); + advancePastFailure(); } finally { if (loadTokenRef.current === myToken) setLoading(false); } @@ -233,6 +252,21 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) { // eslint-disable-next-line }, [currentTrack && currentTrack.id]); + // 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 + // decode failure can also surface later, asynchronously, as an `error` + // event on the element itself rather than a rejected promise — found live + // to matter (a truncated file with a valid-looking header but no audio + // stream). Both paths converge on the same bounded advance, so whichever + // fires first, the queue moves on exactly once. + const handleMediaError = useCallback(() => { + const audio = audioRef.current; + const mediaError = audio && audio.error; + setError((mediaError && mediaError.message) || t('music.err_playback')); + setPlaying(false); + advancePastFailure(); + }, [advancePastFailure]); + useEffect(() => { const audio = audioRef.current; if (audio) audio.volume = volume; @@ -321,7 +355,7 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) { <audio ref=${audioRef} onTimeUpdate=${(e) => setCurrentTime(e.target.currentTime)} onDurationChange=${(e) => setDuration(e.target.duration)} - onEnded=${onEnded} /> + onEnded=${onEnded} onError=${handleMediaError} /> <div class="music-player-info"> <${Icon} name="music" cls="music-player-icon" /> <div class="music-player-text"> |