fix: clean rewrite of audio recovery matching Kima Hub design
playCurrent(): - Remove canplay listener from before play() — it caused a race where both the listener and the initial play() fired simultaneously - canplay is now only registered in the catch (failure path only), eliminating the double-play race entirely - AbortError: reload src+load() with _trackChanging=true (OS interruption recovery); canplay fires once buffered and retries play() - Other errors: reset _trackChanging, let canplay retry (not throttled by Android background timers unlike setTimeout) - Add 10s safety net to reset _trackChanging if Doze kills everything (matches Kima Hub's reload failsafe) main.js error handler: - Skip when _trackChanging=true (playCurrent already recovering) to prevent conflicting dual recovery attempts - _onForeground: reset _trackChanging on play() failure so the flag can't get stuck when foreground recovery also fails index.html: - Add crossorigin=anonymous: Navidrome and NovaSonic backend both send Access-Control-Allow-Origin:* so this is safe, and it enables proper cross-context HTTP caching for cover art and audio responses Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0344dcdcb6
commit
688348545d
3 files changed
+30
-32
No files matched your search
+1
-1
@@ -590,7 +590,7 @@
|
|||||||
<div class="jobs-panel" id="jobs-panel"></div>
|
<div class="jobs-panel" id="jobs-panel"></div>
|
||||||
|
|
||||||
<div class="toast-container" id="toast-container"></div>
|
<div class="toast-container" id="toast-container"></div>
|
||||||
<audio id="audio-element" preload="auto" playsinline></audio>
|
<audio id="audio-element" preload="auto" playsinline crossorigin="anonymous"></audio>
|
||||||
<audio id="preview-audio"></audio>
|
<audio id="preview-audio"></audio>
|
||||||
<audio id="abs-audio"></audio>
|
<audio id="abs-audio"></audio>
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -148,13 +148,15 @@ window.addEventListener('load', () => {
|
|||||||
let _errRetries = 0, _errTimer = null;
|
let _errRetries = 0, _errTimer = null;
|
||||||
S.audio.addEventListener('play', () => { _errRetries = 0; clearTimeout(_errTimer); }, { capture: true });
|
S.audio.addEventListener('play', () => { _errRetries = 0; clearTimeout(_errTimer); }, { capture: true });
|
||||||
S.audio.addEventListener('error', () => {
|
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++;
|
_errRetries++;
|
||||||
const delay = Math.min(1000 * Math.pow(2, _errRetries - 1), 8000); // 1s, 2s, 4s, 8s
|
const delay = Math.min(1000 * Math.pow(2, _errRetries - 1), 8000); // 1s, 2s, 4s, 8s
|
||||||
clearTimeout(_errTimer);
|
clearTimeout(_errTimer);
|
||||||
_errTimer = setTimeout(() => {
|
_errTimer = setTimeout(() => {
|
||||||
if(!S.queue[S.qIdx]) return;
|
if(!S.queue[S.qIdx] || S._trackChanging) return;
|
||||||
S._trackChanging = true; // prevent load()'s pause event from dismissing Samsung notification
|
S._trackChanging = true;
|
||||||
S.audio.load();
|
S.audio.load();
|
||||||
S.audio.play().catch(() => { S._trackChanging = false; });
|
S.audio.play().catch(() => { S._trackChanging = false; });
|
||||||
}, delay);
|
}, delay);
|
||||||
@@ -171,7 +173,7 @@ window.addEventListener('load', () => {
|
|||||||
navigator.mediaSession.playbackState = S.audio.paused ? 'paused' : 'playing';
|
navigator.mediaSession.playbackState = S.audio.paused ? 'paused' : 'playing';
|
||||||
// If audio was playing before backgrounding but is now paused (OS kill), try to resume
|
// If audio was playing before backgrounding but is now paused (OS kill), try to resume
|
||||||
if(!S.audio.paused) return;
|
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', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
|||||||
+23
-27
@@ -71,45 +71,41 @@ 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];
|
||||||
S._trackChanging = true;
|
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';
|
if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing';
|
||||||
updatePlayerUI(s); // update metadata/notification BEFORE src change
|
updatePlayerUI(s);
|
||||||
onNpSongChange(s.id);
|
onNpSongChange(s.id);
|
||||||
renderQueue();
|
renderQueue();
|
||||||
// Reuse preloaded URL so Chrome can serve from its in-memory buffer
|
|
||||||
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;
|
||||||
// canplay is fired by the audio element's internal state machine — not throttled like
|
// 10s safety net: if neither play-event nor catch fires (e.g. Doze killed everything),
|
||||||
// setTimeout in background. Use it as a reliable retry trigger when play() is rejected.
|
// reset _trackChanging so the pause-event guard doesn't stay stuck forever
|
||||||
S.audio.addEventListener('canplay', () => {
|
const _guard = setTimeout(() => { S._trackChanging = false; }, 10000);
|
||||||
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 {}
|
try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {}
|
||||||
S.audio.play().catch(err => {
|
S.audio.play()
|
||||||
S._trackChanging = false;
|
.then(() => clearTimeout(_guard))
|
||||||
if (S.queue[S.qIdx]?.id !== s.id) return; // song changed while we waited — stop
|
.catch(err => {
|
||||||
|
clearTimeout(_guard);
|
||||||
|
if (S.queue[S.qIdx]?.id !== s.id) { S._trackChanging = false; return; }
|
||||||
if (err?.name === 'AbortError') {
|
if (err?.name === 'AbortError') {
|
||||||
// Audio element in bad state (interruption or load race) — reload like Kima Hub does
|
// Audio element in bad state (OS interruption / load race) — Kima Hub pattern:
|
||||||
S._trackChanging = true;
|
// reload the src so the element settles, then let canplay trigger the retry
|
||||||
S.audio.src = src;
|
S.audio.src = src;
|
||||||
S.audio.load(); // canplay listener above will call play() once buffered
|
S.audio.load(); // _trackChanging stays true; load()'s pause event is suppressed
|
||||||
return;
|
} else {
|
||||||
|
// NotAllowed / network error — reset flag, wait for canplay to retry
|
||||||
|
S._trackChanging = false;
|
||||||
}
|
}
|
||||||
if (attempt < 4) {
|
// canplay fires from the audio engine (not throttled by Android background timers).
|
||||||
setTimeout(() => {
|
// Registered ONLY here (in the failure path) — no race with the initial play() call.
|
||||||
if (S.queue[S.qIdx]?.id !== s.id) return;
|
S.audio.addEventListener('canplay', () => {
|
||||||
|
if (S.queue[S.qIdx]?.id !== s.id || !S.audio.paused) return;
|
||||||
S._trackChanging = true;
|
S._trackChanging = true;
|
||||||
_tryPlay(attempt + 1);
|
try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {}
|
||||||
}, Math.min(500 * Math.pow(2, attempt), 8000)); // 500ms, 1s, 2s, 4s, 8s
|
S.audio.play().catch(() => { S._trackChanging = false; });
|
||||||
}
|
}, { once: true });
|
||||||
});
|
});
|
||||||
};
|
|
||||||
_tryPlay(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updatePlayerUI(s) {
|
export function updatePlayerUI(s) {
|
||||||
|
|||||||
Reference in new issue
Block a user