From 128437821adb387d180eb4849b738dadd6d55a5f Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 8 Jun 2026 19:57:52 +0200 Subject: [PATCH] fix: instant OS notification update + real audio preload for gapless playback - Call updateMediaSession immediately on track change so skip buttons and cover art in the OS notification bar update without waiting for audio load - Replace link[rel=prefetch] (no-op for audio streams) with a shadow Audio element (preload=auto) that actually buffers the next track - Start preloading 60s before track end instead of 30s for more buffer margin Co-Authored-By: Claude Sonnet 4.6 --- src/player/player.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/player/player.js b/src/player/player.js index 4b9c93c..2808d56 100644 --- a/src/player/player.js +++ b/src/player/player.js @@ -273,10 +273,10 @@ function _setAudioSession() { try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {} } -// ─── PRELOAD (resource hint keeps connection open through Doze gap) ─────── -let _preUrl = ''; -let _preId = ''; -let _preLink = null; +// ─── PRELOAD — shadow Audio element actually buffers the next track ────────── +let _preUrl = ''; +let _preId = ''; +let _preAudio = null; function _nextIdx() { if(S.repeat === 'one') return S.qIdx; @@ -286,19 +286,18 @@ function _nextIdx() { } function _preloadNext() { - if (HAS_BRIDGE) return; // ExoPlayer handles buffering natively + if (HAS_BRIDGE) return; 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); - if(_preLink) _preLink.remove(); - _preLink = document.createElement('link'); - _preLink.rel = 'prefetch'; - _preLink.as = 'fetch'; - _preLink.href = _preUrl; - document.head.appendChild(_preLink); + // Real Audio preload — actually buffers the stream so canplay fires instantly + if(_preAudio) { _preAudio.src = ''; _preAudio = null; } + _preAudio = new Audio(); + _preAudio.preload = 'auto'; + _preAudio.src = _preUrl; if(next.coverArt) { [256, 512, 1024].forEach(size => { const img = new Image(); @@ -309,7 +308,7 @@ function _preloadNext() { function _clearPreload() { _preId = ''; _preUrl = ''; - if(_preLink) { _preLink.remove(); _preLink = null; } + if(_preAudio) { _preAudio.src = ''; _preAudio = null; } } // ─── PLAY HISTORY (for prev-in-shuffle) ────────────────────────────────────── @@ -388,6 +387,7 @@ export function playCurrent() { _resetProgressUI(); if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing'; updatePlayerUI(s, true); + updateMediaSession(s); onNpSongChange(s.id); renderQueue(); _scheduleScrobble(s.id, s.duration); @@ -830,7 +830,7 @@ export function updateProgress() { if(!S.audio.duration || S._scrubbing) return; syncMediaSessionPosition(); const rem = S.audio.duration - S.audio.currentTime; - if(rem > 0 && rem < 30 && !S._trackChanging) _preloadNext(); + if(rem > 0 && rem < 60 && !S._trackChanging) _preloadNext(); const p = (S.audio.currentTime / S.audio.duration) * 100; document.getElementById('progress-fill').style.width = p + '%'; document.getElementById('time-cur').textContent = formatDur(S.audio.currentTime);