From 502427d02c6852025ad92659db17361174f2fd5b Mon Sep 17 00:00:00 2001 From: Honey Date: Sun, 3 May 2026 20:40:25 +0200 Subject: [PATCH] fix: preload next track 30s early to survive Android Doze network gap When the current song ends, Android's battery optimization briefly kills background network access. By starting the next song's stream request while the current track is still playing (and Chrome holds an active audio session), the buffer is ready before the gap hits. The same URL is reused by the main element so Chrome can serve from its in-memory buffer. Also reset _trackChanging on play() failure to avoid permanently suppressing pause events. Co-Authored-By: Claude Sonnet 4.6 --- src/player.js | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/player.js b/src/player.js index 4121130..73f7d1b 100644 --- a/src/player.js +++ b/src/player.js @@ -5,10 +5,41 @@ 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 = ''; + +function _nextIdx() { + if(S.repeat === 'one') return S.qIdx; + if(S.shuffle) return -1; + if(S.qIdx + 1 < S.queue.length) return S.qIdx + 1; + if(S.repeat === 'all') return 0; + return -1; +} + +function _preloadNext() { + const ni = _nextIdx(); + if(ni < 0) return; + const next = S.queue[ni]; + 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 +} + +function _clearPreload() { + _preId = ''; _preUrl = ''; + if(_preAudio) { _preAudio.src = ''; } +} + // ─── PLAYBACK ─── export function playQueue(songs, startIdx) { S.queue = songs.map(s => ({...s})); S.qIdx = startIdx; + _clearPreload(); playCurrent(); renderQueue(); if(S.radioMode) fillRadioQueue(); @@ -21,8 +52,11 @@ export function playCurrent() { updatePlayerUI(s); // update metadata/notification BEFORE src change onNpSongChange(s.id); renderQueue(); - S.audio.src = streamUrl(s.id); - S.audio.play().catch(() => {}); + // 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; + S.audio.play().catch(() => { S._trackChanging = false; }); } export function updatePlayerUI(s) { @@ -128,11 +162,13 @@ export function setPlayIcon(playing) { } export function prevSong() { + _clearPreload(); if(S.audio.currentTime > 3) { S.audio.currentTime = 0; return; } if(S.qIdx > 0) { S.qIdx--; playCurrent(); } } export function nextSong() { + _clearPreload(); if(S.shuffle) S.qIdx = Math.floor(Math.random() * S.queue.length); else if(S.qIdx < S.queue.length - 1) S.qIdx++; else if(S.repeat) S.qIdx = 0; @@ -200,6 +236,9 @@ let _androidPosTick = 0; export function updateProgress() { if(!S.audio.duration || S._scrubbing) return; syncMediaSessionPosition(); + // Preload next track at 30s remaining so network stays alive through Doze gap + const rem = S.audio.duration - S.audio.currentTime; + if(rem > 0 && rem < 30 && !S._trackChanging) _preloadNext(); // Update Android native position every ~5 seconds to keep lock screen scrubber in sync const now = Date.now(); if(now - _androidPosTick > 5000) {