@banou/media-player 0.10.8 → 0.10.10

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,6 +89,9 @@ 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).
94
+
92
95
  ### jassub's assets are imported, not copied
93
96
 
94
97
  jassub 2's worker is an ES module that imports `abslink` and `lfa-ponyfill` by bare specifier plus
@@ -232,6 +235,40 @@ A finger never rests on the control before it presses, so a tap cannot arm it. T
232
235
  only where the primary pointer hovers (`(hover: hover) and (pointer: fine)`), and a touch hides it
233
236
  until a pointer that hovers moves again.
234
237
 
238
+ ### The volume normalizer
239
+
240
+ A "Normalize volume" switch in the settings menu holds the loudness steady, so a music cue comes out no
241
+ louder than the dialogue around it. It is offered once you pass the worklet's url, and it is off until
242
+ the viewer turns it on or you pass `normalizeVolume`:
243
+
244
+ ```tsx
245
+ import normalizerWorkletUrl from '@banou/media-player/volume-normalizer-worklet.js?no-inline&url'
246
+
247
+ <MediaPlayer
248
+ {...source}
249
+ normalizerWorkletUrl={normalizerWorkletUrl}
250
+ normalizeVolume={saved}
251
+ onNormalizeVolumeChange={save}
252
+ />
253
+ ```
254
+
255
+ The player saves nothing, so an app that wants the choice to stick saves what
256
+ `onNormalizeVolumeChange` reports and passes it back.
257
+
258
+ It is an AudioWorklet that follows EBU short-term loudness (K-weighted, 3 s) and turns the gain down,
259
+ never up, toward -24 LUFS: up to 20 dB, cutting with a 0.25 s time constant and recovering with 1 s. A
260
+ cue is brought within a dialogue's level in about 3 s, and it adds no delay. The url has to be a
261
+ script your origin serves: a page whose CSP says `script-src 'self'` refuses a worklet from a `blob:`
262
+ or `data:` url, and `build.lib` inlines assets as `data:` without `?no-inline`.
263
+
264
+ The first time it is on, the `<video>` is routed through Web Audio for the rest of its life, because an
265
+ element cannot be handed back; off is a bypass. Routing waits until the AudioContext runs: one that
266
+ autoplay rules hold back resumes on the page's next click, tap or key press, and the video plays as it
267
+ always did until then. WebKit is refused (`createMediaElementSource` in `@banou/ponyfill/web-audio`),
268
+ since a routed element loses its volume and mute there. The refusal comes when routing would, once the
269
+ switch is on and the AudioContext runs: until then the switch reads as available, and from then on it
270
+ reads Unavailable, with the element never routed and playing as it always did.
271
+
235
272
  ## Layout
236
273
 
237
274
  One package. `src/` is the demo app, `src/lib` is the library it publishes, and the app imports it by
@@ -0,0 +1,37 @@
1
+ /** The worklet's processorOptions, read once when the node is made; its header has the defaults. */
2
+ export type VolumeNormalizerOptions = {
3
+ targetLufs?: number;
4
+ enabled?: boolean;
5
+ inputGainDb?: number;
6
+ };
7
+ /** The worklet at `workletUrl`, as a node. A context takes the module once: make a node per context. */
8
+ export declare const createVolumeNormalizerNode: (context: BaseAudioContext, workletUrl: string, options?: VolumeNormalizerOptions) => Promise<AudioWorkletNode>;
9
+ export type VolumeNormalizer = {
10
+ context: AudioContext;
11
+ /** Settles once the element is routed, which waits for the context to run. Rejects if it never can be. */
12
+ routed: Promise<AudioWorkletNode>;
13
+ /** Engage or bypass. A routed element stays routed: it cannot be handed back. */
14
+ setEnabled: (enabled: boolean) => void;
15
+ /**
16
+ * For an element going away. One still in the document has its context suspended, and attaching it again resumes
17
+ * it. One out of the document is gone for good, so its context is closed.
18
+ */
19
+ release: () => void;
20
+ };
21
+ /**
22
+ * Routes `element` through the normalizer, once for its whole life, and returns the same handle on every later
23
+ * call. Measured 2026-10-11 in Chrome 153, Firefox 151 and WebKit 26.5:
24
+ *
25
+ * - An element routed into a context that autoplay rules hold suspended goes silent (Firefox, WebKit) or stops
26
+ * advancing (Chrome). So nothing is routed until the context runs; until then the element plays as it always did,
27
+ * and the next pointerdown, pointerup or keydown resumes the context.
28
+ * - Chrome and WebKit refuse a second source for one element, and Chrome stops a playing element whose context is
29
+ * closed, so the context is closed only once the element has left the document, and off is a bypass in the same
30
+ * graph.
31
+ * - Chrome and Firefox apply the element's volume before the graph, so the worklet is told the volume and judges
32
+ * loudness without it: otherwise a viewer at 25% would read as a quiet episode and nothing would be cut.
33
+ * - WebKit loses the volume and mute once routed, so `@banou/ponyfill` refuses it and `routed` rejects with a
34
+ * `NotSupportedError` before the element is touched. That refusal comes once the context runs, the moment routing
35
+ * would happen: telling WebKit apart any earlier would take sniffing here.
36
+ */
37
+ export declare const attachVolumeNormalizer: (element: HTMLMediaElement, workletUrl: string, options?: VolumeNormalizerOptions) => VolumeNormalizer;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export type { DelegatedSelection, DelegatedTracks, ExternalThumbnails, PassThrou
5
5
  export { Player, usePlayer } from './react/player';
6
6
  export type { PlayerStore } from './react/player';
7
7
  export { sourceFeature } from './react/source-feature';
8
- export type { DownloadedRange, PassThroughControl, PlaybackErrorEntry, SourceState, ViewportBox, } from './react/source-feature';
8
+ export type { DownloadedRange, PassThroughControl, PlaybackErrorEntry, SourceState, ViewportBox, VolumeNormalizerControl, } from './react/source-feature';
9
9
  export { useSeekThumbnails } from './react/hooks/use-thumbnails';
10
10
  export { usePictureInPicture } from './react/hooks/use-picture-in-picture';
11
11
  export { inputToRemuxerInput } from './utils/source';