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:
1 parent
a0c6d09c41
commit
0344dcdcb6
2 files changed
+29
-13
No files matched your search
+1
-1
@@ -590,7 +590,7 @@
|
|||||||
<div class="jobs-panel" id="jobs-panel"></div>
|
<div class="jobs-panel" id="jobs-panel"></div>
|
||||||
|
|
||||||
<div class="toast-container" id="toast-container"></div>
|
<div class="toast-container" id="toast-container"></div>
|
||||||
<audio id="audio-element"></audio>
|
<audio id="audio-element" preload="auto" playsinline></audio>
|
||||||
<audio id="preview-audio"></audio>
|
<audio id="preview-audio"></audio>
|
||||||
<audio id="abs-audio"></audio>
|
<audio id="abs-audio"></audio>
|
||||||
|
|
||||||
|
|||||||
+28
-12
@@ -5,6 +5,17 @@ import { api, streamUrl, coverUrl, downloadUrl } from './api.js';
|
|||||||
import { loadRecommendations } from './views.js';
|
import { loadRecommendations } from './views.js';
|
||||||
import { updateLyricsHighlight, onNpSongChange } from './nowplaying.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) ───
|
// ─── PRELOAD (resource hint keeps connection open through Doze gap) ───
|
||||||
let _preUrl = '';
|
let _preUrl = '';
|
||||||
let _preId = '';
|
let _preId = '';
|
||||||
@@ -31,6 +42,14 @@ function _preloadNext() {
|
|||||||
_preLink.as = 'fetch';
|
_preLink.as = 'fetch';
|
||||||
_preLink.href = _preUrl;
|
_preLink.href = _preUrl;
|
||||||
document.head.appendChild(_preLink);
|
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() {
|
function _clearPreload() {
|
||||||
@@ -73,8 +92,15 @@ export function playCurrent() {
|
|||||||
try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {}
|
try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {}
|
||||||
S.audio.play().catch(err => {
|
S.audio.play().catch(err => {
|
||||||
S._trackChanging = false;
|
S._trackChanging = false;
|
||||||
if (err?.name === 'AbortError') return; // src changed mid-flight, stop
|
if (S.queue[S.qIdx]?.id !== s.id) return; // song changed while we waited — stop
|
||||||
if (attempt < 4 && S.queue[S.qIdx]?.id === s.id) {
|
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(() => {
|
setTimeout(() => {
|
||||||
if (S.queue[S.qIdx]?.id !== s.id) return;
|
if (S.queue[S.qIdx]?.id !== s.id) return;
|
||||||
S._trackChanging = true;
|
S._trackChanging = true;
|
||||||
@@ -354,16 +380,6 @@ export function setVolume(v) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── MEDIA SESSION ───
|
// ─── 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) {
|
export function updateMediaSession(s) {
|
||||||
// Android native MediaSession bridge — use 512px art for high-DPI screens
|
// Android native MediaSession bridge — use 512px art for high-DPI screens
|
||||||
if (window.Android) {
|
if (window.Android) {
|
||||||
|
|||||||
Reference in new issue
Block a user