fix: generation counter prevents stale _reset() from clobbering active transition
When skipping rapidly, old playCurrent() closures had _reset() callbacks that could fire _trackChanging=false while a newer transition was still in progress — breaking both notification guard and background recovery. Each playCurrent() now captures a generation number (++_playGen). _reset() is a no-op if _playGen has advanced (newer call took over). canplay and play().catch handlers also check the generation before acting. This makes all stale callbacks safe regardless of how quickly the user skips. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
b941672a77
commit
ec13124d99
1 file changed
+14
-11
+14
-11
@@ -58,6 +58,8 @@ function _clearPreload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── PLAYBACK ───
|
// ─── PLAYBACK ───
|
||||||
|
let _playGen = 0; // incremented on every playCurrent() — stale callbacks check this
|
||||||
|
|
||||||
export function playQueue(songs, startIdx) {
|
export function playQueue(songs, startIdx) {
|
||||||
S.queue = songs.map(s => ({...s}));
|
S.queue = songs.map(s => ({...s}));
|
||||||
S.qIdx = startIdx;
|
S.qIdx = startIdx;
|
||||||
@@ -70,6 +72,7 @@ export function playQueue(songs, startIdx) {
|
|||||||
export function playCurrent() {
|
export function playCurrent() {
|
||||||
if(S.qIdx < 0 || S.qIdx >= S.queue.length) return;
|
if(S.qIdx < 0 || S.qIdx >= S.queue.length) return;
|
||||||
const s = S.queue[S.qIdx];
|
const s = S.queue[S.qIdx];
|
||||||
|
const gen = ++_playGen; // capture generation — any callback that sees gen !== _playGen is stale
|
||||||
S._trackChanging = true;
|
S._trackChanging = true;
|
||||||
// Keep Samsung notification alive — set playing BEFORE metadata swap
|
// Keep Samsung notification alive — set playing BEFORE metadata swap
|
||||||
if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing';
|
if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing';
|
||||||
@@ -80,16 +83,19 @@ export function playCurrent() {
|
|||||||
_clearPreload();
|
_clearPreload();
|
||||||
S.audio.src = src;
|
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).
|
// Wait for canplay (audio engine — not throttled by Android background timers).
|
||||||
// _trackChanging stays TRUE throughout so spurious pause events from Android revoking
|
// _trackChanging stays TRUE so Android audio-focus-loss pause events can't reach
|
||||||
// audio focus on locked screen can't slip through and dismiss the Samsung cover screen.
|
// Samsung and dismiss the lock screen notification during recovery.
|
||||||
function _waitCanPlay() {
|
function _waitCanPlay() {
|
||||||
const guard = setTimeout(_reset, 10000); // 10s Kima-style failsafe
|
const guard = setTimeout(_reset, 10000); // 10s Kima-style failsafe
|
||||||
S.audio.addEventListener('canplay', () => {
|
S.audio.addEventListener('canplay', () => {
|
||||||
clearTimeout(guard);
|
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 {}
|
try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {}
|
||||||
S.audio.play().catch(_reset);
|
S.audio.play().catch(_reset);
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
@@ -101,16 +107,13 @@ export function playCurrent() {
|
|||||||
.then(() => clearTimeout(_initGuard))
|
.then(() => clearTimeout(_initGuard))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
clearTimeout(_initGuard);
|
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') {
|
if (err?.name === 'AbortError') {
|
||||||
// Audio element in bad state (OS interruption / load race) — reload so the
|
// Audio element in bad state — reload so it settles; _trackChanging stays true
|
||||||
// element settles; _trackChanging stays true so load()'s pause is suppressed
|
// so load()'s pause event is still suppressed
|
||||||
S.audio.src = src;
|
S.audio.src = src;
|
||||||
S.audio.load();
|
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();
|
_waitCanPlay();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user