feat(web): stream resilience — preload=auto, stall recovery, next-track prefetch
Phases 1+2 of the background-play resilience work. Net effect on desktop: gap-free track transitions on slow networks, automatic recovery from transient stalls / network errors mid-track. - preload="metadata" → "auto" on the main audio element so the browser pre-buffers more aggressively. - attachStallRetry() — listens for stalled / waiting / network-error events. Schedules a reload-from-current-position after delayMs (3s default) if the buffer hasn't recovered. MEDIA_ERR_NETWORK triggers immediate retry. Bounded by maxRetries (3 default) to prevent tight loops on persistent failures. - audioLoader seam — `lib/player/audioLoader.ts` exposes a resolve(track) → URL + optional prefetch(track) hint. Default returns track.stream_url; the planned Tauri shell can swap via setAudioLoader() to return blob URLs backed by a Rust cache, mirroring the Flutter drift+LockCachingAudioSource pattern. - Hidden prefetch <audio> element. When current track passes 50%, start loading queue[index+1]. On track-end the swap is gap-free via browser HTTP cache + audio buffer warm-up. Phase 3 (service worker chunk cache) deliberately deferred — that work belongs in the Tauri shell as native Rust caching, not as a web-only investment that would compete with it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
// Audio loader seam. Today: returns the server-relative stream URL
|
||||
// directly. In the future Tauri shell, the loader can be swapped to
|
||||
// return blob URLs backed by a Rust-side cache (mirroring the Flutter
|
||||
// drift+LockCachingAudioSource pattern), without touching the player UI.
|
||||
//
|
||||
// Two operations:
|
||||
// - resolve(track) — returns the URL the <audio> element should src.
|
||||
// - prefetch(track) — hint that a track will be needed soon. Default
|
||||
// no-op (the browser handles it via the prefetch <audio> element);
|
||||
// Tauri can warm its native cache here.
|
||||
|
||||
import type { TrackRef } from '$lib/api/types';
|
||||
|
||||
export type AudioLoader = {
|
||||
resolve: (track: TrackRef) => string;
|
||||
prefetch?: (track: TrackRef) => void;
|
||||
};
|
||||
|
||||
const defaultLoader: AudioLoader = {
|
||||
resolve: (track) => track.stream_url
|
||||
};
|
||||
|
||||
let _current: AudioLoader = defaultLoader;
|
||||
|
||||
export const audioLoader: AudioLoader = {
|
||||
resolve: (track) => _current.resolve(track),
|
||||
prefetch: (track) => _current.prefetch?.(track)
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace the active loader. Call once at startup from the Tauri shell;
|
||||
* web shell uses the default and never calls this.
|
||||
*/
|
||||
export function setAudioLoader(loader: AudioLoader): void {
|
||||
_current = loader;
|
||||
}
|
||||
|
||||
/** Test-only: restore the default loader between tests. */
|
||||
export function __resetAudioLoader(): void {
|
||||
_current = defaultLoader;
|
||||
}
|
||||
Reference in New Issue
Block a user