Add prefetching for images and audio. Add cover image to mediasession

This commit is contained in:
2026-09-02 00:13:32 +02:00
parent 2dafceb304
commit 4639b2d19c
2 changed files with 81 additions and 19 deletions

View File

@@ -14,6 +14,54 @@ let playing = $state(false)
let media_session = false
let siblings: FSNode[] = $state([])
// The player source is managed manually instead of with a src attribute so
// that change_track can swap it without svelte reloading it afterwards
let current_src = ""
$effect(() => {
const url = fs_path_url($nav.base.path)
if (url !== current_src) {
current_src = url
player.src = url
player.play().catch(() => {})
}
})
// Preloads the next track into the browser cache so that switching to it
// doesn't have to wait for the network. Files are served with a one year cache
// lifetime, so the player picks the data straight out of the cache
const preloader = new Audio()
preloader.preload = "auto"
$effect(() => {
const index = siblings.findIndex(s => s.path === $nav.base.path)
const next = siblings[index + 1]
if (index !== -1 && next !== undefined && next.type === "file" && !nav.shuffle) {
preloader.src = fs_path_url(next.path)
}
})
// Switching tracks starts playback of the new file immediately and only then
// navigates to it. Firefox on Android freezes a background tab as soon as it
// stops playing audio, so anything waiting on a network request after the
// ended event would never run
const change_track = (offset: number) => {
const index = siblings.findIndex(s => s.path === $nav.base.path)
if (index === -1) {
return
}
const next = nav.shuffle ?
siblings[Math.floor(Math.random() * siblings.length)] :
siblings[index + offset]
if (next === undefined || next.type === "dir") {
return
}
current_src = fs_path_url(next.path)
player.src = current_src
player.play().catch(() => {})
nav.navigate(next.path, true)
}
export const toggle_playback = () => playing ? player.pause() : player.play()
export const toggle_mute = () => player.muted = !player.muted
@@ -29,24 +77,29 @@ export const seek = (delta: number) => {
var background_div: HTMLDivElement
export const update = async () => {
siblings = await nav.get_siblings()
let cover = ""
for (const sib of siblings) {
const lower = sib.name.toLowerCase()
if (lower === "cover.jpg" || lower === "cover.png" || lower === "cover.webp") {
cover = fs_path_url(sib.path)
break
}
}
// Always assigned, so that the cover of the previous album is cleared when
// the new one doesn't have one
background_div.style.backgroundImage = cover === "" ? "" : `url("${cover}")`
if (media_session) {
navigator.mediaSession.metadata = new MediaMetadata({
title: nav.base.name,
artist: "Nova",
album: "unknown",
artwork: cover === "" ? [] : [{ src: cover }],
});
}
siblings = await nav.get_siblings()
for(const sib of siblings) {
const lower = sib.name.toLowerCase()
if (lower === "cover.jpg" || lower === "cover.png" || lower === "cover.webp") {
console.debug("Found album cover image", sib)
background_div.style.backgroundImage = `url("/api/filesystem/${sib.path}")`
break
}
}
}
onMount(() => {
@@ -55,8 +108,8 @@ onMount(() => {
navigator.mediaSession.setActionHandler('play', () => player.play());
navigator.mediaSession.setActionHandler('pause', () => player.pause());
navigator.mediaSession.setActionHandler('stop', () => player.pause());
navigator.mediaSession.setActionHandler('previoustrack', () => nav.open_sibling(-1));
navigator.mediaSession.setActionHandler('nexttrack', () => nav.open_sibling(1));
navigator.mediaSession.setActionHandler('previoustrack', () => change_track(-1));
navigator.mediaSession.setActionHandler('nexttrack', () => change_track(1));
}
})
</script>
@@ -68,16 +121,14 @@ onMount(() => {
<audio
bind:this={player}
class="player"
src={fs_path_url($nav.base.path)}
autoplay
controls
onpause={() => playing = false}
onplay={() => playing = true}
onended={() => nav.open_sibling(1)}>
onended={() => change_track(1)}>
<track kind="captions"/>
</audio>
<div style="text-align: center;">
<button onclick={() => nav.open_sibling(-1)}><i class="icon">skip_previous</i></button>
<button onclick={() => change_track(-1)}><i class="icon">skip_previous</i></button>
<button onclick={() => seek(-10)}><i class="icon">replay_10</i></button>
<button onclick={toggle_playback}>
{#if playing}
@@ -87,7 +138,7 @@ onMount(() => {
{/if}
</button>
<button onclick={() => seek(10)}><i class="icon">forward_10</i></button>
<button onclick={() => nav.open_sibling(1)}><i class="icon">skip_next</i></button>
<button onclick={() => change_track(1)}><i class="icon">skip_next</i></button>
</div>
<h2>Tracklist</h2>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { swipe_nav } from "lib/SwipeNavigate";
import { fs_path_url } from "lib/FilesystemAPI.svelte";
import { fs_path_url, fs_node_type, type FSNode } from "lib/FilesystemAPI.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
@@ -56,11 +56,22 @@ export const update = async () => {
if (siblings[i].name === nav.base.name) {
swipe_prev = i > 0
swipe_next = i < siblings.length-1
preload(siblings[i-1])
preload(siblings[i+1])
break
}
}
}
// Preloads a neighbouring image into the browser cache so that swiping to it
// doesn't have to wait for the network. Files are served with a one year cache
// lifetime, so the <img> element picks the data straight out of the cache
const preload = (node: FSNode | undefined) => {
if (node !== undefined && fs_node_type(node) === "image") {
new Image().src = fs_path_url(node.path)
}
}
const on_load = () => loading_finish()
const clamp = (val: number, min: number, max: number) => Math.min(Math.max(val, min), max)