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 media_session = false
|
||||||
let siblings: FSNode[] = $state([])
|
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_playback = () => playing ? player.pause() : player.play()
|
||||||
export const toggle_mute = () => player.muted = !player.muted
|
export const toggle_mute = () => player.muted = !player.muted
|
||||||
|
|
||||||
@@ -29,24 +77,29 @@ export const seek = (delta: number) => {
|
|||||||
|
|
||||||
var background_div: HTMLDivElement
|
var background_div: HTMLDivElement
|
||||||
export const update = async () => {
|
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) {
|
if (media_session) {
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
title: nav.base.name,
|
title: nav.base.name,
|
||||||
artist: "Nova",
|
artist: "Nova",
|
||||||
album: "unknown",
|
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(() => {
|
onMount(() => {
|
||||||
@@ -55,8 +108,8 @@ onMount(() => {
|
|||||||
navigator.mediaSession.setActionHandler('play', () => player.play());
|
navigator.mediaSession.setActionHandler('play', () => player.play());
|
||||||
navigator.mediaSession.setActionHandler('pause', () => player.pause());
|
navigator.mediaSession.setActionHandler('pause', () => player.pause());
|
||||||
navigator.mediaSession.setActionHandler('stop', () => player.pause());
|
navigator.mediaSession.setActionHandler('stop', () => player.pause());
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', () => nav.open_sibling(-1));
|
navigator.mediaSession.setActionHandler('previoustrack', () => change_track(-1));
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', () => nav.open_sibling(1));
|
navigator.mediaSession.setActionHandler('nexttrack', () => change_track(1));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -68,16 +121,14 @@ onMount(() => {
|
|||||||
<audio
|
<audio
|
||||||
bind:this={player}
|
bind:this={player}
|
||||||
class="player"
|
class="player"
|
||||||
src={fs_path_url($nav.base.path)}
|
|
||||||
autoplay
|
|
||||||
controls
|
controls
|
||||||
onpause={() => playing = false}
|
onpause={() => playing = false}
|
||||||
onplay={() => playing = true}
|
onplay={() => playing = true}
|
||||||
onended={() => nav.open_sibling(1)}>
|
onended={() => change_track(1)}>
|
||||||
<track kind="captions"/>
|
<track kind="captions"/>
|
||||||
</audio>
|
</audio>
|
||||||
<div style="text-align: center;">
|
<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={() => seek(-10)}><i class="icon">replay_10</i></button>
|
||||||
<button onclick={toggle_playback}>
|
<button onclick={toggle_playback}>
|
||||||
{#if playing}
|
{#if playing}
|
||||||
@@ -87,7 +138,7 @@ onMount(() => {
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => seek(10)}><i class="icon">forward_10</i></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>
|
</div>
|
||||||
|
|
||||||
<h2>Tracklist</h2>
|
<h2>Tracklist</h2>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { swipe_nav } from "lib/SwipeNavigate";
|
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 type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
import { loading_finish, loading_start } from "lib/Loading";
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
@@ -56,11 +56,22 @@ export const update = async () => {
|
|||||||
if (siblings[i].name === nav.base.name) {
|
if (siblings[i].name === nav.base.name) {
|
||||||
swipe_prev = i > 0
|
swipe_prev = i > 0
|
||||||
swipe_next = i < siblings.length-1
|
swipe_next = i < siblings.length-1
|
||||||
|
preload(siblings[i-1])
|
||||||
|
preload(siblings[i+1])
|
||||||
break
|
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 on_load = () => loading_finish()
|
||||||
|
|
||||||
const clamp = (val: number, min: number, max: number) => Math.min(Math.max(val, min), max)
|
const clamp = (val: number, min: number, max: number) => Math.min(Math.max(val, min), max)
|
||||||
|
|||||||
Reference in New Issue
Block a user