allow seeking to unbuffered parts, display buffers better

AuthorKonata <konata@posteo.jp>
Date
Commit12f191f879dc8d4316257e7b72fa81f0d4bf77d6
Parenta779ebe
4 files changed, 36 insertions(+), 31 deletions(-)
▾MREADME.md
@@ -49,6 +49,9 @@ You can configure the backend with the following environment variables:
- My synced files randomly don't work, are missing
- By default websites aren't granted infinite storage or have persisten data. The frontend requests these permissions by calling `navigator.storage.persist()` on startup, which causes some browsers to show a permissions popup (e.g. firefox), while others just seem to ignore this request and use a heuristic like interaction amount and bookmarking instead. So if you have the error described above you either are running out of actual disk storage, or you need to check what heuristics your browser uses and trigger them.
## Known issues
- Range requests are not correctly handled in Bun right now, and e.g. doesn't send the total byte length correctly. I worked around this by sending a chunked transfer-encoded response, and setting the range and content-length header manually, which seems to work with the browsers I tested, but still might cause issues in some cases
## Attributions
- Font: https://github.com/TakWolf/ark-pixel-font
- Icons: https://github.com/hackernoon/pixel-icon-library
▾Mwebclient/src/App.tsx
@@ -33,7 +33,7 @@ const App: Component = () => {
currentTrack: null,
currentTime: 0,
volume: 0.5,
buffered: 0,
buffered: [],
rate: 1,
waitingForStart: false,
});
@@ -151,7 +151,7 @@ const App: Component = () => {
batch(() => {
setPlayerState("isPlaying", true);
setPlayerState("currentTime", 0);
setPlayerState("buffered", 0);
setPlayerState("buffered", []);
//clone, otherwise it shares the reference
setPlayerState("currentTrack", { ...track });
setPlayerState("waitingForStart", true);
@@ -161,8 +161,6 @@ const App: Component = () => {
} catch (e) {
setPlayerState("isPlaying", false);
handlePlayerError(e);
} finally {
setPlayerState("waitingForStart", false);
}
}
@@ -225,13 +223,21 @@ const App: Component = () => {
}
}
function timeRangesToArray(timeRanges: TimeRanges) {
const result = [];
for (let i = 0; i < timeRanges.length; i++) {
result.push({
start: timeRanges.start(i),
end: timeRanges.end(i),
});
}
return result;
}
// Update progress when the time updates
function updateProgress() {
setPlayerState("currentTime", audioElement.currentTime);
setPlayerState(
"buffered",
audioElement.buffered.length > 0 ? audioElement.buffered.end(audioElement.buffered.length - 1) : 0,
);
setPlayerState("buffered", timeRangesToArray(audioElement.buffered));
}
// Initialize app
@@ -284,6 +290,7 @@ const App: Component = () => {
audioElement.addEventListener("pause", () => setPlayerState("isPlaying", false));
audioElement.addEventListener("play", () => setPlayerState("isPlaying", true));
audioElement.addEventListener("volumechange", () => setPlayerState("volume", audioElement.volume));
audioElement.addEventListener("canplay", () => setPlayerState("waitingForStart", false));
window.addEventListener("hashchange", () => {
setCurrentDir(decodePath(window.location.hash.slice(1)));
▾Mwebclient/src/components/PlayerControls.tsx
@@ -1,6 +1,6 @@
import "./PlayerControls.css";
import { type AppOptions, getItemName, type PlayerState } from "../types";
import { type Accessor, createEffect, createSignal, type Setter } from "solid-js";
import { type Accessor, createEffect, createSignal, For, type Setter } from "solid-js";
import { NextTrackSVG, PauseSVG, PlaySVG, PreviousTrackSVG, RepeatSVG, ShuffleSVG, VolumeSVG } from "./SVG";
import { formatTime } from "../utils";
import type { SetStoreFunction } from "solid-js/store";
@@ -50,9 +50,8 @@ export default function PlayerControls(props: PlayerControlsProps) {
const pos = (e.clientX - rect.left) / rect.width;
const duration = playerState.currentTrack.metadata.duration;
props.audioElement.currentTime = isLocalPlaying()
? pos * duration
: Math.min(pos * duration, playerState.buffered - 1);
props.setPlayerState("waitingForStart", true);
props.audioElement.currentTime = pos * duration;
props.setPlayerState("currentTime", pos * duration);
}
@@ -68,25 +67,21 @@ export default function PlayerControls(props: PlayerControlsProps) {
""
)}
</div>
<div class="progress-container">
<div class="progress-bar" onClick={seekTo} style={controlsDisabled() ? { cursor: "default" } : {}}>
<div
class="buffered-fill"
style={{
width: `${
props.playerState.currentTrack
? isLocalPlaying()
? 100
: Math.min(
100,
(props.playerState.buffered /
(props.playerState.currentTrack?.metadata.duration ?? Number.POSITIVE_INFINITY)) *
100,
)
: 0
}%`,
<div class="progress-container" onClick={seekTo}>
<div class="progress-bar" style={controlsDisabled() ? { cursor: "default" } : {}}>
<For each={props.playerState.buffered}>
{(bufferRange) => {
return (
<div
class="buffered-fill"
style={{
left: `${(bufferRange.start / (props.playerState.currentTrack?.metadata.duration as number)) * 100}%`,
right: `${100 - (bufferRange.end / (props.playerState.currentTrack?.metadata.duration as number)) * 100}%`,
}}
/>
);
}}
/>
</For>
<div
class="progress-fill"
style={{
▾Mwebclient/src/types.ts
@@ -22,7 +22,7 @@ export interface PlayerState {
currentTrack: PlaylistItem | null;
currentTime: number;
volume: number;
buffered: number;
buffered: { start: number; end: number }[];
rate: number;
waitingForStart: boolean;
}