v4: an audio file's waveform is made once by the server, and the player seeks on one accessible scrubber — every player downloaded the whole file a second time and decoded all of it in the browser to draw 150 bars, so a long FLAC cost tens of megabytes of memory per page, and the waveform could only be clicked: no drag, no keyboard, no slider for a screen reader. GET /api/dumps/:id/waveform now answers an audio dump's peaks, which lib/ffmpeg.ts decodes with ffmpeg on first request, mono at 8 kHz and streamed, keeps beside the thumbnails as the response itself, and serves with an ETag (a ten-minute MP3 takes about a second, then a few hundred bytes). The peaks are taken by shared/waveform.ts's peaksOf, scaled to the loudest so a quiet recording fills the track, and the dump form's preview of a file not yet uploaded decodes it with the same function, at 8 kHz in an OfflineAudioContext. Scrubber replaces Waveform and SeekBar: a role="slider" that seeks where a pointer lets go, follows a drag (the clock shows where it is held), takes arrows, Home and End, and draws the bars as one path with the played part clipped by a nested svg, so it moves smoothly and a timeupdate redraws two attributes instead of 150 rects; audio without a waveform, and video, get a plain bar. MediaPlayer reads whether it plays from the element's events alone, so a click no longer reports the change twice, its handles for PlayerProvider are stable callbacks instead of refs to refs, the drag no longer reattaches every listener, and the <source> children that src made dead are gone, with the mime prop. The file, thumbnail and waveform routes resolve their dump, token and caching through lib/dump-media.ts, and what is generated from an upload is named and removed in lib/ffmpeg.ts, the custom thumbnail by dump-service's customThumbnailPath, instead of paths spelled out in each. The test setup's play() and pause() fire their events as a browser does. 277 backend tests and 684 frontend tests pass, and deno check, tsc, eslint, doc:lint and i18n:compile --strict are clean; checked in the app with a real FLAC on its dump page and in the global player at 1280 and 390px, seeking by keyboard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
38
src/hooks/player/usePeaks.ts
Normal file
38
src/hooks/player/usePeaks.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Gives MediaPlayer the waveform it draws on its `Scrubber`.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
loadPeaks,
|
||||
peaksKey,
|
||||
type PeaksSource,
|
||||
} from "../../utils/player/waveform.ts";
|
||||
|
||||
/**
|
||||
* Returns the peaks of `source` once loaded, and null until then, without a
|
||||
* source, or when it can't be loaded: the scrubber then draws a plain bar.
|
||||
*/
|
||||
export function usePeaks(source: PeaksSource | undefined): number[] | null {
|
||||
const key = source && peaksKey(source);
|
||||
const [loaded, setLoaded] = useState<{ key: string; peaks: number[] }>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!source) return;
|
||||
let cancelled = false;
|
||||
loadPeaks(source).then(
|
||||
(peaks) => !cancelled && setLoaded({ key: peaksKey(source), peaks }),
|
||||
() => {},
|
||||
);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// The source is a new object on each render; its key says when it changed.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [key]);
|
||||
|
||||
// A load for another source may still be in, until this one's arrives.
|
||||
return loaded && loaded.key === key ? loaded.peaks : null;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
dumpFileUrl,
|
||||
dumpThumbnailUrl,
|
||||
dumpUrl,
|
||||
dumpWaveformUrl,
|
||||
} from "../../utils/shared/urls.ts";
|
||||
|
||||
/** Checks whether `dump` is an uploaded file the global player can play. */
|
||||
@@ -76,6 +77,9 @@ export function usePlayFile(dump: Dump): UsePlayFileResult {
|
||||
kind: "file",
|
||||
fileUrl,
|
||||
mimeType: dump.fileMime ?? "",
|
||||
waveformUrl: dump.fileMime?.startsWith("audio/")
|
||||
? dumpWaveformUrl(dump, token)
|
||||
: undefined,
|
||||
title: dump.title,
|
||||
dumpHref: dumpUrl(dump),
|
||||
artworkUrl: fileArtworkUrl(dump, token),
|
||||
|
||||
Reference in New Issue
Block a user