global player pt. 8 preload stuff
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user