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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-08 19:57:52 +02:00
1 parent f04d87254b
commit 128437821a
1 file changed
+11 -11
+11 -11
View File
@@ -273,10 +273,10 @@ function _setAudioSession() {
try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {} try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {}
} }
// ─── PRELOAD (resource hint keeps connection open through Doze gap) ─────── // ─── PRELOAD — shadow Audio element actually buffers the next track ──────────
let _preUrl = ''; let _preUrl = '';
let _preId = ''; let _preId = '';
let _preLink = null; let _preAudio = null;
function _nextIdx() { function _nextIdx() {
if(S.repeat === 'one') return S.qIdx; if(S.repeat === 'one') return S.qIdx;
@@ -286,19 +286,18 @@ function _nextIdx() {
} }
function _preloadNext() { function _preloadNext() {
if (HAS_BRIDGE) return; // ExoPlayer handles buffering natively if (HAS_BRIDGE) return;
const ni = _nextIdx(); const ni = _nextIdx();
if(ni < 0) return; if(ni < 0) return;
const next = S.queue[ni]; const next = S.queue[ni];
if(!next || _preId === next.id) return; if(!next || _preId === next.id) return;
_preId = next.id; _preId = next.id;
_preUrl = streamUrl(next.id); _preUrl = streamUrl(next.id);
if(_preLink) _preLink.remove(); // Real Audio preload — actually buffers the stream so canplay fires instantly
_preLink = document.createElement('link'); if(_preAudio) { _preAudio.src = ''; _preAudio = null; }
_preLink.rel = 'prefetch'; _preAudio = new Audio();
_preLink.as = 'fetch'; _preAudio.preload = 'auto';
_preLink.href = _preUrl; _preAudio.src = _preUrl;
document.head.appendChild(_preLink);
if(next.coverArt) { if(next.coverArt) {
[256, 512, 1024].forEach(size => { [256, 512, 1024].forEach(size => {
const img = new Image(); const img = new Image();
@@ -309,7 +308,7 @@ function _preloadNext() {
function _clearPreload() { function _clearPreload() {
_preId = ''; _preUrl = ''; _preId = ''; _preUrl = '';
if(_preLink) { _preLink.remove(); _preLink = null; } if(_preAudio) { _preAudio.src = ''; _preAudio = null; }
} }
// ─── PLAY HISTORY (for prev-in-shuffle) ────────────────────────────────────── // ─── PLAY HISTORY (for prev-in-shuffle) ──────────────────────────────────────
@@ -388,6 +387,7 @@ export function playCurrent() {
_resetProgressUI(); _resetProgressUI();
if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing'; if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing';
updatePlayerUI(s, true); updatePlayerUI(s, true);
updateMediaSession(s);
onNpSongChange(s.id); onNpSongChange(s.id);
renderQueue(); renderQueue();
_scheduleScrobble(s.id, s.duration); _scheduleScrobble(s.id, s.duration);
@@ -830,7 +830,7 @@ export function updateProgress() {
if(!S.audio.duration || S._scrubbing) return; if(!S.audio.duration || S._scrubbing) return;
syncMediaSessionPosition(); syncMediaSessionPosition();
const rem = S.audio.duration - S.audio.currentTime; 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; const p = (S.audio.currentTime / S.audio.duration) * 100;
document.getElementById('progress-fill').style.width = p + '%'; document.getElementById('progress-fill').style.width = p + '%';
document.getElementById('time-cur').textContent = formatDur(S.audio.currentTime); document.getElementById('time-cur').textContent = formatDur(S.audio.currentTime);