track switching fixed! (sorta!)
This commit is contained in:
@@ -60,12 +60,6 @@
|
|||||||
|
|
||||||
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);
|
||||||
@@ -110,6 +104,66 @@
|
|||||||
prevIsMobile = nextIsMobile;
|
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
|
// Media Session bindings
|
||||||
const media = createMediaSessionBindings({
|
const media = createMediaSessionBindings({
|
||||||
play: () => void audioEl?.play(),
|
play: () => void audioEl?.play(),
|
||||||
@@ -135,83 +189,6 @@
|
|||||||
// Scrubber is now driven by `bind:currentTime` on the <audio> element and
|
// 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.
|
// `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() {
|
function onAudioPlay() {
|
||||||
isPlaying = true;
|
isPlaying = true;
|
||||||
media.setPlaybackState("playing");
|
media.setPlaybackState("playing");
|
||||||
@@ -232,7 +209,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function onAudioLoadedMetadata() {
|
function onAudioLoadedMetadata() {
|
||||||
isSrcChanging = false;
|
|
||||||
// `duration` is synced via `bind:duration` on <audio>.
|
// `duration` is synced via `bind:duration` on <audio>.
|
||||||
if (!audioEl) return;
|
if (!audioEl) return;
|
||||||
|
|
||||||
@@ -520,31 +496,15 @@
|
|||||||
media.setTrack(snap.currentTrack);
|
media.setTrack(snap.currentTrack);
|
||||||
schedulePersistNow();
|
schedulePersistNow();
|
||||||
|
|
||||||
// Keep preload targets updated as the queue/traversal state changes
|
// When track changes for any reason, ensure volume is correct.
|
||||||
updatePreloadTargets();
|
if (audioEl) {
|
||||||
|
audioEl.volume = snap.volume;
|
||||||
if (!audioEl) return;
|
|
||||||
|
|
||||||
audioEl.volume = snap.volume;
|
|
||||||
|
|
||||||
const track = snap.currentTrack;
|
|
||||||
if (!track) {
|
|
||||||
if (audioEl.src) {
|
|
||||||
audioEl.removeAttribute("src");
|
|
||||||
audioEl.load();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const desired = track.src;
|
// If there's no track, ensure audio element is reset
|
||||||
const desiredAbs = new URL(desired, window.location.href).href;
|
if (!snap.currentTrack && audioEl && audioEl.src) {
|
||||||
|
audioEl.removeAttribute("src");
|
||||||
// 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.load();
|
||||||
audioEl.currentTime = 0;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -554,13 +514,6 @@
|
|||||||
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;
|
||||||
@@ -596,10 +549,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<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>
|
</svelte:head>
|
||||||
|
|
||||||
<!-- Mobile UI is portaled to document.body so the GlobalPlayer can be mounted
|
<!-- Mobile UI is portaled to document.body so the GlobalPlayer can be mounted
|
||||||
|
|||||||
Reference in New Issue
Block a user