@banou/media-player 0.10.20 → 0.10.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -89,8 +89,8 @@ reads as a browser bug rather than a missing asset. Serve both.
89
89
 
90
90
  `libavWorkerUrl` (`libav-wasm/build/worker.js`) is named individually and still copies fine.
91
91
 
92
- `normalizerWorkletUrl` is this package's own `dist/volume-normalizer-worklet.js`, a plain script that
93
- copies fine or imports as a url; see [the volume normalizer](#the-volume-normalizer).
92
+ `normalizerWorkletUrl` is this package's own `dist/volume-normalizer-worklet.js`, one plain module with no imports
93
+ that copies fine or imports as a url; see [the volume normalizer](#the-volume-normalizer).
94
94
 
95
95
  ### jassub's assets are imported, not copied
96
96
 
@@ -241,9 +241,9 @@ until a pointer that hovers moves again.
241
241
 
242
242
  ### The volume normalizer
243
243
 
244
- A "Normalize volume" switch in the settings menu holds the loudness steady, so a music cue comes out no
245
- louder than the dialogue around it. It is offered once you pass the worklet's url, and it is off until
246
- the viewer turns it on or you pass `normalizeVolume`:
244
+ A "Normalize volume" switch in the settings menu keeps the file's own normal level and turns down what goes well over
245
+ it, so a sudden blast comes out near the dialogue and the dialogue is left as it was. It is offered once you pass the
246
+ worklet's url, and it is on unless the viewer turns it off or you pass `normalizeVolume={false}`:
247
247
 
248
248
  ```tsx
249
249
  import normalizerWorkletUrl from '@banou/media-player/volume-normalizer-worklet.js?no-inline&url'
@@ -251,27 +251,34 @@ import normalizerWorkletUrl from '@banou/media-player/volume-normalizer-worklet.
251
251
  <MediaPlayer
252
252
  {...source}
253
253
  normalizerWorkletUrl={normalizerWorkletUrl}
254
- normalizeVolume={saved}
254
+ normalizeVolume={saved ?? true}
255
255
  onNormalizeVolumeChange={save}
256
256
  />
257
257
  ```
258
258
 
259
- The player saves nothing, so an app that wants the choice to stick saves what
260
- `onNormalizeVolumeChange` reports and passes it back.
261
-
262
- It is an AudioWorklet that follows EBU short-term loudness (K-weighted, 3 s) and turns the gain down,
263
- never up, toward -24 LUFS: up to 20 dB, cutting with a 0.25 s time constant and recovering with 1 s. A
264
- cue is brought within a dialogue's level in about 3 s, and it adds no delay. The url has to be a
265
- script your origin serves: a page whose CSP says `script-src 'self'` refuses a worklet from a `blob:`
266
- or `data:` url, and `build.lib` inlines assets as `data:` without `?no-inline`.
267
-
268
- The first time it is on, the `<video>` is routed through Web Audio for the rest of its life, because an
269
- element cannot be handed back; off is a bypass. Routing waits until the AudioContext runs: one that
270
- autoplay rules hold back resumes on the page's next click, tap or key press, and the video plays as it
271
- always did until then. WebKit is refused (`createMediaElementSource` in `@banou/ponyfill/web-audio`),
272
- since a routed element loses its volume and mute there. The refusal comes when routing would, once the
273
- switch is on and the AudioContext runs: until then the switch reads as available, and from then on it
274
- reads Unavailable, with the element never routed and playing as it always did.
259
+ The player saves nothing, so an app that wants the choice to stick saves what `onNormalizeVolumeChange` reports and
260
+ passes it back.
261
+
262
+ It works in two stages. A worker reads the file a second time, through `thumbnailRead` when you pass one and only
263
+ within `downloadedRanges`, decodes its audio with WebCodecs, learns the file's normal level (the median 400 ms
264
+ loudness) and computes a cut with unlimited lookahead, so a cut starts before a spike: every 10 ms of the output is
265
+ held to 10 LU over the normal level and every 50 ms to 8 LU. It analyses what is near the playhead first and then the
266
+ rest of the file, keeping only 1 ms loudness readings, never the audio. Wherever that analysis has not reached the
267
+ playhead, and for a track this browser's WebCodecs cannot decode, an AudioWorklet plays a live stage: a 100 ms detector
268
+ that cuts what goes 5 LU over the level heard so far, and the 10 ms loudness held to 10 LU over it, seen 10 ms ahead.
269
+ Neither stage ever boosts. The sound plays 10 ms behind the picture, on or off, once routed.
270
+
271
+ The url names one file that is both the worklet and the worker's script, so it has to be a script your origin serves:
272
+ a page whose CSP says `script-src 'self'` refuses a worklet or a worker from a `blob:` or `data:` url, and
273
+ `build.lib` inlines assets as `data:` without `?no-inline`. A worker must also be same-origin, so serve it from the
274
+ page's own origin.
275
+
276
+ The first time it is on, the `<video>` is routed through Web Audio for the rest of its life, because an element cannot
277
+ be handed back; off is a bypass. Routing waits until the AudioContext runs: one that autoplay rules hold back resumes
278
+ on the page's next click, tap or key press, and the video plays as it always did until then. WebKit is refused
279
+ (`createMediaElementSource` in `@banou/ponyfill/web-audio`), since a routed element loses its volume and mute there.
280
+ The refusal comes when routing would, once the switch is on and the AudioContext runs: until then the switch reads as
281
+ available, and from then on it reads Unavailable, with the element never routed and playing as it always did.
275
282
 
276
283
  ## Layout
277
284
 
@@ -0,0 +1,30 @@
1
+ /** The file the analysis reads, the same one playback plays. */
2
+ export type LoudnessSource = {
3
+ read: (offset: number, size: number) => Promise<ArrayBuffer>;
4
+ length: number;
5
+ publicPath: string;
6
+ libavWorkerUrl: string;
7
+ /** The input stream index of the audio track playing. */
8
+ audioStreamIndex: number;
9
+ };
10
+ export type LoudnessAnalysis = {
11
+ /**
12
+ * Which bytes can be read without waiting on a download, as [start, end) pairs; undefined when all of them can.
13
+ * Nothing outside them is asked of the source.
14
+ */
15
+ update: (ranges?: [number, number][]) => void;
16
+ stop: () => void;
17
+ };
18
+ /**
19
+ * Analyses `source` ahead of the playhead with a second remuxer, in the worker the normalizer's script starts, and
20
+ * hands the worklet behind `node` the cut it computes. It reads only what `update` says is there, analyses what is
21
+ * nearest the playhead first, then the rest of the file, and drops nothing it learned on a seek. Until a stretch is
22
+ * analysed, and for the whole file where this browser's WebCodecs cannot decode the track, the worklet's live stage
23
+ * plays.
24
+ */
25
+ export declare const startLoudnessAnalysis: ({ node, element, scriptUrl, source }: {
26
+ node: AudioWorkletNode;
27
+ element: HTMLMediaElement;
28
+ scriptUrl: string;
29
+ source: LoudnessSource;
30
+ }) => LoudnessAnalysis;
@@ -1,6 +1,8 @@
1
- /** The worklet's processorOptions, read once when the node is made; its header has the defaults. */
1
+ /**
2
+ * The worklet's processorOptions, read once when the node is made: `enabled` false starts it bypassed, and
3
+ * `inputGainDb` is the gain already applied upstream (the element's volume), which loudness is judged without.
4
+ */
2
5
  export type VolumeNormalizerOptions = {
3
- targetLufs?: number;
4
6
  enabled?: boolean;
5
7
  inputGainDb?: number;
6
8
  };