diff --git a/src/main.js b/src/main.js index 5204e0e..44ca135 100644 --- a/src/main.js +++ b/src/main.js @@ -127,6 +127,8 @@ window.addEventListener('load', () => { S.audio.addEventListener('ended', onEnded); S.audio.addEventListener('play', () => { 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(); setPlayIcon(true); 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. // 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', () => { - if(S.queue[S.qIdx]) { - const t = setTimeout(() => { S.audio.load(); S.audio.play().catch(()=>{}); }, 2000); - S.audio.addEventListener('play', () => clearTimeout(t), { once: true }); - } + if(!S.queue[S.qIdx]) return; + _errRetries++; + 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; - // Re-register MediaSession handlers when returning from background/screen-off/flip - document.addEventListener('visibilitychange', () => { - if(document.visibilityState !== 'visible') return; + function _onForeground() { const s = S.queue[S.qIdx]; if(!s) return; updateMediaSession(s); if(navigator.mediaSession) 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) let _wdLast = 0, _wdStalledAt = 0; diff --git a/src/player.js b/src/player.js index 73f7d1b..d483603 100644 --- a/src/player.js +++ b/src/player.js @@ -5,10 +5,10 @@ import { api, streamUrl, coverUrl, downloadUrl } from './api.js'; import { loadRecommendations } from './views.js'; import { updateLyricsHighlight, onNpSongChange } from './nowplaying.js'; -// ─── PRELOAD (keeps network alive through Doze mode gap between tracks) ─── -let _preAudio = null; -let _preUrl = ''; -let _preId = ''; +// ─── PRELOAD (resource hint keeps connection open through Doze gap) ─── +let _preUrl = ''; +let _preId = ''; +let _preLink = null; function _nextIdx() { if(S.repeat === 'one') return S.qIdx; @@ -25,14 +25,17 @@ function _preloadNext() { if(!next || _preId === next.id) return; _preId = next.id; _preUrl = streamUrl(next.id); // generate URL once — reused by main element - if(!_preAudio) { _preAudio = new Audio(); _preAudio.preload = 'auto'; } - _preAudio.volume = 0; - _preAudio.src = _preUrl; // Chrome starts buffering immediately + if(_preLink) _preLink.remove(); + _preLink = document.createElement('link'); + _preLink.rel = 'prefetch'; + _preLink.as = 'fetch'; + _preLink.href = _preUrl; + document.head.appendChild(_preLink); } function _clearPreload() { _preId = ''; _preUrl = ''; - if(_preAudio) { _preAudio.src = ''; } + if(_preLink) { _preLink.remove(); _preLink = null; } } // ─── PLAYBACK ───