allow seeking to unbuffered parts, display buffers better
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={{