diff --git a/src/main.js b/src/main.js index 81a1efd..28c8180 100644 --- a/src/main.js +++ b/src/main.js @@ -166,7 +166,8 @@ window.addEventListener('load', () => { const s = S.queue[S.qIdx]; if(!s) return; updateMediaSession(s); - if(navigator.mediaSession) + // Don't stomp playbackState during a src-swap — _trackChanging guards the transition + if(navigator.mediaSession && !S._trackChanging) navigator.mediaSession.playbackState = S.audio.paused ? 'paused' : 'playing'; // If audio was playing before backgrounding but is now paused (OS kill), try to resume if(!S.audio.paused) return; diff --git a/src/player.js b/src/player.js index d7aef7d..fefa697 100644 --- a/src/player.js +++ b/src/player.js @@ -61,7 +61,16 @@ export function playCurrent() { const src = (_preId === s.id && _preUrl) ? _preUrl : streamUrl(s.id); _clearPreload(); S.audio.src = src; + // canplay is fired by the audio element's internal state machine — not throttled like + // setTimeout in background. Use it as a reliable retry trigger when play() is rejected. + S.audio.addEventListener('canplay', () => { + if (S.queue[S.qIdx]?.id === s.id && S.audio.paused) { + S._trackChanging = true; + S.audio.play().catch(() => { S._trackChanging = false; }); + } + }, { once: true }); const _tryPlay = (attempt) => { + 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 @@ -345,6 +354,16 @@ 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) { @@ -360,9 +379,9 @@ export function updateMediaSession(s) { if(!navigator.mediaSession) return; const artwork = s.coverArt ? [ - { src: coverUrl(s.coverArt,96), sizes:'96x96', type:'image/jpeg' }, - { src: coverUrl(s.coverArt,300), sizes:'300x300', type:'image/jpeg' }, - { src: coverUrl(s.coverArt,512), sizes:'512x512', type:'image/jpeg' }, + { src: _cachedCoverUrl(s.coverArt, 96), sizes:'96x96', type:'image/jpeg' }, + { src: _cachedCoverUrl(s.coverArt, 300), sizes:'300x300', type:'image/jpeg' }, + { src: _cachedCoverUrl(s.coverArt, 512), sizes:'512x512', type:'image/jpeg' }, ] : []; navigator.mediaSession.metadata = new MediaMetadata({ title:s.title, artist:s.artist||'', album:s.album||'', artwork }); navigator.mediaSession.setActionHandler('play', togglePlay);