decrease chunk size to 4MB, fix more potential breakages

This commit is contained in:
2026-09-02 20:18:42 +02:00
parent c038316461
commit 39593d470d
4 changed files with 78 additions and 29 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ const PARTS_DIR = 'parts';
// backoff below tolerates roughly 8 minutes of outage before giving up
const MAX_ATTEMPTS = 8;
const CHUNK_SIZE = 8 * 1024 * 1024;
const CHUNK_SIZE = 4 * 1024 * 1024;
// 60s as the limit for downloading one chunk is a reasonable guess
// as the intent is to catch socket breakage during cellular network changes for example
+3 -2
View File
@@ -23,14 +23,15 @@ export class StreamContext {
}
getSongAtTime(time: number): number {
return this.#locationOf(time, this.#timestamps) - 1;
return Math.max(this.#locationOf(time, this.#timestamps) - 1, 0);
}
updateCurrentSong(currentTime: number, songIndex: number) {
const ts = this.#timestamps;
// the first track need not start at 0, so descending past index 0 is possible
const recurse = (idx: number): number => {
if (currentTime >= ts[idx + 2]) return recurse(idx + 1);
if (currentTime < ts[idx + 1]) return recurse(idx - 1);
if (idx > 0 && currentTime < ts[idx + 1]) return recurse(idx - 1);
return idx;
};
this.songIndex = recurse(songIndex);
+35 -16
View File
@@ -2,29 +2,47 @@
import StreamPage from './StreamPage.svelte';
import MetadataEditor from './MetadataEditor.svelte';
import Player from './Player.svelte';
import { untrack } from 'svelte';
import { dev } from '$app/environment';
import { getStreamContext } from '$lib/streamContext.svelte.ts';
import * as streamCache from '$lib/streamCache.svelte.ts';
let { data } = $props();
const ctx = getStreamContext();
let playerSrc = $state<string | null>(null);
// reactivity runs on `stream` and `streamCache.cached` here
$effect(() => {
const stream = data.stream;
ctx.setCurrent(stream);
playerSrc = null;
let objectUrl: string | null = null;
if (streamCache.cached.has(stream.id)) {
streamCache.getUrl(stream.id).then((url) => {
objectUrl = url;
playerSrc = url ?? `/media/tracks/${stream.filename}`;
let source = $state<{ id: string; url: string } | null>(null);
const streamId = $derived(data.stream.id);
const isCached = $derived(streamCache.cached.has(streamId));
// update stream context only when we actually swap streams
// this avoids reloading the audio element when we click on the same stream
$effect.pre(() => {
streamId;
ctx.setCurrent(untrack(() => data.stream));
});
} else {
playerSrc = `/media/tracks/${stream.filename}`;
$effect.pre(() => {
const id = streamId;
const cached = isCached;
const networkUrl = untrack(() => `/media/tracks/${data.stream.filename}`);
// not in cache (or just deleted from cache)? use network url
if (!cached) {
source = { id, url: networkUrl };
return;
}
// otherwise use the cached blob
// if the cache finished downloading while the stream is playing,
// Player.svelte will notice and swap it in at the right time uninterrupted
let objectUrl: string | null = null;
streamCache.getUrl(id).then((url) => {
objectUrl = url;
source = { id, url: url ?? networkUrl };
});
return () => {
// cleanup blob handle when switching away from cached stream
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
});
@@ -38,9 +56,10 @@
<StreamPage />
</div>
<div id="player">
{#key playerSrc}
{#if playerSrc}
<Player display={true} src={playerSrc} />
{#key streamId}
<!-- until streamCache.getUrl() resolves, source still points at the previous stream -->
{#if source?.id === streamId}
<Player display={true} src={source.url} />
{/if}
{/key}
</div>
+37 -8
View File
@@ -4,11 +4,13 @@
-->
<script module lang="ts">
let getAudio: () => HTMLAudioElement;
let getAudio: (() => HTMLAudioElement) | null = null;
export function jumpToTrack(s: number) {
getAudio().currentTime = s;
getAudio().play();
const el = getAudio?.();
if (!el) return;
el.currentTime = s;
el.play();
}
</script>
@@ -23,7 +25,6 @@
interface Props {
src: any;
audio?: any;
paused?: boolean;
duration?: number;
muted?: boolean;
@@ -42,7 +43,6 @@
let {
src,
audio = $bindable(null),
paused = $bindable(true),
duration = $bindable(0),
muted = $bindable(false),
@@ -59,9 +59,27 @@
onended
}: Props = $props();
getAudio = () => {
return audio;
let audio = $state<HTMLAudioElement>()!;
// if src changes on the same stream id (in practice when a stream cache finishes downloading),
// save current playhead, then call load() to use the new src
// playhead is restored in the onLoadedMetadata below in this file
let pending: { time: number; playing: boolean } | null = null;
$effect(() => {
src;
if (!audio) return;
pending =
audio.currentTime > 0 ? { time: audio.currentTime, playing: !audio.paused } : null;
audio.load();
});
// module scope, so it has to be released or a destroyed instance keeps answering
$effect(() => {
getAudio = () => audio;
return () => {
getAudio = null;
};
});
let currentTime = $state(0);
let tooltip = $state<HTMLElement>();
@@ -92,9 +110,14 @@
setVolume(volume);
// set actions for previous/next track media buttons
// mediaSession is global, so handlers of a destroyed instance would keep firing
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('previoustrack', previousTrack);
navigator.mediaSession.setActionHandler('nexttrack', nextTrack);
return () => {
navigator.mediaSession.setActionHandler('previoustrack', null);
navigator.mediaSession.setActionHandler('nexttrack', null);
};
}
});
@@ -154,7 +177,7 @@
}
}
// workaround for bug https://github.com/sveltejs/svelte/issues/5347
// partly workaround for bug https://github.com/sveltejs/svelte/issues/5347
// need to init duration & volume after SSR first load
// also workaround for bug https://github.com/sveltejs/svelte/issues/5914
$effect(() => {
@@ -162,6 +185,12 @@
const onLoadedMetadata = () => {
duration = audio.duration;
if (pending) {
// same stream but audio url changed, reload audio at current playhead
audio.currentTime = currentTime = pending.time;
if (pending.playing) audio.play().catch(() => {});
pending = null;
}
paused = audio.paused;
setVolume(volume);
};