fix: audioSession playback type, exponential backoff, pageshow recovery
- Set navigator.audioSession.type = 'playback' on first play — tells Android OS this is a music app, keeping background network alive through Doze mode - Replace flat 2s error retry with exponential backoff (1s/2s/4s/8s) - Add pageshow event alongside visibilitychange for Samsung/iOS PWA reliability - Attempt audio resume on foreground if OS killed playback while backgrounded - Switch preload from hidden audio element to link rel=prefetch (lighter, avoids triggering two-audio-stream detection on Android) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
502427d02c
commit
33dc07ed5d
2 files changed
+28
-13
No files matched your search
+19
-7
@@ -127,6 +127,8 @@ window.addEventListener('load', () => {
|
|||||||
S.audio.addEventListener('ended', onEnded);
|
S.audio.addEventListener('ended', onEnded);
|
||||||
S.audio.addEventListener('play', () => {
|
S.audio.addEventListener('play', () => {
|
||||||
S._trackChanging = false;
|
S._trackChanging = false;
|
||||||
|
// Tell OS this is a music playback app — keeps background network alive (Doze mode)
|
||||||
|
try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {}
|
||||||
absInterrupt();
|
absInterrupt();
|
||||||
setPlayIcon(true);
|
setPlayIcon(true);
|
||||||
document.getElementById('player-art').classList.add('spinning');
|
document.getElementById('player-art').classList.add('spinning');
|
||||||
@@ -143,24 +145,34 @@ window.addEventListener('load', () => {
|
|||||||
|
|
||||||
// stalled = browser buffer ran out briefly; let the browser retry naturally.
|
// stalled = browser buffer ran out briefly; let the browser retry naturally.
|
||||||
// The watchdog below catches genuine long freezes.
|
// The watchdog below catches genuine long freezes.
|
||||||
|
let _errRetries = 0, _errTimer = null;
|
||||||
|
S.audio.addEventListener('play', () => { _errRetries = 0; clearTimeout(_errTimer); }, { capture: true });
|
||||||
S.audio.addEventListener('error', () => {
|
S.audio.addEventListener('error', () => {
|
||||||
if(S.queue[S.qIdx]) {
|
if(!S.queue[S.qIdx]) return;
|
||||||
const t = setTimeout(() => { S.audio.load(); S.audio.play().catch(()=>{}); }, 2000);
|
_errRetries++;
|
||||||
S.audio.addEventListener('play', () => clearTimeout(t), { once: true });
|
const delay = Math.min(1000 * Math.pow(2, _errRetries - 1), 8000); // 1s, 2s, 4s, 8s
|
||||||
}
|
clearTimeout(_errTimer);
|
||||||
|
_errTimer = setTimeout(() => { S.audio.load(); S.audio.play().catch(() => {}); }, delay);
|
||||||
});
|
});
|
||||||
|
|
||||||
S.audio.volume = 0.5;
|
S.audio.volume = 0.5;
|
||||||
|
|
||||||
// Re-register MediaSession handlers when returning from background/screen-off/flip
|
function _onForeground() {
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if(document.visibilityState !== 'visible') return;
|
|
||||||
const s = S.queue[S.qIdx];
|
const s = S.queue[S.qIdx];
|
||||||
if(!s) return;
|
if(!s) return;
|
||||||
updateMediaSession(s);
|
updateMediaSession(s);
|
||||||
if(navigator.mediaSession)
|
if(navigator.mediaSession)
|
||||||
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(!S.audio.paused) return;
|
||||||
|
if(s && S.audio.src) S.audio.play().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if(document.visibilityState === 'visible') _onForeground();
|
||||||
});
|
});
|
||||||
|
// pageshow fires more reliably than visibilitychange on iOS PWA and Samsung
|
||||||
|
window.addEventListener('pageshow', e => { if(e.persisted) _onForeground(); });
|
||||||
|
|
||||||
// Watchdog: detect when audio silently stalled in background (ended event not fired, or frozen)
|
// Watchdog: detect when audio silently stalled in background (ended event not fired, or frozen)
|
||||||
let _wdLast = 0, _wdStalledAt = 0;
|
let _wdLast = 0, _wdStalledAt = 0;
|
||||||
|
|||||||
+9
-6
@@ -5,10 +5,10 @@ import { api, streamUrl, coverUrl, downloadUrl } from './api.js';
|
|||||||
import { loadRecommendations } from './views.js';
|
import { loadRecommendations } from './views.js';
|
||||||
import { updateLyricsHighlight, onNpSongChange } from './nowplaying.js';
|
import { updateLyricsHighlight, onNpSongChange } from './nowplaying.js';
|
||||||
|
|
||||||
// ─── PRELOAD (keeps network alive through Doze mode gap between tracks) ───
|
// ─── PRELOAD (resource hint keeps connection open through Doze gap) ───
|
||||||
let _preAudio = null;
|
|
||||||
let _preUrl = '';
|
let _preUrl = '';
|
||||||
let _preId = '';
|
let _preId = '';
|
||||||
|
let _preLink = null;
|
||||||
|
|
||||||
function _nextIdx() {
|
function _nextIdx() {
|
||||||
if(S.repeat === 'one') return S.qIdx;
|
if(S.repeat === 'one') return S.qIdx;
|
||||||
@@ -25,14 +25,17 @@ function _preloadNext() {
|
|||||||
if(!next || _preId === next.id) return;
|
if(!next || _preId === next.id) return;
|
||||||
_preId = next.id;
|
_preId = next.id;
|
||||||
_preUrl = streamUrl(next.id); // generate URL once — reused by main element
|
_preUrl = streamUrl(next.id); // generate URL once — reused by main element
|
||||||
if(!_preAudio) { _preAudio = new Audio(); _preAudio.preload = 'auto'; }
|
if(_preLink) _preLink.remove();
|
||||||
_preAudio.volume = 0;
|
_preLink = document.createElement('link');
|
||||||
_preAudio.src = _preUrl; // Chrome starts buffering immediately
|
_preLink.rel = 'prefetch';
|
||||||
|
_preLink.as = 'fetch';
|
||||||
|
_preLink.href = _preUrl;
|
||||||
|
document.head.appendChild(_preLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _clearPreload() {
|
function _clearPreload() {
|
||||||
_preId = ''; _preUrl = '';
|
_preId = ''; _preUrl = '';
|
||||||
if(_preAudio) { _preAudio.src = ''; }
|
if(_preLink) { _preLink.remove(); _preLink = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── PLAYBACK ───
|
// ─── PLAYBACK ───
|
||||||
|
|||||||
Reference in new issue
Block a user