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 | |
| 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')
11 files changed, 45 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index 70d2b95..6df5a77 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -189,6 +189,7 @@ export default { other: '{n} Titel', }, 'music.err_transport': 'Transport nicht verbunden', + 'music.err_playback': 'Wiedergabefehler', 'music.player_play': 'Abspielen', 'music.player_pause': 'Pause', 'music.player_prev': 'Vorheriger', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 0ee0972..4c0db39 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -187,6 +187,7 @@ export default { other: '{n} tracks', }, 'music.err_transport': 'Transport not connected', + 'music.err_playback': 'Playback error', 'music.player_play': 'Play', 'music.player_pause': 'Pause', 'music.player_prev': 'Previous', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 69e2687..2d795fc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -187,6 +187,7 @@ export default { other: '{n} pistas', }, 'music.err_transport': 'Transporte no conectado', + 'music.err_playback': 'Error de reproducción', 'music.player_play': 'Reproducir', 'music.player_pause': 'Pausa', 'music.player_prev': 'Anterior', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 8d52d37..0ce4c53 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -188,6 +188,7 @@ export default { other: '{n} pistes', }, 'music.err_transport': 'Transport non connecté', + 'music.err_playback': 'Erreur de lecture', 'music.player_play': 'Lecture', 'music.player_pause': 'Pause', 'music.player_prev': 'Précédent', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index dfd4bbc..2ddea64 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -188,6 +188,7 @@ export default { other: '{n} tracce', }, 'music.err_transport': 'Trasporto non connesso', + 'music.err_playback': 'Errore di riproduzione', 'music.player_play': 'Riproduci', 'music.player_pause': 'Pausa', 'music.player_prev': 'Precedente', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 10d2088..82a8fc2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -185,6 +185,7 @@ export default { other: '{n}曲', }, 'music.err_transport': 'トランスポートが接続されていません', + 'music.err_playback': '再生エラー', 'music.player_play': '再生', 'music.player_pause': '一時停止', 'music.player_prev': '前へ', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 3342559..be0184a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -189,6 +189,7 @@ export default { other: '{n} nummers', }, 'music.err_transport': 'Transport niet verbonden', + 'music.err_playback': 'Afspeelfout', 'music.player_play': 'Afspelen', 'music.player_pause': 'Pauzeren', 'music.player_prev': 'Vorige', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 1b52a8b..0738cf5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -198,6 +198,7 @@ export default { other: '{n} utworu', }, 'music.err_transport': 'Transport niepołączony', + 'music.err_playback': 'Błąd odtwarzania', 'music.player_play': 'Odtwórz', 'music.player_pause': 'Pauza', 'music.player_prev': 'Poprzedni', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 1b8c50c..1874a5b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -189,6 +189,7 @@ export default { other: '{n} faixas', }, 'music.err_transport': 'Transporte não conectado', + 'music.err_playback': 'Erro de reprodução', 'music.player_play': 'Reproduzir', 'music.player_pause': 'Pausar', 'music.player_prev': 'Anterior', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 5cc6cb3..08fd940 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -182,6 +182,7 @@ export default { other: '{n} 首曲目', }, 'music.err_transport': '传输未连接', + 'music.err_playback': '播放错误', 'music.player_play': '播放', 'music.player_pause': '暂停', 'music.player_prev': '上一首', 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"> |