fix: preload=auto, artwork pre-warm, AbortError reload (matching Kima Hub)

- Add preload="auto" playsinline to <audio> element: browser starts
  buffering as soon as src is set, not only when play() is called —
  critical for background where play() has no data to work with
- Fix AbortError handling: instead of silently returning (was swallowing
  audio element bad-state errors), reload src + load() like Kima Hub
  so the already-registered canplay listener retries play() once buffered
- Pre-warm next song's cover art in _preloadNext() using _cachedCoverUrl
  so the lock screen notification never cold-starts artwork on first skip
- Move _coverCache to top of file so _preloadNext and updateMediaSession
  share the same stable token URLs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-03 23:09:46 +02:00
1 parent a0c6d09c41
commit 0344dcdcb6
2 files changed
+29 -13

No files matched your search

+28 -12
View File
@@ -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) {