diff --git a/src/player.js b/src/player.js index beac6d4..19e3374 100644 --- a/src/player.js +++ b/src/player.js @@ -58,6 +58,8 @@ function _clearPreload() { } // ─── PLAYBACK ─── +let _playGen = 0; // incremented on every playCurrent() — stale callbacks check this + export function playQueue(songs, startIdx) { S.queue = songs.map(s => ({...s})); S.qIdx = startIdx; @@ -70,6 +72,7 @@ export function playQueue(songs, startIdx) { export function playCurrent() { if(S.qIdx < 0 || S.qIdx >= S.queue.length) return; const s = S.queue[S.qIdx]; + const gen = ++_playGen; // capture generation — any callback that sees gen !== _playGen is stale S._trackChanging = true; // Keep Samsung notification alive — set playing BEFORE metadata swap if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing'; @@ -80,16 +83,19 @@ export function playCurrent() { _clearPreload(); S.audio.src = src; - const _reset = () => { S._trackChanging = false; }; + // Only resets _trackChanging if no newer playCurrent() has started since. + // Prevents rapid skips from letting a stale reset() clobber the active transition. + const _reset = () => { if (_playGen === gen) S._trackChanging = false; }; - // After play() fails: wait for canplay (audio engine, not throttled by Android timers). - // _trackChanging stays TRUE throughout so spurious pause events from Android revoking - // audio focus on locked screen can't slip through and dismiss the Samsung cover screen. + // Wait for canplay (audio engine — not throttled by Android background timers). + // _trackChanging stays TRUE so Android audio-focus-loss pause events can't reach + // Samsung and dismiss the lock screen notification during recovery. function _waitCanPlay() { const guard = setTimeout(_reset, 10000); // 10s Kima-style failsafe S.audio.addEventListener('canplay', () => { clearTimeout(guard); - if (S.queue[S.qIdx]?.id !== s.id || !S.audio.paused) { _reset(); return; } + if (_playGen !== gen) return; // newer playCurrent() took over — do nothing + if (!S.audio.paused) return; // already playing — play event will clear flag try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {} S.audio.play().catch(_reset); }, { once: true }); @@ -101,16 +107,13 @@ export function playCurrent() { .then(() => clearTimeout(_initGuard)) .catch(err => { clearTimeout(_initGuard); - if (S.queue[S.qIdx]?.id !== s.id) { _reset(); return; } + if (_playGen !== gen) return; // superseded — let the newer call handle everything if (err?.name === 'AbortError') { - // Audio element in bad state (OS interruption / load race) — reload so the - // element settles; _trackChanging stays true so load()'s pause is suppressed + // Audio element in bad state — reload so it settles; _trackChanging stays true + // so load()'s pause event is still suppressed S.audio.src = src; S.audio.load(); } - // For ALL errors: keep _trackChanging=true until canplay fires (or 10s timeout). - // This prevents Android audio-focus-loss pause events from reaching Samsung - // and dismissing the lock screen notification during the recovery window. _waitCanPlay(); }); }