@remotion/media 4.0.364 → 4.0.366
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/dist/audio/audio-for-preview.js +99 -33
- package/dist/audio/audio-preview-iterator.d.ts +30 -7
- package/dist/audio/audio-preview-iterator.js +173 -20
- package/dist/audio-iterator-manager.d.ts +66 -0
- package/dist/audio-iterator-manager.js +181 -0
- package/dist/calculate-playbacktime.d.ts +5 -0
- package/dist/calculate-playbacktime.js +4 -0
- package/dist/debug-overlay/preview-overlay.d.ts +11 -5
- package/dist/debug-overlay/preview-overlay.js +37 -8
- package/dist/esm/index.mjs +1023 -550
- package/dist/media-player.d.ts +28 -34
- package/dist/media-player.js +187 -313
- package/dist/nonce-manager.d.ts +9 -0
- package/dist/nonce-manager.js +13 -0
- package/dist/video/video-for-preview.js +91 -40
- package/dist/video-iterator-manager.d.ts +37 -0
- package/dist/video-iterator-manager.js +83 -0
- package/package.json +4 -4
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { Internals, Audio as RemotionAudio, useBufferState, useCurrentFrame, } from 'remotion';
|
|
3
|
+
import { Internals, Audio as RemotionAudio, useBufferState, useCurrentFrame, useVideoConfig, } from 'remotion';
|
|
4
|
+
import { getTimeInSeconds } from '../get-time-in-seconds';
|
|
4
5
|
import { MediaPlayer } from '../media-player';
|
|
5
6
|
import { useLoopDisplay } from '../show-in-timeline';
|
|
6
7
|
import { useMediaInTimeline } from '../use-media-in-timeline';
|
|
7
8
|
const { useUnsafeVideoConfig, Timeline, SharedAudioContext, useMediaMutedState, useMediaVolumeState, useFrameForVolumeProp, evaluateVolume, warnAboutTooHighVolume, usePreload, SequenceContext, } = Internals;
|
|
8
|
-
const
|
|
9
|
+
const AudioForPreviewAssertedShowing = ({ src, playbackRate, logLevel, muted, volume, loopVolumeCurveBehavior, loop, trimAfter, trimBefore, name, showInTimeline, stack, disallowFallbackToHtml5Audio, toneFrequency, audioStreamIndex, fallbackHtml5AudioProps, }) => {
|
|
9
10
|
const videoConfig = useUnsafeVideoConfig();
|
|
10
11
|
const frame = useCurrentFrame();
|
|
11
12
|
const mediaPlayerRef = useRef(null);
|
|
@@ -37,6 +38,8 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
37
38
|
currentTimeRef.current = currentTime;
|
|
38
39
|
const preloadedSrc = usePreload(src);
|
|
39
40
|
const parentSequence = useContext(SequenceContext);
|
|
41
|
+
const isPremounting = Boolean(parentSequence?.premounting);
|
|
42
|
+
const isPostmounting = Boolean(parentSequence?.postmounting);
|
|
40
43
|
const loopDisplay = useLoopDisplay({
|
|
41
44
|
loop,
|
|
42
45
|
mediaDurationInSeconds: videoConfig.durationInFrames,
|
|
@@ -59,6 +62,11 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
59
62
|
trimAfter,
|
|
60
63
|
trimBefore,
|
|
61
64
|
});
|
|
65
|
+
const buffering = useContext(Internals.BufferingContextReact);
|
|
66
|
+
if (!buffering) {
|
|
67
|
+
throw new Error('useMediaPlayback must be used inside a <BufferingContext>');
|
|
68
|
+
}
|
|
69
|
+
const isPlayerBuffering = Internals.useIsPlayerBuffering(buffering);
|
|
62
70
|
useEffect(() => {
|
|
63
71
|
if (!sharedAudioContext)
|
|
64
72
|
return;
|
|
@@ -78,6 +86,9 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
78
86
|
audioStreamIndex: audioStreamIndex ?? 0,
|
|
79
87
|
debugOverlay: false,
|
|
80
88
|
bufferState: buffer,
|
|
89
|
+
isPostmounting,
|
|
90
|
+
isPremounting,
|
|
91
|
+
globalPlaybackRate,
|
|
81
92
|
});
|
|
82
93
|
mediaPlayerRef.current = player;
|
|
83
94
|
player
|
|
@@ -117,16 +128,16 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
117
128
|
}
|
|
118
129
|
if (result.type === 'success') {
|
|
119
130
|
setMediaPlayerReady(true);
|
|
120
|
-
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[
|
|
131
|
+
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[AudioForPreview] MediaPlayer initialized successfully`);
|
|
121
132
|
}
|
|
122
133
|
})
|
|
123
134
|
.catch((error) => {
|
|
124
|
-
Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[
|
|
135
|
+
Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[AudioForPreview] Failed to initialize MediaPlayer', error);
|
|
125
136
|
setShouldFallbackToNativeAudio(true);
|
|
126
137
|
});
|
|
127
138
|
}
|
|
128
139
|
catch (error) {
|
|
129
|
-
Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[
|
|
140
|
+
Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[AudioForPreview] MediaPlayer initialization failed', error);
|
|
130
141
|
setShouldFallbackToNativeAudio(true);
|
|
131
142
|
}
|
|
132
143
|
return () => {
|
|
@@ -135,7 +146,7 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
135
146
|
delayHandleRef.current = null;
|
|
136
147
|
}
|
|
137
148
|
if (mediaPlayerRef.current) {
|
|
138
|
-
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[
|
|
149
|
+
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[AudioForPreview] Disposing MediaPlayer`);
|
|
139
150
|
mediaPlayerRef.current.dispose();
|
|
140
151
|
mediaPlayerRef.current = null;
|
|
141
152
|
}
|
|
@@ -155,43 +166,30 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
155
166
|
audioStreamIndex,
|
|
156
167
|
disallowFallbackToHtml5Audio,
|
|
157
168
|
buffer,
|
|
169
|
+
isPremounting,
|
|
170
|
+
isPostmounting,
|
|
171
|
+
globalPlaybackRate,
|
|
158
172
|
]);
|
|
159
173
|
useEffect(() => {
|
|
160
174
|
const audioPlayer = mediaPlayerRef.current;
|
|
161
175
|
if (!audioPlayer)
|
|
162
176
|
return;
|
|
163
|
-
if (playing) {
|
|
164
|
-
audioPlayer.play(
|
|
165
|
-
Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewAudioForPreview] Failed to play', error);
|
|
166
|
-
});
|
|
177
|
+
if (playing && !isPlayerBuffering) {
|
|
178
|
+
audioPlayer.play(currentTimeRef.current);
|
|
167
179
|
}
|
|
168
180
|
else {
|
|
169
181
|
audioPlayer.pause();
|
|
170
182
|
}
|
|
171
|
-
}, [
|
|
183
|
+
}, [isPlayerBuffering, logLevel, playing]);
|
|
172
184
|
useEffect(() => {
|
|
173
185
|
const audioPlayer = mediaPlayerRef.current;
|
|
174
186
|
if (!audioPlayer || !mediaPlayerReady)
|
|
175
187
|
return;
|
|
176
|
-
audioPlayer.seekTo(currentTime)
|
|
177
|
-
|
|
178
|
-
}, [currentTime, logLevel, mediaPlayerReady]);
|
|
179
|
-
useEffect(() => {
|
|
180
|
-
const audioPlayer = mediaPlayerRef.current;
|
|
181
|
-
if (!audioPlayer || !mediaPlayerReady)
|
|
182
|
-
return;
|
|
183
|
-
audioPlayer.onBufferingChange((newBufferingState) => {
|
|
184
|
-
if (newBufferingState && !delayHandleRef.current) {
|
|
185
|
-
delayHandleRef.current = buffer.delayPlayback();
|
|
186
|
-
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[NewAudioForPreview] MediaPlayer buffering - blocking Remotion playback');
|
|
187
|
-
}
|
|
188
|
-
else if (!newBufferingState && delayHandleRef.current) {
|
|
189
|
-
delayHandleRef.current.unblock();
|
|
190
|
-
delayHandleRef.current = null;
|
|
191
|
-
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[NewAudioForPreview] MediaPlayer unbuffering - unblocking Remotion playback');
|
|
192
|
-
}
|
|
188
|
+
audioPlayer.seekTo(currentTime).catch(() => {
|
|
189
|
+
// Might be disposed
|
|
193
190
|
});
|
|
194
|
-
|
|
191
|
+
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[AudioForPreview] Updating target time to ${currentTime.toFixed(3)}s`);
|
|
192
|
+
}, [currentTime, logLevel, mediaPlayerReady]);
|
|
195
193
|
const effectiveMuted = muted || mediaMuted || userPreferredVolume <= 0;
|
|
196
194
|
useEffect(() => {
|
|
197
195
|
const audioPlayer = mediaPlayerRef.current;
|
|
@@ -206,14 +204,20 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
206
204
|
}
|
|
207
205
|
audioPlayer.setVolume(userPreferredVolume);
|
|
208
206
|
}, [userPreferredVolume, mediaPlayerReady]);
|
|
209
|
-
const effectivePlaybackRate = useMemo(() => playbackRate * globalPlaybackRate, [playbackRate, globalPlaybackRate]);
|
|
210
207
|
useEffect(() => {
|
|
211
208
|
const audioPlayer = mediaPlayerRef.current;
|
|
212
209
|
if (!audioPlayer || !mediaPlayerReady) {
|
|
213
210
|
return;
|
|
214
211
|
}
|
|
215
|
-
audioPlayer.setPlaybackRate(
|
|
216
|
-
}, [
|
|
212
|
+
audioPlayer.setPlaybackRate(playbackRate);
|
|
213
|
+
}, [playbackRate, mediaPlayerReady]);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
const audioPlayer = mediaPlayerRef.current;
|
|
216
|
+
if (!audioPlayer || !mediaPlayerReady) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
audioPlayer.setGlobalPlaybackRate(globalPlaybackRate);
|
|
220
|
+
}, [globalPlaybackRate, mediaPlayerReady]);
|
|
217
221
|
useEffect(() => {
|
|
218
222
|
const audioPlayer = mediaPlayerRef.current;
|
|
219
223
|
if (!audioPlayer || !mediaPlayerReady) {
|
|
@@ -221,6 +225,41 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
221
225
|
}
|
|
222
226
|
audioPlayer.setFps(videoConfig.fps);
|
|
223
227
|
}, [videoConfig.fps, mediaPlayerReady]);
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
const mediaPlayer = mediaPlayerRef.current;
|
|
230
|
+
if (!mediaPlayer || !mediaPlayerReady) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
mediaPlayer.setTrimBefore(trimBefore);
|
|
234
|
+
}, [trimBefore, mediaPlayerReady]);
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
const mediaPlayer = mediaPlayerRef.current;
|
|
237
|
+
if (!mediaPlayer || !mediaPlayerReady) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
mediaPlayer.setTrimAfter(trimAfter);
|
|
241
|
+
}, [trimAfter, mediaPlayerReady]);
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
const mediaPlayer = mediaPlayerRef.current;
|
|
244
|
+
if (!mediaPlayer || !mediaPlayerReady) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
mediaPlayer.setLoop(loop);
|
|
248
|
+
}, [loop, mediaPlayerReady]);
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
const mediaPlayer = mediaPlayerRef.current;
|
|
251
|
+
if (!mediaPlayer || !mediaPlayerReady) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
mediaPlayer.setIsPremounting(isPremounting);
|
|
255
|
+
}, [isPremounting, mediaPlayerReady]);
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
const mediaPlayer = mediaPlayerRef.current;
|
|
258
|
+
if (!mediaPlayer || !mediaPlayerReady) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
mediaPlayer.setIsPostmounting(isPostmounting);
|
|
262
|
+
}, [isPostmounting, mediaPlayerReady]);
|
|
224
263
|
if (shouldFallbackToNativeAudio && !disallowFallbackToHtml5Audio) {
|
|
225
264
|
return (_jsx(RemotionAudio, { src: src, muted: muted, volume: volume, startFrom: trimBefore, endAt: trimAfter, playbackRate: playbackRate, loopVolumeCurveBehavior: loopVolumeCurveBehavior, name: name, loop: loop, showInTimeline: showInTimeline, stack: stack ?? undefined, toneFrequency: toneFrequency, audioStreamIndex: audioStreamIndex, pauseWhenBuffering: fallbackHtml5AudioProps?.pauseWhenBuffering, ...fallbackHtml5AudioProps }));
|
|
226
265
|
}
|
|
@@ -228,5 +267,32 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
228
267
|
};
|
|
229
268
|
export const AudioForPreview = ({ loop, src, logLevel, muted, name, volume, loopVolumeCurveBehavior, playbackRate, trimAfter, trimBefore, showInTimeline, stack, disallowFallbackToHtml5Audio, toneFrequency, audioStreamIndex, fallbackHtml5AudioProps, }) => {
|
|
230
269
|
const preloadedSrc = usePreload(src);
|
|
231
|
-
|
|
270
|
+
const frame = useCurrentFrame();
|
|
271
|
+
const videoConfig = useVideoConfig();
|
|
272
|
+
const currentTime = frame / videoConfig.fps;
|
|
273
|
+
const showShow = useMemo(() => {
|
|
274
|
+
return (getTimeInSeconds({
|
|
275
|
+
unloopedTimeInSeconds: currentTime,
|
|
276
|
+
playbackRate: playbackRate ?? 1,
|
|
277
|
+
loop: loop ?? false,
|
|
278
|
+
trimBefore,
|
|
279
|
+
trimAfter,
|
|
280
|
+
mediaDurationInSeconds: Infinity,
|
|
281
|
+
fps: videoConfig.fps,
|
|
282
|
+
ifNoMediaDuration: 'infinity',
|
|
283
|
+
src,
|
|
284
|
+
}) !== null);
|
|
285
|
+
}, [
|
|
286
|
+
currentTime,
|
|
287
|
+
loop,
|
|
288
|
+
playbackRate,
|
|
289
|
+
src,
|
|
290
|
+
trimAfter,
|
|
291
|
+
trimBefore,
|
|
292
|
+
videoConfig.fps,
|
|
293
|
+
]);
|
|
294
|
+
if (!showShow) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
return (_jsx(AudioForPreviewAssertedShowing, { audioStreamIndex: audioStreamIndex ?? 0, src: preloadedSrc, playbackRate: playbackRate ?? 1, logLevel: logLevel ?? window.remotion_logLevel, muted: muted ?? false, volume: volume ?? 1, loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? 'repeat', loop: loop ?? false, trimAfter: trimAfter, trimBefore: trimBefore, name: name, showInTimeline: showInTimeline ?? true, stack: stack, disallowFallbackToHtml5Audio: disallowFallbackToHtml5Audio ?? false, toneFrequency: toneFrequency, fallbackHtml5AudioProps: fallbackHtml5AudioProps }));
|
|
232
298
|
};
|
|
@@ -1,14 +1,37 @@
|
|
|
1
|
-
import type { AudioBufferSink } from 'mediabunny';
|
|
1
|
+
import type { AudioBufferSink, WrappedAudioBuffer } from 'mediabunny';
|
|
2
2
|
export declare const HEALTHY_BUFFER_THRESHOLD_SECONDS = 1;
|
|
3
|
+
export type QueuedNode = {
|
|
4
|
+
node: AudioBufferSourceNode;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
buffer: AudioBuffer;
|
|
7
|
+
};
|
|
3
8
|
export declare const makeAudioIterator: (audioSink: AudioBufferSink, startFromSecond: number) => {
|
|
4
|
-
cleanupAudioQueue: () => void;
|
|
5
9
|
destroy: () => void;
|
|
6
|
-
|
|
7
|
-
setAudioIteratorStarted: (started: boolean) => void;
|
|
8
|
-
getNext: () => Promise<IteratorResult<import("mediabunny").WrappedAudioBuffer, void>>;
|
|
9
|
-
setAudioBufferHealth: (health: number) => void;
|
|
10
|
+
getNext: () => Promise<IteratorResult<WrappedAudioBuffer, void>>;
|
|
10
11
|
isDestroyed: () => boolean;
|
|
11
|
-
addQueuedAudioNode: (node: AudioBufferSourceNode) => void;
|
|
12
|
+
addQueuedAudioNode: (node: AudioBufferSourceNode, timestamp: number, buffer: AudioBuffer) => void;
|
|
12
13
|
removeQueuedAudioNode: (node: AudioBufferSourceNode) => void;
|
|
14
|
+
getAndClearAudioChunksForAfterResuming: () => {
|
|
15
|
+
buffer: AudioBuffer;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
}[];
|
|
18
|
+
getQueuedPeriod: (pendingBuffers: WrappedAudioBuffer[]) => {
|
|
19
|
+
from: number;
|
|
20
|
+
until: number;
|
|
21
|
+
} | null;
|
|
22
|
+
tryToSatisfySeek: (time: number, allowWait: boolean) => Promise<{
|
|
23
|
+
type: "not-satisfied";
|
|
24
|
+
reason: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: "satisfied";
|
|
27
|
+
buffers: WrappedAudioBuffer[];
|
|
28
|
+
}>;
|
|
29
|
+
addChunkForAfterResuming: (buffer: AudioBuffer, timestamp: number) => void;
|
|
30
|
+
moveQueuedChunksToPauseQueue: () => void;
|
|
31
|
+
getNumberOfChunksAfterResuming: () => number;
|
|
13
32
|
};
|
|
14
33
|
export type AudioIterator = ReturnType<typeof makeAudioIterator>;
|
|
34
|
+
export declare const isAlreadyQueued: (time: number, queuedPeriod: {
|
|
35
|
+
from: number;
|
|
36
|
+
until: number;
|
|
37
|
+
} | undefined | null) => boolean;
|
|
@@ -1,43 +1,196 @@
|
|
|
1
|
+
import { roundTo4Digits } from '../helpers/round-to-4-digits';
|
|
1
2
|
export const HEALTHY_BUFFER_THRESHOLD_SECONDS = 1;
|
|
2
3
|
export const makeAudioIterator = (audioSink, startFromSecond) => {
|
|
3
4
|
let destroyed = false;
|
|
4
5
|
const iterator = audioSink.buffers(startFromSecond);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const queuedAudioNodes = new Set();
|
|
6
|
+
const queuedAudioNodes = [];
|
|
7
|
+
const audioChunksForAfterResuming = [];
|
|
8
8
|
const cleanupAudioQueue = () => {
|
|
9
9
|
for (const node of queuedAudioNodes) {
|
|
10
|
-
node.stop();
|
|
10
|
+
node.node.stop();
|
|
11
11
|
}
|
|
12
|
-
queuedAudioNodes.
|
|
12
|
+
queuedAudioNodes.length = 0;
|
|
13
|
+
};
|
|
14
|
+
let lastReturnedBuffer = null;
|
|
15
|
+
let iteratorEnded = false;
|
|
16
|
+
const getNextOrNullIfNotAvailable = async (allowWait) => {
|
|
17
|
+
const next = iterator.next();
|
|
18
|
+
const result = allowWait
|
|
19
|
+
? await next
|
|
20
|
+
: await Promise.race([
|
|
21
|
+
next,
|
|
22
|
+
new Promise((resolve) => {
|
|
23
|
+
Promise.resolve().then(() => resolve());
|
|
24
|
+
}),
|
|
25
|
+
]);
|
|
26
|
+
if (!result) {
|
|
27
|
+
return {
|
|
28
|
+
type: 'need-to-wait-for-it',
|
|
29
|
+
waitPromise: async () => {
|
|
30
|
+
const res = await next;
|
|
31
|
+
if (res.value) {
|
|
32
|
+
lastReturnedBuffer = res.value;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
iteratorEnded = true;
|
|
36
|
+
}
|
|
37
|
+
return res.value;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (result.value) {
|
|
42
|
+
lastReturnedBuffer = result.value;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
iteratorEnded = true;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
type: 'got-buffer-or-end',
|
|
49
|
+
buffer: result.value ?? null,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
const tryToSatisfySeek = async (time, allowWait) => {
|
|
53
|
+
if (lastReturnedBuffer) {
|
|
54
|
+
const bufferTimestamp = roundTo4Digits(lastReturnedBuffer.timestamp);
|
|
55
|
+
const bufferEndTimestamp = roundTo4Digits(lastReturnedBuffer.timestamp + lastReturnedBuffer.duration);
|
|
56
|
+
if (roundTo4Digits(time) < bufferTimestamp) {
|
|
57
|
+
return {
|
|
58
|
+
type: 'not-satisfied',
|
|
59
|
+
reason: `iterator is too far, most recently returned ${bufferTimestamp}-${bufferEndTimestamp}, requested ${time}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (roundTo4Digits(time) <= bufferEndTimestamp) {
|
|
63
|
+
return {
|
|
64
|
+
type: 'satisfied',
|
|
65
|
+
buffers: [lastReturnedBuffer],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// fall through
|
|
69
|
+
}
|
|
70
|
+
if (iteratorEnded) {
|
|
71
|
+
return {
|
|
72
|
+
type: 'satisfied',
|
|
73
|
+
buffers: lastReturnedBuffer ? [lastReturnedBuffer] : [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const toBeReturned = [];
|
|
77
|
+
while (true) {
|
|
78
|
+
const buffer = await getNextOrNullIfNotAvailable(allowWait);
|
|
79
|
+
if (buffer.type === 'need-to-wait-for-it') {
|
|
80
|
+
return {
|
|
81
|
+
type: 'not-satisfied',
|
|
82
|
+
reason: 'iterator did not have buffer ready',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (buffer.type === 'got-buffer-or-end') {
|
|
86
|
+
if (buffer.buffer === null) {
|
|
87
|
+
iteratorEnded = true;
|
|
88
|
+
return {
|
|
89
|
+
type: 'satisfied',
|
|
90
|
+
buffers: lastReturnedBuffer ? [lastReturnedBuffer] : [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const bufferTimestamp = roundTo4Digits(buffer.buffer.timestamp);
|
|
94
|
+
const bufferEndTimestamp = roundTo4Digits(buffer.buffer.timestamp + buffer.buffer.duration);
|
|
95
|
+
const timestamp = roundTo4Digits(time);
|
|
96
|
+
if (bufferTimestamp <= timestamp && bufferEndTimestamp > timestamp) {
|
|
97
|
+
return {
|
|
98
|
+
type: 'satisfied',
|
|
99
|
+
buffers: [...toBeReturned, buffer.buffer],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
toBeReturned.push(buffer.buffer);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
throw new Error('Unreachable');
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const removeAndReturnAllQueuedAudioNodes = () => {
|
|
109
|
+
const nodes = queuedAudioNodes.slice();
|
|
110
|
+
for (const node of nodes) {
|
|
111
|
+
node.node.stop();
|
|
112
|
+
}
|
|
113
|
+
queuedAudioNodes.length = 0;
|
|
114
|
+
return nodes;
|
|
115
|
+
};
|
|
116
|
+
const addChunkForAfterResuming = (buffer, timestamp) => {
|
|
117
|
+
audioChunksForAfterResuming.push({ buffer, timestamp });
|
|
118
|
+
};
|
|
119
|
+
const moveQueuedChunksToPauseQueue = () => {
|
|
120
|
+
const toQueue = removeAndReturnAllQueuedAudioNodes();
|
|
121
|
+
for (const chunk of toQueue) {
|
|
122
|
+
addChunkForAfterResuming(chunk.buffer, chunk.timestamp);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const getNumberOfChunksAfterResuming = () => {
|
|
126
|
+
return audioChunksForAfterResuming.length;
|
|
13
127
|
};
|
|
14
128
|
return {
|
|
15
|
-
cleanupAudioQueue,
|
|
16
129
|
destroy: () => {
|
|
17
130
|
cleanupAudioQueue();
|
|
18
131
|
destroyed = true;
|
|
19
132
|
iterator.return().catch(() => undefined);
|
|
133
|
+
audioChunksForAfterResuming.length = 0;
|
|
20
134
|
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
setAudioBufferHealth: (health) => {
|
|
31
|
-
audioBufferHealth = health;
|
|
135
|
+
getNext: async () => {
|
|
136
|
+
const next = await iterator.next();
|
|
137
|
+
if (next.value) {
|
|
138
|
+
lastReturnedBuffer = next.value;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
iteratorEnded = true;
|
|
142
|
+
}
|
|
143
|
+
return next;
|
|
32
144
|
},
|
|
33
145
|
isDestroyed: () => {
|
|
34
146
|
return destroyed;
|
|
35
147
|
},
|
|
36
|
-
addQueuedAudioNode: (node) => {
|
|
37
|
-
queuedAudioNodes.
|
|
148
|
+
addQueuedAudioNode: (node, timestamp, buffer) => {
|
|
149
|
+
queuedAudioNodes.push({ node, timestamp, buffer });
|
|
38
150
|
},
|
|
39
151
|
removeQueuedAudioNode: (node) => {
|
|
40
|
-
queuedAudioNodes.
|
|
152
|
+
const index = queuedAudioNodes.findIndex((n) => n.node === node);
|
|
153
|
+
if (index !== -1) {
|
|
154
|
+
queuedAudioNodes.splice(index, 1);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
getAndClearAudioChunksForAfterResuming: () => {
|
|
158
|
+
const chunks = audioChunksForAfterResuming.slice();
|
|
159
|
+
audioChunksForAfterResuming.length = 0;
|
|
160
|
+
return chunks;
|
|
41
161
|
},
|
|
162
|
+
getQueuedPeriod: (pendingBuffers) => {
|
|
163
|
+
let until = -Infinity;
|
|
164
|
+
let from = Infinity;
|
|
165
|
+
for (const buffer of pendingBuffers) {
|
|
166
|
+
until = Math.max(until, buffer.timestamp + buffer.duration);
|
|
167
|
+
from = Math.min(from, buffer.timestamp);
|
|
168
|
+
}
|
|
169
|
+
for (const node of queuedAudioNodes) {
|
|
170
|
+
until = Math.max(until, node.timestamp + node.buffer.duration);
|
|
171
|
+
from = Math.min(from, node.timestamp);
|
|
172
|
+
}
|
|
173
|
+
for (const chunk of audioChunksForAfterResuming) {
|
|
174
|
+
until = Math.max(until, chunk.timestamp + chunk.buffer.duration);
|
|
175
|
+
from = Math.min(from, chunk.timestamp);
|
|
176
|
+
}
|
|
177
|
+
if (!Number.isFinite(from) || !Number.isFinite(until)) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
from,
|
|
182
|
+
until,
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
tryToSatisfySeek,
|
|
186
|
+
addChunkForAfterResuming,
|
|
187
|
+
moveQueuedChunksToPauseQueue,
|
|
188
|
+
getNumberOfChunksAfterResuming,
|
|
42
189
|
};
|
|
43
190
|
};
|
|
191
|
+
export const isAlreadyQueued = (time, queuedPeriod) => {
|
|
192
|
+
if (!queuedPeriod) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return time >= queuedPeriod.from && time < queuedPeriod.until;
|
|
196
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { InputAudioTrack, WrappedAudioBuffer } from 'mediabunny';
|
|
2
|
+
import type { Nonce } from './nonce-manager';
|
|
3
|
+
export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfNotPremounting, sharedAudioContext, }: {
|
|
4
|
+
audioTrack: InputAudioTrack;
|
|
5
|
+
delayPlaybackHandleIfNotPremounting: () => {
|
|
6
|
+
unblock: () => void;
|
|
7
|
+
};
|
|
8
|
+
sharedAudioContext: AudioContext;
|
|
9
|
+
}) => {
|
|
10
|
+
startAudioIterator: ({ nonce, playbackRate, startFromSecond, getIsPlaying, scheduleAudioNode, }: {
|
|
11
|
+
startFromSecond: number;
|
|
12
|
+
nonce: Nonce;
|
|
13
|
+
playbackRate: number;
|
|
14
|
+
getIsPlaying: () => boolean;
|
|
15
|
+
scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
|
|
16
|
+
}) => Promise<void>;
|
|
17
|
+
resumeScheduledAudioChunks: ({ playbackRate, scheduleAudioNode, }: {
|
|
18
|
+
playbackRate: number;
|
|
19
|
+
scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
|
|
20
|
+
}) => void;
|
|
21
|
+
pausePlayback: () => void;
|
|
22
|
+
getAudioBufferIterator: () => {
|
|
23
|
+
destroy: () => void;
|
|
24
|
+
getNext: () => Promise<IteratorResult<WrappedAudioBuffer, void>>;
|
|
25
|
+
isDestroyed: () => boolean;
|
|
26
|
+
addQueuedAudioNode: (node: AudioBufferSourceNode, timestamp: number, buffer: AudioBuffer) => void;
|
|
27
|
+
removeQueuedAudioNode: (node: AudioBufferSourceNode) => void;
|
|
28
|
+
getAndClearAudioChunksForAfterResuming: () => {
|
|
29
|
+
buffer: AudioBuffer;
|
|
30
|
+
timestamp: number;
|
|
31
|
+
}[];
|
|
32
|
+
getQueuedPeriod: (pendingBuffers: WrappedAudioBuffer[]) => {
|
|
33
|
+
from: number;
|
|
34
|
+
until: number;
|
|
35
|
+
} | null;
|
|
36
|
+
tryToSatisfySeek: (time: number, allowWait: boolean) => Promise<{
|
|
37
|
+
type: "not-satisfied";
|
|
38
|
+
reason: string;
|
|
39
|
+
} | {
|
|
40
|
+
type: "satisfied";
|
|
41
|
+
buffers: WrappedAudioBuffer[];
|
|
42
|
+
}>;
|
|
43
|
+
addChunkForAfterResuming: (buffer: AudioBuffer, timestamp: number) => void;
|
|
44
|
+
moveQueuedChunksToPauseQueue: () => void;
|
|
45
|
+
getNumberOfChunksAfterResuming: () => number;
|
|
46
|
+
} | null;
|
|
47
|
+
destroy: () => void;
|
|
48
|
+
seek: ({ newTime, nonce, fps, playbackRate, getIsPlaying, scheduleAudioNode, }: {
|
|
49
|
+
newTime: number;
|
|
50
|
+
nonce: Nonce;
|
|
51
|
+
fps: number;
|
|
52
|
+
playbackRate: number;
|
|
53
|
+
getIsPlaying: () => boolean;
|
|
54
|
+
scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
|
|
55
|
+
}) => Promise<void>;
|
|
56
|
+
getAudioIteratorsCreated: () => number;
|
|
57
|
+
setMuted: (newMuted: boolean) => void;
|
|
58
|
+
setVolume: (volume: number) => void;
|
|
59
|
+
scheduleAudioChunk: ({ buffer, mediaTimestamp, playbackRate, scheduleAudioNode, }: {
|
|
60
|
+
buffer: AudioBuffer;
|
|
61
|
+
mediaTimestamp: number;
|
|
62
|
+
playbackRate: number;
|
|
63
|
+
scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
|
|
64
|
+
}) => void;
|
|
65
|
+
};
|
|
66
|
+
export type AudioIteratorManager = ReturnType<typeof audioIteratorManager>;
|