diff --git a/index.html b/index.html index c837020..c2f8f75 100644 --- a/index.html +++ b/index.html @@ -590,7 +590,7 @@
- + diff --git a/src/player.js b/src/player.js index fefa697..55a33fb 100644 --- a/src/player.js +++ b/src/player.js @@ -5,6 +5,17 @@ import { api, streamUrl, coverUrl, downloadUrl } from './api.js'; import { loadRecommendations } from './views.js'; import { updateLyricsHighlight, onNpSongChange } from './nowplaying.js'; +// ─── COVER ART URL CACHE ─── +// Stable URLs let the OS cache artwork images — without this every skip regenerates a new +// auth token URL, Samsung has to re-fetch the image, and the lock screen notification +// visually reloads. Defined here (top of file) so both preload and MediaSession can use it. +const _coverCache = new Map(); +function _cachedCoverUrl(id, size) { + const key = `${id}:${size}`; + if (!_coverCache.has(key)) _coverCache.set(key, coverUrl(id, size)); + return _coverCache.get(key); +} + // ─── PRELOAD (resource hint keeps connection open through Doze gap) ─── let _preUrl = ''; let _preId = ''; @@ -31,6 +42,14 @@ function _preloadNext() { _preLink.as = 'fetch'; _preLink.href = _preUrl; document.head.appendChild(_preLink); + // Pre-warm cover art so lock screen notification never cold-starts artwork on first skip. + // Uses _cachedCoverUrl so the same token is reused when updateMediaSession() runs later. + if(next.coverArt) { + [96, 300, 512].forEach(size => { + const img = new Image(); + img.src = _cachedCoverUrl(next.coverArt, size); + }); + } } function _clearPreload() { @@ -73,8 +92,15 @@ export function playCurrent() { try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {} S.audio.play().catch(err => { S._trackChanging = false; - if (err?.name === 'AbortError') return; // src changed mid-flight, stop - if (attempt < 4 && S.queue[S.qIdx]?.id === s.id) { + if (S.queue[S.qIdx]?.id !== s.id) return; // song changed while we waited — stop + if (err?.name === 'AbortError') { + // Audio element in bad state (interruption or load race) — reload like Kima Hub does + S._trackChanging = true; + S.audio.src = src; + S.audio.load(); // canplay listener above will call play() once buffered + return; + } + if (attempt < 4) { setTimeout(() => { if (S.queue[S.qIdx]?.id !== s.id) return; S._trackChanging = true; @@ -354,16 +380,6 @@ export function setVolume(v) { } // ─── MEDIA SESSION ─── -// Cache cover art URLs — same auth token is reused so the OS can cache the artwork image. -// Without this, every metadata update regenerates a new token URL and Samsung re-fetches -// the artwork on each skip, causing the notification to visually "restart". -const _coverCache = new Map(); -function _cachedCoverUrl(id, size) { - const key = `${id}:${size}`; - if (!_coverCache.has(key)) _coverCache.set(key, coverUrl(id, size)); - return _coverCache.get(key); -} - export function updateMediaSession(s) { // Android native MediaSession bridge — use 512px art for high-DPI screens if (window.Android) {