@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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createMediaElementSource } from '@banou/ponyfill/web-audio'
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* The worklet's processorOptions, read once when the node is made: `enabled` false starts it bypassed, and
|
|
5
|
+
* `inputGainDb` is the gain already applied upstream (the element's volume), which loudness is judged without.
|
|
6
|
+
*/
|
|
4
7
|
export type VolumeNormalizerOptions = {
|
|
5
|
-
targetLufs?: number
|
|
6
8
|
enabled?: boolean
|
|
7
9
|
inputGainDb?: number
|
|
8
10
|
}
|
|
@@ -106,7 +108,7 @@ export const attachVolumeNormalizer = (
|
|
|
106
108
|
created.port.postMessage({ enabled, inputGainDb: inputGainDb() })
|
|
107
109
|
source.connect(created).connect(context.destination)
|
|
108
110
|
element.addEventListener('volumechange', () => created.port.postMessage({ inputGainDb: inputGainDb() }))
|
|
109
|
-
// the
|
|
111
|
+
// the live detector describes where the playhead was, and a new file owes nothing to the last one's level
|
|
110
112
|
element.addEventListener('seeking', () => created.port.postMessage({ reset: 'seek' }))
|
|
111
113
|
element.addEventListener('emptied', () => created.port.postMessage({ reset: 'source' }))
|
|
112
114
|
node = created
|
package/src/lib/engine/index.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
|
|
|
4
4
|
export { createSubtitleRenderer, SUBTITLES_OFF } from './subtitles'
|
|
5
5
|
export type { SubtitleRenderer, SubtitleRendererOptions, SubtitleStream } from './subtitles'
|
|
@@ -23,6 +23,18 @@ export type MediaIndex = { pos: number, timestamp: number }
|
|
|
23
23
|
*/
|
|
24
24
|
export type MediaChapter = { start: number, end: number, title: string }
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* What the remuxer produced, in the order it was appended: the init segment and the input audio stream it carries,
|
|
28
|
+
* a run at load and at every remuxer seek (`pts` is the timestampOffset its fragments are appended at), each
|
|
29
|
+
* fragment, and the end of the file once read. The buffers are playback's own and may be appended again, so a
|
|
30
|
+
* consumer copies what it keeps.
|
|
31
|
+
*/
|
|
32
|
+
export type PlaybackOutput =
|
|
33
|
+
| { type: 'init', data: ArrayBuffer, audioMimeType: string, audioStreamIndex: number }
|
|
34
|
+
| { type: 'run', pts: number }
|
|
35
|
+
| { type: 'fragment', data: ArrayBuffer }
|
|
36
|
+
| { type: 'end' }
|
|
37
|
+
|
|
26
38
|
export type PlaybackOptions = {
|
|
27
39
|
videoElement: HTMLVideoElement
|
|
28
40
|
/**
|
|
@@ -51,6 +63,7 @@ export type PlaybackOptions = {
|
|
|
51
63
|
onSeek?: (fraction: number) => void
|
|
52
64
|
onSubtitleStreams?: (streams: SubtitleStream[]) => void
|
|
53
65
|
onAudioStreams?: (streams: AudioStream[], selected: number) => void
|
|
66
|
+
onOutput?: (output: PlaybackOutput) => void
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
export type PlaybackController = {
|
|
@@ -146,7 +159,7 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
146
159
|
videoElement, subtitleContainer, read, length, publicPath, libavWorkerUrl,
|
|
147
160
|
jassubWorkerUrl, jassubWasmUrl, jassubLegacyWasmUrl, defaultFontUrl, bufferSize = DEFAULT_BUFFER_SIZE,
|
|
148
161
|
audioStreamIndex, onReady, onError, onRecovered, onSeek, onSubtitleStreams,
|
|
149
|
-
onAudioStreams,
|
|
162
|
+
onAudioStreams, onOutput,
|
|
150
163
|
} = options
|
|
151
164
|
|
|
152
165
|
const remuxer = await makeRemuxer({
|
|
@@ -253,6 +266,8 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
253
266
|
|
|
254
267
|
const { appendBuffer, unbufferRange, updateTimestampOffset, endOfStream } = updateSourceBuffer(sourceBuffer, mediaSource)
|
|
255
268
|
await appendBuffer(metadata.data)
|
|
269
|
+
onOutput?.({ type: 'init', data: metadata.data, audioMimeType: metadata.info.output.audioMimeType, audioStreamIndex: selectedAudio })
|
|
270
|
+
onOutput?.({ type: 'run', pts: 0 })
|
|
256
271
|
if (metadata.subtitles?.length) subtitles.pushFragments(metadata.subtitles)
|
|
257
272
|
onReady?.()
|
|
258
273
|
|
|
@@ -400,6 +415,9 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
400
415
|
if (fragments?.length) subtitles.pushFragments(fragments)
|
|
401
416
|
await updateTimestampOffset(pts)
|
|
402
417
|
if (destroyed || generation !== seekGeneration) return
|
|
418
|
+
onOutput?.({ type: 'run', pts })
|
|
419
|
+
if (data.byteLength) onOutput?.({ type: 'fragment', data })
|
|
420
|
+
if (done) onOutput?.({ type: 'end' })
|
|
403
421
|
if (data.byteLength) {
|
|
404
422
|
await appendBuffer(data)
|
|
405
423
|
trackAppendedEnd()
|
|
@@ -458,6 +476,8 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
458
476
|
if (stale()) return
|
|
459
477
|
if (done) finished = true
|
|
460
478
|
if (fragments?.length) subtitles.pushFragments(fragments)
|
|
479
|
+
if (data.byteLength) onOutput?.({ type: 'fragment', data })
|
|
480
|
+
if (done) onOutput?.({ type: 'end' })
|
|
461
481
|
if (!data.byteLength) return
|
|
462
482
|
pending = data
|
|
463
483
|
pendingAttempts = 0
|
package/src/lib/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PlaybackController } from '../../engine'
|
|
1
|
+
import type { PlaybackController, PlaybackOutput } from '../../engine'
|
|
2
2
|
import type { PlaybackErrorEntry } from '../source-feature'
|
|
3
3
|
import type { MediaPlayerLocalOptions } from '../video-player'
|
|
4
4
|
|
|
@@ -74,6 +74,7 @@ export const usePlayback = (
|
|
|
74
74
|
subtitles: HTMLElement | null,
|
|
75
75
|
/** null when the media is remote: there are no bytes, so there is no pipeline to run. */
|
|
76
76
|
options: MediaPlayerLocalOptions | null,
|
|
77
|
+
onOutput?: (output: PlaybackOutput) => void,
|
|
77
78
|
) => {
|
|
78
79
|
const player = usePlayer()
|
|
79
80
|
// The four asset urls are required on the local arm, so a default only ever applies when options is
|
|
@@ -147,6 +148,8 @@ export const usePlayback = (
|
|
|
147
148
|
onSeekRef.current = options?.onSeek
|
|
148
149
|
const onPlaybackErrorRef = useRef(options?.onPlaybackError)
|
|
149
150
|
onPlaybackErrorRef.current = options?.onPlaybackError
|
|
151
|
+
const onOutputRef = useRef(onOutput)
|
|
152
|
+
onOutputRef.current = onOutput
|
|
150
153
|
|
|
151
154
|
// The store's ids are opaque, but everything this hook writes into it came from libav, so every id
|
|
152
155
|
// that comes back is one of its own stream indices.
|
|
@@ -355,6 +358,7 @@ export const usePlayback = (
|
|
|
355
358
|
onError: fail,
|
|
356
359
|
onRecovered: () => { if (!cancelled) player.setSourceState({ playbackError: null }) },
|
|
357
360
|
onSeek: (fraction) => onSeekRef.current?.(fraction),
|
|
361
|
+
onOutput: (output) => onOutputRef.current?.(output),
|
|
358
362
|
onSubtitleStreams: (streams) => {
|
|
359
363
|
if (cancelled) return
|
|
360
364
|
player.setSourceState({
|
|
@@ -1,24 +1,39 @@
|
|
|
1
1
|
import type { VolumeNormalizer } from '../../audio/volume-normalizer'
|
|
2
|
+
import type { LoudnessSource } from '../../audio/loudness-analysis'
|
|
3
|
+
import type { PlaybackTap } from '../../audio/playback-tap'
|
|
2
4
|
|
|
3
5
|
import { useEffect, useRef, useState } from 'react'
|
|
4
6
|
|
|
5
7
|
import { attachVolumeNormalizer } from '../../audio/volume-normalizer'
|
|
8
|
+
import { startLoudnessAnalysis } from '../../audio/loudness-analysis'
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Normalizes the element's volume while `enabled`. Nothing is built until it first is, and turning it off
|
|
9
12
|
* afterwards bypasses rather than unroutes, since an element cannot be handed back. `refused` is true once the
|
|
10
13
|
* engine turned the element down, which leaves it playing exactly as before.
|
|
14
|
+
*
|
|
15
|
+
* With a `source`, the file is also analysed ahead of the playhead while it is on, reading only the byte `ranges`
|
|
16
|
+
* given (all of them when undefined), or with a `tap`, from the audio playback remuxed.
|
|
11
17
|
*/
|
|
12
|
-
export const useVolumeNormalizer = (
|
|
18
|
+
export const useVolumeNormalizer = (
|
|
19
|
+
video: HTMLVideoElement | null,
|
|
20
|
+
workletUrl: string | undefined,
|
|
21
|
+
enabled: boolean,
|
|
22
|
+
source?: LoudnessSource | { tap: PlaybackTap, audioStreamIndex: number },
|
|
23
|
+
ranges?: [number, number][],
|
|
24
|
+
) => {
|
|
13
25
|
const handle = useRef<VolumeNormalizer | null>(null)
|
|
14
26
|
const [refused, setRefused] = useState(false)
|
|
27
|
+
const [node, setNode] = useState<AudioWorkletNode | null>(null)
|
|
15
28
|
|
|
16
29
|
useEffect(() => {
|
|
17
30
|
if (!video || !workletUrl) return
|
|
18
31
|
if (enabled && !handle.current) {
|
|
19
32
|
const attached = attachVolumeNormalizer(video, workletUrl, { enabled })
|
|
20
33
|
handle.current = attached
|
|
21
|
-
attached.routed.
|
|
34
|
+
attached.routed.then((routed) => {
|
|
35
|
+
if (handle.current === attached) setNode(routed)
|
|
36
|
+
}, (error) => {
|
|
22
37
|
// released first, which closes a context that had not run yet: nothing was refused
|
|
23
38
|
if (handle.current !== attached) return
|
|
24
39
|
console.warn('[media-player] the volume normalizer is unavailable:', error)
|
|
@@ -31,7 +46,30 @@ export const useVolumeNormalizer = (video: HTMLVideoElement | null, workletUrl:
|
|
|
31
46
|
useEffect(() => () => {
|
|
32
47
|
handle.current?.release()
|
|
33
48
|
handle.current = null
|
|
49
|
+
setNode(null)
|
|
34
50
|
}, [video])
|
|
35
51
|
|
|
52
|
+
const rangesRef = useRef(ranges)
|
|
53
|
+
rangesRef.current = ranges
|
|
54
|
+
const analysis = useRef<ReturnType<typeof startLoudnessAnalysis> | null>(null)
|
|
55
|
+
const sourceRef = useRef(source)
|
|
56
|
+
sourceRef.current = source
|
|
57
|
+
const { length, publicPath, libavWorkerUrl } = source && 'read' in source ? source : {}
|
|
58
|
+
const tap = source && 'tap' in source ? source.tap : undefined
|
|
59
|
+
const audioStreamIndex = source?.audioStreamIndex
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const current = sourceRef.current
|
|
62
|
+
if (!node || !video || !workletUrl || !enabled || !current) return
|
|
63
|
+
const started = startLoudnessAnalysis({ node, element: video, scriptUrl: workletUrl, source: current })
|
|
64
|
+
started.update(rangesRef.current)
|
|
65
|
+
analysis.current = started
|
|
66
|
+
return () => {
|
|
67
|
+
started.stop()
|
|
68
|
+
analysis.current = null
|
|
69
|
+
}
|
|
70
|
+
}, [node, video, workletUrl, enabled, length, publicPath, libavWorkerUrl, audioStreamIndex, tap])
|
|
71
|
+
|
|
72
|
+
useEffect(() => { analysis.current?.update(ranges) }, [ranges])
|
|
73
|
+
|
|
36
74
|
return { refused }
|
|
37
75
|
}
|
|
@@ -15,15 +15,20 @@ import { useSeekThumbnails } from './hooks/use-thumbnails'
|
|
|
15
15
|
import { usePictureInPicture } from './hooks/use-picture-in-picture'
|
|
16
16
|
import { usePassThroughPictureInPicture } from './hooks/use-pass-through-picture-in-picture'
|
|
17
17
|
import { useVolumeNormalizer } from './hooks/use-volume-normalizer'
|
|
18
|
+
import { createPlaybackTap } from '../audio/playback-tap'
|
|
18
19
|
import Chrome from './components/chrome'
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* The source, read a range at a time: the player never downloads the whole file. Both fields travel
|
|
22
23
|
* together, so passing one without the other is a type error.
|
|
24
|
+
*
|
|
25
|
+
* `readsDownload` marks a source each read of which is a download, a file over HTTP, so the volume normalizer
|
|
26
|
+
* analyses the audio playback fetched instead of reading the file again. Without it, and with no
|
|
27
|
+
* `downloadedRanges`, every byte is taken to be readable for free, as a local file's are.
|
|
23
28
|
*/
|
|
24
29
|
export type MediaPlayerSource =
|
|
25
|
-
| { read: (offset: number, size: number) => Promise<ArrayBuffer>, size: number }
|
|
26
|
-
| { read?: undefined, size?: undefined }
|
|
30
|
+
| { read: (offset: number, size: number) => Promise<ArrayBuffer>, size: number, readsDownload?: boolean }
|
|
31
|
+
| { read?: undefined, size?: undefined, readsDownload?: undefined }
|
|
27
32
|
|
|
28
33
|
/** Shared by both arms: nothing here depends on who owns the media. */
|
|
29
34
|
type CommonOptions = {
|
|
@@ -156,11 +161,13 @@ export type MediaPlayerLocalOptions =
|
|
|
156
161
|
downloadedRanges?: DownloadedRange[]
|
|
157
162
|
|
|
158
163
|
/**
|
|
159
|
-
* Reader for the thumbnail engine, when it should differ from playback's.
|
|
164
|
+
* Reader for the thumbnail engine and the volume normalizer's analysis, when it should differ from playback's.
|
|
160
165
|
*
|
|
161
166
|
* They are the same by default so generation shares playback's fetch order. A consumer whose
|
|
162
167
|
* reads are not free wants them apart: a torrent hands this a non-prioritising, fail-fast reader
|
|
163
|
-
* so generating previews cannot steal download order from the bytes playback is blocked on.
|
|
168
|
+
* so generating previews cannot steal download order from the bytes playback is blocked on. The
|
|
169
|
+
* analysis asks it only for bytes inside `downloadedRanges`, with none given for the whole file, and for
|
|
170
|
+
* nothing when the source `readsDownload`.
|
|
164
171
|
*/
|
|
165
172
|
thumbnailRead?: (offset: number, size: number) => Promise<ArrayBuffer>
|
|
166
173
|
/** Off entirely. A second wasm worker during pipeline boot is worth avoiding on a slow source. */
|
|
@@ -176,10 +183,14 @@ export type MediaPlayerLocalOptions =
|
|
|
176
183
|
*/
|
|
177
184
|
normalizerWorkletUrl?: string
|
|
178
185
|
/**
|
|
179
|
-
* The settings menu's "Normalize volume" switch:
|
|
180
|
-
*
|
|
181
|
-
* clicks; nothing is saved, so an app that wants it to stick saves what
|
|
182
|
-
* passes it back.
|
|
186
|
+
* The settings menu's "Normalize volume" switch: keeps the file's normal level and turns down what goes well
|
|
187
|
+
* over it, a loud burst before it starts. On by default; pass false for off. The player keeps the switch's
|
|
188
|
+
* state from here and from the viewer's clicks; nothing is saved, so an app that wants it to stick saves what
|
|
189
|
+
* `onNormalizeVolumeChange` reports and passes it back.
|
|
190
|
+
*
|
|
191
|
+
* While it is on the file is read a second time, in a worker, as far as `downloadedRanges` allows, to find the
|
|
192
|
+
* loud parts ahead of the playhead; a source that `readsDownload` is not read again, and its loud parts are found
|
|
193
|
+
* in what playback fetched. The sound plays 10 ms behind its input, on or off, once routed.
|
|
183
194
|
*
|
|
184
195
|
* The first time it is on, the element is routed through Web Audio for the rest of its life (off then
|
|
185
196
|
* bypasses). If autoplay rules hold the audio back, that waits for the page's next click, tap or key press, and
|
|
@@ -188,7 +199,7 @@ export type MediaPlayerLocalOptions =
|
|
|
188
199
|
* the element is never routed.
|
|
189
200
|
*/
|
|
190
201
|
normalizeVolume?: boolean
|
|
191
|
-
/** The viewer flipped the switch
|
|
202
|
+
/** The viewer flipped the switch, to `normalizeVolume`: what an app saves to pass back as `normalizeVolume`. */
|
|
192
203
|
onNormalizeVolumeChange?: (normalizeVolume: boolean) => void
|
|
193
204
|
}
|
|
194
205
|
|
|
@@ -246,7 +257,7 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
|
|
|
246
257
|
// local arm and is absent when the media is remote.
|
|
247
258
|
const { title, chapters } = options
|
|
248
259
|
const {
|
|
249
|
-
size, downloadedRanges, publicPath, libavWorkerUrl, read, thumbnailRead, thumbnailsEnabled,
|
|
260
|
+
size, downloadedRanges, publicPath, libavWorkerUrl, read, thumbnailRead, thumbnailsEnabled, readsDownload,
|
|
250
261
|
} = local ?? ({} as Partial<MediaPlayerLocalOptions>)
|
|
251
262
|
|
|
252
263
|
const setMedia = useMediaAttach()
|
|
@@ -294,17 +305,46 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
|
|
|
294
305
|
return () => { dropped = true; stop?.() }
|
|
295
306
|
}, [media, !!expose, exposeId, exposeOrigin, exposeChannel])
|
|
296
307
|
|
|
308
|
+
// one per file, as `size` is the source's identity everywhere here
|
|
309
|
+
const tap = useMemo(() => createPlaybackTap(), [size])
|
|
310
|
+
|
|
297
311
|
// Each of these no-ops on null inputs, which is what a remote arm supplies: it renders no <video>,
|
|
298
312
|
// so there is nothing for them to attach to and nothing to guard at the call site.
|
|
299
|
-
usePlayback(video, subtitleLayer, local)
|
|
313
|
+
usePlayback(video, subtitleLayer, local, readsDownload ? tap.push : undefined)
|
|
300
314
|
|
|
301
|
-
const normalizeVolumeProp = local?.normalizeVolume
|
|
302
|
-
const [normalizeVolume, setNormalizeVolume] = useState(
|
|
303
|
-
useEffect(() => { setNormalizeVolume(
|
|
315
|
+
const normalizeVolumeProp = local?.normalizeVolume ?? true
|
|
316
|
+
const [normalizeVolume, setNormalizeVolume] = useState(normalizeVolumeProp)
|
|
317
|
+
useEffect(() => { setNormalizeVolume(normalizeVolumeProp) }, [normalizeVolumeProp])
|
|
304
318
|
const onNormalizeVolumeChange = useRef(local?.onNormalizeVolumeChange)
|
|
305
319
|
onNormalizeVolumeChange.current = local?.onNormalizeVolumeChange
|
|
306
320
|
const normalizerWorkletUrl = local?.normalizerWorkletUrl
|
|
307
|
-
|
|
321
|
+
// the background reader, through a ref: a streaming consumer passes a fresh closure several times a second
|
|
322
|
+
const analysisRead = useRef(thumbnailRead ?? read)
|
|
323
|
+
analysisRead.current = thumbnailRead ?? read
|
|
324
|
+
const playingAudio = usePlayer((state) => state.selectedAudioTrack)
|
|
325
|
+
const hasRead = !!read
|
|
326
|
+
const normalizerSource = useMemo(
|
|
327
|
+
() => hasRead && size && publicPath && libavWorkerUrl && typeof playingAudio === 'number' && playingAudio >= 0
|
|
328
|
+
? readsDownload
|
|
329
|
+
? { tap, audioStreamIndex: playingAudio }
|
|
330
|
+
: {
|
|
331
|
+
read: (offset: number, length: number) => analysisRead.current!(offset, length),
|
|
332
|
+
length: size,
|
|
333
|
+
publicPath,
|
|
334
|
+
libavWorkerUrl,
|
|
335
|
+
audioStreamIndex: playingAudio,
|
|
336
|
+
}
|
|
337
|
+
: undefined,
|
|
338
|
+
[hasRead, size, publicPath, libavWorkerUrl, playingAudio, readsDownload, tap],
|
|
339
|
+
)
|
|
340
|
+
const normalizerRanges = useMemo(
|
|
341
|
+
() => downloadedRanges?.map(({ startByteOffset, endByteOffset }) => [startByteOffset, endByteOffset] as [number, number]),
|
|
342
|
+
// a streaming consumer changes the array identity every tick, so compare contents
|
|
343
|
+
[downloadedRanges?.map(({ startByteOffset, endByteOffset }) => `${startByteOffset}-${endByteOffset}`).join(',')],
|
|
344
|
+
)
|
|
345
|
+
const { refused: normalizerRefused } = useVolumeNormalizer(
|
|
346
|
+
video, normalizerWorkletUrl, normalizeVolume, normalizerSource, normalizerRanges,
|
|
347
|
+
)
|
|
308
348
|
const volumeNormalizer = useMemo<VolumeNormalizerControl | null>(
|
|
309
349
|
() => normalizerWorkletUrl && typeof AudioWorkletNode === 'function'
|
|
310
350
|
? {
|
package/src/lib/utils/source.ts
CHANGED
|
@@ -42,14 +42,18 @@ const fromUrl = async (
|
|
|
42
42
|
if (!response.ok) throw new Error(`The source could not be read: HTTP ${response.status}`)
|
|
43
43
|
return response.arrayBuffer()
|
|
44
44
|
}
|
|
45
|
-
return { length, name, read }
|
|
45
|
+
return { length, name, read, readsDownload: true }
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* A source resolved down to what the remuxer needs: a total length and a byte-range reader, and whether each read
|
|
50
|
+
* is a download, which the player's `readsDownload` takes.
|
|
51
|
+
*/
|
|
49
52
|
export type RemuxerInput = {
|
|
50
53
|
length: number
|
|
51
54
|
name?: string
|
|
52
55
|
read: (offset: number, size: number) => Promise<ArrayBuffer>
|
|
56
|
+
readsDownload?: boolean
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
export const inputToRemuxerInput = async (
|