fix: stable notification artwork + canplay retry for background audio
- Cache cover art URLs per song so the same auth token is reused across metadata updates; Samsung/Chrome can cache the artwork image and the notification no longer visually reloads on every skip - Add canplay listener as background-safe play() retry: the audio element fires canplay from its internal state machine (not throttled like setTimeout by Android Doze), so play() is retried as soon as the browser has buffered enough data - Set navigator.audioSession.type='playback' on each _tryPlay attempt (not just on play event) so the OS audio session claim happens before Chrome requests the play grant - Guard _onForeground playbackState assignment with !_trackChanging to prevent visibilitychange/pageshow from stomping 'playing' to 'paused' during an in-progress track transition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
3f5a5a4c43
commit
a0c6d09c41
2 files changed
+24
-4
No files matched your search
+2
-1
@@ -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;
|
||||
|
||||
+22
-3
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user