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:
HoneyandClaude Sonnet 4.6 committed 2026-05-03 23:34:28 +02:00
1 parent 0344dcdcb6
commit 688348545d
3 files changed
+32 -34

No files matched your search

+6 -4
View File
@@ -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', () => {