aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-24 20:30:12 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-24 20:30:12 +0200
commited97f138cfc0aee5ac04e4d12b9e392277cbcc40 (patch)
tree9206bf295ebbb98a2805b82a5732b4a8401277b4 /packages/meshbay-hub/src/meshbay_hub/static/music-player.js
parent28be8eead316f5026e1c0c8ffd46b873d479091d (diff)
downloadmeshbay-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.js36
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">