From b941672a777eb70b1a9b8bd7961b5f97aa57efc1 Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 4 May 2026 00:24:11 +0200 Subject: [PATCH] fix: keep _trackChanging=true through entire recovery window (lock screen) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/player.js | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/player.js b/src/player.js index 75c0caa..beac6d4 100644 --- a/src/player.js +++ b/src/player.js @@ -79,32 +79,39 @@ export function playCurrent() { const src = (_preId === s.id && _preUrl) ? _preUrl : streamUrl(s.id); _clearPreload(); 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 _guard = setTimeout(() => { S._trackChanging = false; }, 10000); + + const _reset = () => { 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. + 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 {} S.audio.play() - .then(() => clearTimeout(_guard)) + .then(() => clearTimeout(_initGuard)) .catch(err => { - clearTimeout(_guard); - if (S.queue[S.qIdx]?.id !== s.id) { S._trackChanging = false; return; } + clearTimeout(_initGuard); + if (S.queue[S.qIdx]?.id !== s.id) { _reset(); return; } if (err?.name === 'AbortError') { - // Audio element in bad state (OS interruption / load race) — Kima Hub pattern: - // reload the src so the element settles, then let canplay trigger the retry + // Audio element in bad state (OS interruption / load race) — reload so the + // element settles; _trackChanging stays true so load()'s pause is suppressed S.audio.src = src; - S.audio.load(); // _trackChanging stays true; load()'s pause event is suppressed - } else { - // NotAllowed / network error — reset flag, wait for canplay to retry - S._trackChanging = false; + S.audio.load(); } - // canplay fires from the audio engine (not throttled by Android background timers). - // Registered ONLY here (in the failure path) — no race with the initial play() call. - S.audio.addEventListener('canplay', () => { - if (S.queue[S.qIdx]?.id !== s.id || !S.audio.paused) return; - S._trackChanging = true; - try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {} - S.audio.play().catch(() => { S._trackChanging = false; }); - }, { once: true }); + // 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(); }); }