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 <noreply@anthropic.com>
This commit is contained in:
1 parent
89e169b70a
commit
502427d02c
1 file changed
+41
-2
+41
-2
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user