track switching fixed! (sorta!)
This commit is contained in:
@@ -60,12 +60,6 @@
|
||||
|
||||
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 currentTime = $state(0);
|
||||
let duration = $state(0);
|
||||
@@ -110,6 +104,66 @@
|
||||
prevIsMobile = nextIsMobile;
|
||||
}
|
||||
|
||||
async function waitForEvent(el: HTMLMediaElement, eventName: string) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const onEvent = () => {
|
||||
el.removeEventListener(eventName, onEvent);
|
||||
resolve();
|
||||
};
|
||||
el.addEventListener(eventName, onEvent, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
async function syncAndAutoplay() {
|
||||
const el = audioEl;
|
||||
if (!el) return;
|
||||
|
||||
// Centralized logic to sync and play the current track.
|
||||
const track = snap.currentTrack;
|
||||
if (!track) {
|
||||
if (el.src) {
|
||||
el.removeAttribute("src");
|
||||
el.load();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const desiredSrc = track.src;
|
||||
const desiredAbs = new URL(desiredSrc, window.location.href).href;
|
||||
const srcChanged = el.src !== desiredAbs;
|
||||
|
||||
if (srcChanged) {
|
||||
el.src = desiredSrc;
|
||||
el.load();
|
||||
}
|
||||
|
||||
el.volume = snap.volume;
|
||||
|
||||
// If src changed, we MUST wait until the browser says it's ready.
|
||||
if (srcChanged || el.readyState < 3) {
|
||||
try {
|
||||
await Promise.race([
|
||||
waitForEvent(el, "canplay"),
|
||||
// Add a timeout to prevent getting stuck forever
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("canplay timeout")), 8000),
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.error("Audio load failed or timed out", e);
|
||||
return; // Don't try to play if loading failed
|
||||
}
|
||||
}
|
||||
|
||||
// Now, try to play.
|
||||
try {
|
||||
await el.play();
|
||||
} catch (e) {
|
||||
console.warn("Autoplay was prevented.", e);
|
||||
// isPlaying state will be updated by onAudioPause handler.
|
||||
}
|
||||
}
|
||||
|
||||
// Media Session bindings
|
||||
const media = createMediaSessionBindings({
|
||||
play: () => void audioEl?.play(),
|
||||
@@ -135,83 +189,6 @@
|
||||
// Scrubber is now driven by `bind:currentTime` on the <audio> element and
|
||||
// `bind:value={currentTime}` on the sliders, so we don't need explicit handlers.
|
||||
|
||||
let isSrcChanging = $state(false);
|
||||
|
||||
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) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const onEvent = () => {
|
||||
el.removeEventListener(eventName, onEvent);
|
||||
resolve();
|
||||
};
|
||||
el.addEventListener(eventName, onEvent, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
async function syncAndAutoplay() {
|
||||
const el = audioEl;
|
||||
if (!el) return;
|
||||
|
||||
// If src is changing (set by $effect) or readyState is too low, wait until ready.
|
||||
// This is the key fix: it respects that the $effect is asynchronously loading media.
|
||||
if (isSrcChanging || el.readyState < 3) {
|
||||
await waitForEvent(el, "canplay");
|
||||
}
|
||||
|
||||
try {
|
||||
await el.play();
|
||||
} catch {
|
||||
// Autoplay may be blocked; bail out quietly.
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.paused) {
|
||||
if (el.readyState < 3) {
|
||||
await waitForEvent(el, "canplay");
|
||||
}
|
||||
await Promise.race([waitForEvent(el, "playing"), Promise.resolve()]);
|
||||
try {
|
||||
await el.play();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onAudioPlay() {
|
||||
isPlaying = true;
|
||||
media.setPlaybackState("playing");
|
||||
@@ -232,7 +209,6 @@
|
||||
});
|
||||
}
|
||||
function onAudioLoadedMetadata() {
|
||||
isSrcChanging = false;
|
||||
// `duration` is synced via `bind:duration` on <audio>.
|
||||
if (!audioEl) return;
|
||||
|
||||
@@ -520,32 +496,16 @@
|
||||
media.setTrack(snap.currentTrack);
|
||||
schedulePersistNow();
|
||||
|
||||
// Keep preload targets updated as the queue/traversal state changes
|
||||
updatePreloadTargets();
|
||||
|
||||
if (!audioEl) return;
|
||||
|
||||
// When track changes for any reason, ensure volume is correct.
|
||||
if (audioEl) {
|
||||
audioEl.volume = snap.volume;
|
||||
}
|
||||
|
||||
const track = snap.currentTrack;
|
||||
if (!track) {
|
||||
if (audioEl.src) {
|
||||
// If there's no track, ensure audio element is reset
|
||||
if (!snap.currentTrack && audioEl && audioEl.src) {
|
||||
audioEl.removeAttribute("src");
|
||||
audioEl.load();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const desired = track.src;
|
||||
const desiredAbs = new URL(desired, window.location.href).href;
|
||||
|
||||
// Use .src for a synchronous check. The .src property returns the resolved absolute URL.
|
||||
if (audioEl.src !== desiredAbs) {
|
||||
isSrcChanging = true;
|
||||
audioEl.src = desired;
|
||||
audioEl.load();
|
||||
audioEl.currentTime = 0;
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
@@ -554,13 +514,6 @@
|
||||
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
|
||||
unsubscribe = subscribe((s) => {
|
||||
snap = s;
|
||||
@@ -596,10 +549,6 @@
|
||||
</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>
|
||||
|
||||
<!-- Mobile UI is portaled to document.body so the GlobalPlayer can be mounted
|
||||
|
||||
Reference in New Issue
Block a user