decrease chunk size to 4MB, fix more potential breakages
This commit is contained in:
@@ -11,7 +11,7 @@ const PARTS_DIR = 'parts';
|
|||||||
// backoff below tolerates roughly 8 minutes of outage before giving up
|
// backoff below tolerates roughly 8 minutes of outage before giving up
|
||||||
const MAX_ATTEMPTS = 8;
|
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
|
// 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
|
// as the intent is to catch socket breakage during cellular network changes for example
|
||||||
|
|||||||
@@ -23,14 +23,15 @@ export class StreamContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSongAtTime(time: number): number {
|
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) {
|
updateCurrentSong(currentTime: number, songIndex: number) {
|
||||||
const ts = this.#timestamps;
|
const ts = this.#timestamps;
|
||||||
|
// the first track need not start at 0, so descending past index 0 is possible
|
||||||
const recurse = (idx: number): number => {
|
const recurse = (idx: number): number => {
|
||||||
if (currentTime >= ts[idx + 2]) return recurse(idx + 1);
|
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;
|
return idx;
|
||||||
};
|
};
|
||||||
this.songIndex = recurse(songIndex);
|
this.songIndex = recurse(songIndex);
|
||||||
|
|||||||
@@ -2,29 +2,47 @@
|
|||||||
import StreamPage from './StreamPage.svelte';
|
import StreamPage from './StreamPage.svelte';
|
||||||
import MetadataEditor from './MetadataEditor.svelte';
|
import MetadataEditor from './MetadataEditor.svelte';
|
||||||
import Player from './Player.svelte';
|
import Player from './Player.svelte';
|
||||||
|
import { untrack } from 'svelte';
|
||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import { getStreamContext } from '$lib/streamContext.svelte.ts';
|
import { getStreamContext } from '$lib/streamContext.svelte.ts';
|
||||||
import * as streamCache from '$lib/streamCache.svelte.ts';
|
import * as streamCache from '$lib/streamCache.svelte.ts';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
const ctx = getStreamContext();
|
const ctx = getStreamContext();
|
||||||
let playerSrc = $state<string | null>(null);
|
|
||||||
|
|
||||||
// reactivity runs on `stream` and `streamCache.cached` here
|
let source = $state<{ id: string; url: string } | null>(null);
|
||||||
$effect(() => {
|
|
||||||
const stream = data.stream;
|
const streamId = $derived(data.stream.id);
|
||||||
ctx.setCurrent(stream);
|
const isCached = $derived(streamCache.cached.has(streamId));
|
||||||
playerSrc = null;
|
|
||||||
let objectUrl: string | null = null;
|
// update stream context only when we actually swap streams
|
||||||
if (streamCache.cached.has(stream.id)) {
|
// this avoids reloading the audio element when we click on the same stream
|
||||||
streamCache.getUrl(stream.id).then((url) => {
|
$effect.pre(() => {
|
||||||
objectUrl = url;
|
streamId;
|
||||||
playerSrc = url ?? `/media/tracks/${stream.filename}`;
|
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 () => {
|
return () => {
|
||||||
|
// cleanup blob handle when switching away from cached stream
|
||||||
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -38,9 +56,10 @@
|
|||||||
<StreamPage />
|
<StreamPage />
|
||||||
</div>
|
</div>
|
||||||
<div id="player">
|
<div id="player">
|
||||||
{#key playerSrc}
|
{#key streamId}
|
||||||
{#if playerSrc}
|
<!-- until streamCache.getUrl() resolves, source still points at the previous stream -->
|
||||||
<Player display={true} src={playerSrc} />
|
{#if source?.id === streamId}
|
||||||
|
<Player display={true} src={source.url} />
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
let getAudio: () => HTMLAudioElement;
|
let getAudio: (() => HTMLAudioElement) | null = null;
|
||||||
|
|
||||||
export function jumpToTrack(s: number) {
|
export function jumpToTrack(s: number) {
|
||||||
getAudio().currentTime = s;
|
const el = getAudio?.();
|
||||||
getAudio().play();
|
if (!el) return;
|
||||||
|
el.currentTime = s;
|
||||||
|
el.play();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -23,7 +25,6 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
src: any;
|
src: any;
|
||||||
audio?: any;
|
|
||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
@@ -42,7 +43,6 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
src,
|
src,
|
||||||
audio = $bindable(null),
|
|
||||||
paused = $bindable(true),
|
paused = $bindable(true),
|
||||||
duration = $bindable(0),
|
duration = $bindable(0),
|
||||||
muted = $bindable(false),
|
muted = $bindable(false),
|
||||||
@@ -59,9 +59,27 @@
|
|||||||
onended
|
onended
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
getAudio = () => {
|
let audio = $state<HTMLAudioElement>()!;
|
||||||
return audio;
|
|
||||||
};
|
// 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 currentTime = $state(0);
|
||||||
let tooltip = $state<HTMLElement>();
|
let tooltip = $state<HTMLElement>();
|
||||||
@@ -92,9 +110,14 @@
|
|||||||
setVolume(volume);
|
setVolume(volume);
|
||||||
|
|
||||||
// set actions for previous/next track media buttons
|
// set actions for previous/next track media buttons
|
||||||
|
// mediaSession is global, so handlers of a destroyed instance would keep firing
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', previousTrack);
|
navigator.mediaSession.setActionHandler('previoustrack', previousTrack);
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', nextTrack);
|
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
|
// need to init duration & volume after SSR first load
|
||||||
// also workaround for bug https://github.com/sveltejs/svelte/issues/5914
|
// also workaround for bug https://github.com/sveltejs/svelte/issues/5914
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -162,6 +185,12 @@
|
|||||||
|
|
||||||
const onLoadedMetadata = () => {
|
const onLoadedMetadata = () => {
|
||||||
duration = audio.duration;
|
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;
|
paused = audio.paused;
|
||||||
setVolume(volume);
|
setVolume(volume);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user