fix: keep _trackChanging=true through entire recovery window (lock screen)

When play() fails on locked screen, Android may revoke audio focus and
fire a spurious pause event. Previously we reset _trackChanging=false
right in the catch, leaving a window for that pause event to set
playbackState='paused' and dismiss the Samsung cover screen notification.

Now _trackChanging stays true for the ENTIRE recovery (initial fail ->
canplay retry -> success/fail), with a 10s Kima-style safety net as the
only exit. This means:
- AbortError: reload src+load(), _trackChanging stays true, canplay retries
- NotAllowed/network: same — _trackChanging stays true, canplay retries
- If canplay fires and play() succeeds: play event resets _trackChanging
- If canplay fires and play() fails: _reset() is called
- If canplay never fires (Doze): 10s timeout resets _trackChanging

Net effect: Samsung lock screen notification never sees playbackState='paused'
during the recovery window, so it stays open instead of dismissing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-04 00:24:11 +02:00
1 parent 688348545d
commit b941672a77
1 file changed
+27 -20
+27 -20
View File
@@ -79,32 +79,39 @@ export function playCurrent() {
const src = (_preId === s.id && _preUrl) ? _preUrl : streamUrl(s.id); const src = (_preId === s.id && _preUrl) ? _preUrl : streamUrl(s.id);
_clearPreload(); _clearPreload();
S.audio.src = src; S.audio.src = src;
// 10s safety net: if neither play-event nor catch fires (e.g. Doze killed everything),
// reset _trackChanging so the pause-event guard doesn't stay stuck forever const _reset = () => { S._trackChanging = false; };
const _guard = setTimeout(() => { S._trackChanging = false; }, 10000);
// 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.
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; }
try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {}
S.audio.play().catch(_reset);
}, { once: true });
}
const _initGuard = setTimeout(_reset, 10000);
try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {} try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {}
S.audio.play() S.audio.play()
.then(() => clearTimeout(_guard)) .then(() => clearTimeout(_initGuard))
.catch(err => { .catch(err => {
clearTimeout(_guard); clearTimeout(_initGuard);
if (S.queue[S.qIdx]?.id !== s.id) { S._trackChanging = false; return; } if (S.queue[S.qIdx]?.id !== s.id) { _reset(); return; }
if (err?.name === 'AbortError') { if (err?.name === 'AbortError') {
// Audio element in bad state (OS interruption / load race) — Kima Hub pattern: // Audio element in bad state (OS interruption / load race) — reload so the
// reload the src so the element settles, then let canplay trigger the retry // element settles; _trackChanging stays true so load()'s pause is suppressed
S.audio.src = src; S.audio.src = src;
S.audio.load(); // _trackChanging stays true; load()'s pause event is suppressed S.audio.load();
} else {
// NotAllowed / network error — reset flag, wait for canplay to retry
S._trackChanging = false;
} }
// canplay fires from the audio engine (not throttled by Android background timers). // For ALL errors: keep _trackChanging=true until canplay fires (or 10s timeout).
// Registered ONLY here (in the failure path) — no race with the initial play() call. // This prevents Android audio-focus-loss pause events from reaching Samsung
S.audio.addEventListener('canplay', () => { // and dismissing the lock screen notification during the recovery window.
if (S.queue[S.qIdx]?.id !== s.id || !S.audio.paused) return; _waitCanPlay();
S._trackChanging = true;
try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {}
S.audio.play().catch(() => { S._trackChanging = false; });
}, { once: true });
}); });
} }