global player pt. 8 preload stuff

This commit is contained in:
2026-02-06 02:09:37 -08:00
parent 5efe06e640
commit cedec104a1

View File

@@ -26,6 +26,12 @@
let audioEl: HTMLAudioElement | null = null; let audioEl: HTMLAudioElement | null = null;
// Best-effort preload of the upcoming track's URL
let nextPreloadHref = $state<string | null>(null);
// Dedicated preloader element to warm the connection / decode pipeline a bit
let preloadEl = $state<HTMLAudioElement | null>(null);
let isPlaying = $state(false); let isPlaying = $state(false);
let currentTime = $state(0); let currentTime = $state(0);
let duration = $state(0); let duration = $state(0);
@@ -95,6 +101,41 @@
audioEl.volume = snap.volume; audioEl.volume = snap.volume;
} }
function computeNextTrackToPreload(): Track | null {
// Prefer to preload the "linear next" item. For shuffle mode, we can still
// best-effort preload the first item in the upcoming order.
if (snap.queue.length === 0) return null;
if (snap.shuffleEnabled) {
const nextIdx = snap.order[0];
if (typeof nextIdx === "number") return snap.queue[nextIdx] ?? null;
return null;
}
if (snap.currentIndex == null) return snap.queue[0] ?? null;
const nextIdx = snap.currentIndex + 1;
return nextIdx >= 0 && nextIdx < snap.queue.length
? snap.queue[nextIdx]
: null;
}
function updatePreloadTargets() {
const nextTrack = computeNextTrackToPreload();
nextPreloadHref = nextTrack?.src ?? null;
// Also warm via an offscreen <audio> element. This may or may not help depending
// on browser caching policies, but it's safe and often reduces first-buffer delay.
if (!preloadEl) return;
if (nextTrack?.src) {
preloadEl.src = nextTrack.src;
preloadEl.load();
} else {
preloadEl.removeAttribute("src");
preloadEl.load();
}
}
async function waitForEvent(el: HTMLMediaElement, eventName: string) { async function waitForEvent(el: HTMLMediaElement, eventName: string) {
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
const onEvent = () => { const onEvent = () => {
@@ -114,35 +155,25 @@
syncAudioToCurrentTrack(); syncAudioToCurrentTrack();
// If the src changed, some browsers require a "load -> playing" sequence.
// Strategy:
// - if src changed: ensure load is kicked, then wait for loadedmetadata/canplay
// - call play()
// - if it's still paused, wait for 'playing' and retry play once
const afterSrc = el.currentSrc; const afterSrc = el.currentSrc;
if (afterSrc !== beforeSrc) { if (afterSrc !== beforeSrc) {
// Ensure metadata exists (duration available, etc.)
if (el.readyState < 1) { if (el.readyState < 1) {
await waitForEvent(el, "loadedmetadata"); await waitForEvent(el, "loadedmetadata");
} }
// Ensure enough data to play.
if (el.readyState < 3) { if (el.readyState < 3) {
await waitForEvent(el, "canplay"); await waitForEvent(el, "canplay");
} }
} else { } else {
// Even if src didn't change, allow a minimal yield to let state settle.
await Promise.resolve(); await Promise.resolve();
} }
try { try {
await el.play(); await el.play();
} catch { } catch {
// Autoplay may be blocked; bail out quietly.
return; return;
} }
// If the browser didn't actually start playback, retry after 'canplay'/'playing'.
if (el.paused) { if (el.paused) {
if (el.readyState < 3) { if (el.readyState < 3) {
await waitForEvent(el, "canplay"); await waitForEvent(el, "canplay");
@@ -218,11 +249,11 @@
$effect(() => { $effect(() => {
media.setTrack(snap.currentTrack); media.setTrack(snap.currentTrack);
// Persist queue/settings/UI state (throttled) from within a component-scoped effect
// to avoid orphaned module-level `$effect`.
schedulePersistNow(); schedulePersistNow();
// Keep preload targets updated as the queue/traversal state changes
updatePreloadTargets();
if (!audioEl) return; if (!audioEl) return;
audioEl.volume = snap.volume; audioEl.volume = snap.volume;
@@ -235,6 +266,13 @@
window.addEventListener("resize", updateIsMobile); window.addEventListener("resize", updateIsMobile);
} }
// Create the offscreen preloader audio element in the browser
if (browser) {
preloadEl = new Audio();
preloadEl.preload = "auto";
preloadEl.muted = true;
}
// Subscribe to player changes instead of polling // Subscribe to player changes instead of polling
unsubscribe = subscribe((s) => { unsubscribe = subscribe((s) => {
snap = s; snap = s;
@@ -252,6 +290,13 @@
}); });
</script> </script>
<svelte:head>
<!-- Preload the next track (best-effort). We set this href reactively below. -->
{#if nextPreloadHref}
<link rel="preload" as="audio" href={nextPreloadHref} />
{/if}
</svelte:head>
{#if isMobile} {#if isMobile}
<!-- Mobile: mini bar + expandable drawer --> <!-- Mobile: mini bar + expandable drawer -->
<div <div