@banou/media-player 0.10.20 → 0.10.22
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 +34 -22
- package/dist/audio/loudness-analysis.d.ts +38 -0
- package/dist/audio/playback-tap.d.ts +11 -0
- package/dist/audio/volume-normalizer.d.ts +4 -2
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/engine/playback.d.ts +21 -0
- package/dist/{engine-C4Gj_4zs.js → engine-Cr_7OImR.js} +120 -103
- package/dist/index.d.ts +1 -1
- package/dist/index.js +929 -651
- package/dist/react/hooks/use-playback.d.ts +2 -1
- package/dist/react/hooks/use-volume-normalizer.d.ts +9 -1
- package/dist/react/video-player.d.ts +19 -7
- package/dist/utils/source.d.ts +6 -1
- package/dist/volume-normalizer-worklet.js +1200 -78
- package/package.json +3 -3
- package/src/lib/audio/loudness-analysis.ts +307 -0
- package/src/lib/audio/playback-tap.ts +37 -0
- package/src/lib/audio/volume-normalizer-worklet.d.ts +49 -0
- package/src/lib/audio/volume-normalizer-worklet.js +1200 -78
- package/src/lib/audio/volume-normalizer.ts +5 -3
- package/src/lib/engine/index.ts +1 -1
- package/src/lib/engine/playback.ts +21 -1
- package/src/lib/index.tsx +1 -0
- package/src/lib/react/hooks/use-playback.ts +5 -1
- package/src/lib/react/hooks/use-volume-normalizer.ts +40 -2
- package/src/lib/react/video-player.tsx +55 -15
- package/src/lib/utils/source.ts +6 -2
package/README.md
CHANGED
|
@@ -44,6 +44,10 @@ const source = await inputToRemuxerInput({ url: 'https://example.com/episode.mkv
|
|
|
44
44
|
const source = await inputToRemuxerInput({ length, read })
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
A URL's source comes back with `readsDownload: true`, which the player takes beside `read` and `size`: each read of
|
|
48
|
+
it is a download, so the volume normalizer then analyses the audio playback fetched rather than fetching the file
|
|
49
|
+
again.
|
|
50
|
+
|
|
47
51
|
`usePlayer` reads and drives playback state from anywhere inside a `MediaPlayer`. It is the only hook
|
|
48
52
|
the chrome uses: the built-in video.js state and this player's own source state (tracks, thumbnails,
|
|
49
53
|
indexes, readiness) live on one store, so there is no second context to reach for.
|
|
@@ -89,8 +93,8 @@ reads as a browser bug rather than a missing asset. Serve both.
|
|
|
89
93
|
|
|
90
94
|
`libavWorkerUrl` (`libav-wasm/build/worker.js`) is named individually and still copies fine.
|
|
91
95
|
|
|
92
|
-
`normalizerWorkletUrl` is this package's own `dist/volume-normalizer-worklet.js`,
|
|
93
|
-
copies fine or imports as a url; see [the volume normalizer](#the-volume-normalizer).
|
|
96
|
+
`normalizerWorkletUrl` is this package's own `dist/volume-normalizer-worklet.js`, one plain module with no imports
|
|
97
|
+
that copies fine or imports as a url; see [the volume normalizer](#the-volume-normalizer).
|
|
94
98
|
|
|
95
99
|
### jassub's assets are imported, not copied
|
|
96
100
|
|
|
@@ -241,9 +245,9 @@ until a pointer that hovers moves again.
|
|
|
241
245
|
|
|
242
246
|
### The volume normalizer
|
|
243
247
|
|
|
244
|
-
A "Normalize volume" switch in the settings menu
|
|
245
|
-
|
|
246
|
-
the viewer turns it
|
|
248
|
+
A "Normalize volume" switch in the settings menu keeps the file's own normal level and turns down what goes well over
|
|
249
|
+
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
|
|
250
|
+
worklet's url, and it is on unless the viewer turns it off or you pass `normalizeVolume={false}`:
|
|
247
251
|
|
|
248
252
|
```tsx
|
|
249
253
|
import normalizerWorkletUrl from '@banou/media-player/volume-normalizer-worklet.js?no-inline&url'
|
|
@@ -251,27 +255,35 @@ import normalizerWorkletUrl from '@banou/media-player/volume-normalizer-worklet.
|
|
|
251
255
|
<MediaPlayer
|
|
252
256
|
{...source}
|
|
253
257
|
normalizerWorkletUrl={normalizerWorkletUrl}
|
|
254
|
-
normalizeVolume={saved}
|
|
258
|
+
normalizeVolume={saved ?? true}
|
|
255
259
|
onNormalizeVolumeChange={save}
|
|
256
260
|
/>
|
|
257
261
|
```
|
|
258
262
|
|
|
259
|
-
The player saves nothing, so an app that wants the choice to stick saves what
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
It
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
263
|
+
The player saves nothing, so an app that wants the choice to stick saves what `onNormalizeVolumeChange` reports and
|
|
264
|
+
passes it back.
|
|
265
|
+
|
|
266
|
+
It works in two stages. A worker reads the file a second time, through `thumbnailRead` when you pass one and only
|
|
267
|
+
within `downloadedRanges` (for a source that `readsDownload` it reads nothing and takes the audio playback fetched,
|
|
268
|
+
about 30 s ahead of the playhead), decodes its audio with WebCodecs, learns the file's normal level (the median 400 ms
|
|
269
|
+
loudness) and computes a cut with unlimited lookahead, so a cut starts before a spike: every 10 ms of the output is
|
|
270
|
+
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
|
|
271
|
+
rest of the file, keeping only 1 ms loudness readings, never the audio. Wherever that analysis has not reached the
|
|
272
|
+
playhead, and for a track this browser's WebCodecs cannot decode, an AudioWorklet plays a live stage: a 100 ms detector
|
|
273
|
+
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.
|
|
274
|
+
Neither stage ever boosts. The sound plays 10 ms behind the picture, on or off, once routed.
|
|
275
|
+
|
|
276
|
+
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:
|
|
277
|
+
a page whose CSP says `script-src 'self'` refuses a worklet or a worker from a `blob:` or `data:` url, and
|
|
278
|
+
`build.lib` inlines assets as `data:` without `?no-inline`. A worker must also be same-origin, so serve it from the
|
|
279
|
+
page's own origin.
|
|
280
|
+
|
|
281
|
+
The first time it is on, the `<video>` is routed through Web Audio for the rest of its life, because an element cannot
|
|
282
|
+
be handed back; off is a bypass. Routing waits until the AudioContext runs: one that autoplay rules hold back resumes
|
|
283
|
+
on the page's next click, tap or key press, and the video plays as it always did until then. WebKit is refused
|
|
284
|
+
(`createMediaElementSource` in `@banou/ponyfill/web-audio`), since a routed element loses its volume and mute there.
|
|
285
|
+
The refusal comes when routing would, once the switch is on and the AudioContext runs: until then the switch reads as
|
|
286
|
+
available, and from then on it reads Unavailable, with the element never routed and playing as it always did.
|
|
275
287
|
|
|
276
288
|
## Layout
|
|
277
289
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PlaybackTap } from './playback-tap';
|
|
2
|
+
/** The file the analysis reads, the same one playback plays. */
|
|
3
|
+
export type LoudnessSource = {
|
|
4
|
+
read: (offset: number, size: number) => Promise<ArrayBuffer>;
|
|
5
|
+
length: number;
|
|
6
|
+
publicPath: string;
|
|
7
|
+
libavWorkerUrl: string;
|
|
8
|
+
/** The input stream index of the audio track playing. */
|
|
9
|
+
audioStreamIndex: number;
|
|
10
|
+
};
|
|
11
|
+
export type LoudnessAnalysis = {
|
|
12
|
+
/**
|
|
13
|
+
* Which bytes can be read without waiting on a download, as [start, end) pairs; undefined when all of them can.
|
|
14
|
+
* Nothing outside them is asked of the source.
|
|
15
|
+
*/
|
|
16
|
+
update: (ranges?: [number, number][]) => void;
|
|
17
|
+
stop: () => void;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Analyses `source` ahead of the playhead with a second remuxer, in the worker the normalizer's script starts, and
|
|
21
|
+
* hands the worklet behind `node` the cut it computes. It reads only what `update` says is there, analyses what is
|
|
22
|
+
* nearest the playhead first, then the rest of the file, and drops nothing it learned on a seek. Until a stretch is
|
|
23
|
+
* analysed, and for the whole file where this browser's WebCodecs cannot decode the track, the worklet's live stage
|
|
24
|
+
* plays.
|
|
25
|
+
*
|
|
26
|
+
* Given a `tap` instead, it reads nothing and analyses the audio playback remuxed for `audioStreamIndex`, which runs
|
|
27
|
+
* as far ahead of the playhead as playback buffers. It starts at the next fragment playback fetches, so what was
|
|
28
|
+
* buffered before it attached is not analysed.
|
|
29
|
+
*/
|
|
30
|
+
export declare const startLoudnessAnalysis: ({ node, element, scriptUrl, source }: {
|
|
31
|
+
node: AudioWorkletNode;
|
|
32
|
+
element: HTMLMediaElement;
|
|
33
|
+
scriptUrl: string;
|
|
34
|
+
source: LoudnessSource | {
|
|
35
|
+
tap: PlaybackTap;
|
|
36
|
+
audioStreamIndex: number;
|
|
37
|
+
};
|
|
38
|
+
}) => LoudnessAnalysis;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PlaybackOutput } from '../engine/playback';
|
|
2
|
+
/**
|
|
3
|
+
* Playback's output, for an analysis that reads nothing of its own: `push` takes what playback's remuxer produced,
|
|
4
|
+
* and `attach` hands a listener what follows for one input audio stream, starting with the last init segment and the
|
|
5
|
+
* run in progress. `join` marks a run some of whose fragments went by before the listener was attached.
|
|
6
|
+
*/
|
|
7
|
+
export type PlaybackTap = {
|
|
8
|
+
push: (output: PlaybackOutput) => void;
|
|
9
|
+
attach: (audioStreamIndex: number, listener: (output: PlaybackOutput, join?: boolean) => void) => () => void;
|
|
10
|
+
};
|
|
11
|
+
export declare const createPlaybackTap: () => PlaybackTap;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
/**
|
|
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
|
};
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { startPlayback, terminateRemuxer, MediaElementError, isMediaElementError, DEFAULT_BUFFER_SIZE } from './playback';
|
|
2
|
-
export type { PlaybackOptions, PlaybackController, MediaIndex, MediaChapter, AudioStream } from './playback';
|
|
2
|
+
export type { PlaybackOptions, PlaybackController, PlaybackOutput, MediaIndex, MediaChapter, AudioStream } from './playback';
|
|
3
3
|
export { createSubtitleRenderer, SUBTITLES_OFF } from './subtitles';
|
|
4
4
|
export type { SubtitleRenderer, SubtitleRendererOptions, SubtitleStream } from './subtitles';
|
|
5
5
|
export { createThumbnailGenerator } from './thumbnails';
|
package/dist/engine/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, r as c, s as l, t as u, u as d } from "../engine-
|
|
1
|
+
import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, r as c, s as l, t as u, u as d } from "../engine-Cr_7OImR.js";
|
|
2
2
|
export { i as DEFAULT_BUFFER_SIZE, e as MediaElementError, a as SUBTITLES_OFF, u as createPictureInPicture, d as createSubtitleRenderer, c as createThumbnailGenerator, n as getTimeRanges, s as isMediaElementError, o as pictureInPictureMode, l as startPlayback, t as terminateRemuxer, r as updateSourceBuffer };
|
|
@@ -21,6 +21,26 @@ export type MediaChapter = {
|
|
|
21
21
|
end: number;
|
|
22
22
|
title: string;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* What the remuxer produced, in the order it was appended: the init segment and the input audio stream it carries,
|
|
26
|
+
* a run at load and at every remuxer seek (`pts` is the timestampOffset its fragments are appended at), each
|
|
27
|
+
* fragment, and the end of the file once read. The buffers are playback's own and may be appended again, so a
|
|
28
|
+
* consumer copies what it keeps.
|
|
29
|
+
*/
|
|
30
|
+
export type PlaybackOutput = {
|
|
31
|
+
type: 'init';
|
|
32
|
+
data: ArrayBuffer;
|
|
33
|
+
audioMimeType: string;
|
|
34
|
+
audioStreamIndex: number;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'run';
|
|
37
|
+
pts: number;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'fragment';
|
|
40
|
+
data: ArrayBuffer;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'end';
|
|
43
|
+
};
|
|
24
44
|
export type PlaybackOptions = {
|
|
25
45
|
videoElement: HTMLVideoElement;
|
|
26
46
|
/**
|
|
@@ -49,6 +69,7 @@ export type PlaybackOptions = {
|
|
|
49
69
|
onSeek?: (fraction: number) => void;
|
|
50
70
|
onSubtitleStreams?: (streams: SubtitleStream[]) => void;
|
|
51
71
|
onAudioStreams?: (streams: AudioStream[], selected: number) => void;
|
|
72
|
+
onOutput?: (output: PlaybackOutput) => void;
|
|
52
73
|
};
|
|
53
74
|
export type PlaybackController = {
|
|
54
75
|
destroy: () => void;
|