diff --git a/index.html b/index.html index c2f8f75..b7d32d0 100644 --- a/index.html +++ b/index.html @@ -590,7 +590,7 @@
- + diff --git a/src/main.js b/src/main.js index 28c8180..fd1212a 100644 --- a/src/main.js +++ b/src/main.js @@ -148,13 +148,15 @@ window.addEventListener('load', () => { let _errRetries = 0, _errTimer = null; S.audio.addEventListener('play', () => { _errRetries = 0; clearTimeout(_errTimer); }, { capture: true }); S.audio.addEventListener('error', () => { - if(!S.queue[S.qIdx]) return; + // Skip if playCurrent() is already handling recovery (_trackChanging = true means + // we're mid-transition or waiting for canplay — don't conflict with that) + if(!S.queue[S.qIdx] || S._trackChanging) return; _errRetries++; const delay = Math.min(1000 * Math.pow(2, _errRetries - 1), 8000); // 1s, 2s, 4s, 8s clearTimeout(_errTimer); _errTimer = setTimeout(() => { - if(!S.queue[S.qIdx]) return; - S._trackChanging = true; // prevent load()'s pause event from dismissing Samsung notification + if(!S.queue[S.qIdx] || S._trackChanging) return; + S._trackChanging = true; S.audio.load(); S.audio.play().catch(() => { S._trackChanging = false; }); }, delay); @@ -171,7 +173,7 @@ window.addEventListener('load', () => { navigator.mediaSession.playbackState = S.audio.paused ? 'paused' : 'playing'; // If audio was playing before backgrounding but is now paused (OS kill), try to resume if(!S.audio.paused) return; - if(s && S.audio.src) S.audio.play().catch(() => {}); + if(s && S.audio.src) S.audio.play().catch(() => { S._trackChanging = false; }); } document.addEventListener('visibilitychange', () => { diff --git a/src/player.js b/src/player.js index 55a33fb..75c0caa 100644 --- a/src/player.js +++ b/src/player.js @@ -71,45 +71,41 @@ export function playCurrent() { if(S.qIdx < 0 || S.qIdx >= S.queue.length) return; const s = S.queue[S.qIdx]; S._trackChanging = true; - // Explicitly 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'; - updatePlayerUI(s); // update metadata/notification BEFORE src change + updatePlayerUI(s); onNpSongChange(s.id); renderQueue(); - // Reuse preloaded URL so Chrome can serve from its in-memory buffer const src = (_preId === s.id && _preUrl) ? _preUrl : streamUrl(s.id); _clearPreload(); S.audio.src = src; - // canplay is fired by the audio element's internal state machine — not throttled like - // setTimeout in background. Use it as a reliable retry trigger when play() is rejected. - S.audio.addEventListener('canplay', () => { - if (S.queue[S.qIdx]?.id === s.id && S.audio.paused) { - S._trackChanging = true; - S.audio.play().catch(() => { S._trackChanging = false; }); - } - }, { once: true }); - const _tryPlay = (attempt) => { - try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {} - S.audio.play().catch(err => { - S._trackChanging = false; - if (S.queue[S.qIdx]?.id !== s.id) return; // song changed while we waited — stop + // 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); + try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {} + S.audio.play() + .then(() => clearTimeout(_guard)) + .catch(err => { + clearTimeout(_guard); + if (S.queue[S.qIdx]?.id !== s.id) { S._trackChanging = false; return; } if (err?.name === 'AbortError') { - // Audio element in bad state (interruption or load race) — reload like Kima Hub does - S._trackChanging = true; + // 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 S.audio.src = src; - S.audio.load(); // canplay listener above will call play() once buffered - return; - } - if (attempt < 4) { - setTimeout(() => { - if (S.queue[S.qIdx]?.id !== s.id) return; - S._trackChanging = true; - _tryPlay(attempt + 1); - }, Math.min(500 * Math.pow(2, attempt), 8000)); // 500ms, 1s, 2s, 4s, 8s + 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; } + // 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 }); }); - }; - _tryPlay(0); } export function updatePlayerUI(s) {