@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 +37 -0
- package/dist/audio/volume-normalizer.d.ts +37 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +598 -485
- package/dist/react/components/icons.d.ts +1 -4
- package/dist/react/hooks/use-volume-normalizer.d.ts +8 -0
- package/dist/react/player.d.ts +1 -0
- package/dist/react/source-feature.d.ts +11 -0
- package/dist/react/video-player.d.ts +24 -0
- package/dist/volume-normalizer-worklet.js +162 -0
- package/package.json +5 -3
- package/src/lib/audio/volume-normalizer-worklet.js +162 -0
- package/src/lib/audio/volume-normalizer.ts +147 -0
- package/src/lib/index.tsx +1 -1
- package/src/lib/react/components/control-bar.tsx +1 -1
- package/src/lib/react/components/errors.tsx +2 -2
- package/src/lib/react/components/icons.tsx +6 -23
- package/src/lib/react/components/settings.tsx +57 -1
- package/src/lib/react/components/sound.tsx +1 -1
- package/src/lib/react/components/subtitles.tsx +1 -1
- package/src/lib/react/components/track-menu.tsx +1 -1
- package/src/lib/react/hooks/use-volume-normalizer.ts +37 -0
- package/src/lib/react/source-feature.ts +12 -0
- package/src/lib/react/video-player.tsx +53 -2
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';
|