Add prefetching for images and audio. Add cover image to mediasession
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user