@remotion/media 4.0.388 → 4.0.391
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 +2 -0
- package/dist/audio/audio-preview-iterator.d.ts +3 -2
- package/dist/audio/audio-preview-iterator.js +2 -2
- package/dist/audio-iterator-manager.d.ts +5 -1
- package/dist/audio-iterator-manager.js +21 -5
- package/dist/esm/index.mjs +201 -41
- package/dist/get-time-in-seconds.d.ts +8 -0
- package/dist/get-time-in-seconds.js +15 -0
- package/dist/media-player.d.ts +4 -1
- package/dist/media-player.js +71 -30
- package/dist/prewarm-iterator-for-looping.d.ts +17 -0
- package/dist/prewarm-iterator-for-looping.js +56 -0
- package/dist/video/video-for-preview.js +2 -0
- package/dist/video/video-preview-iterator.d.ts +3 -2
- package/dist/video/video-preview-iterator.js +2 -2
- package/dist/video-iterator-manager.d.ts +4 -1
- package/dist/video-iterator-manager.js +28 -8
- package/package.json +3 -3
- package/dist/is-network-error.d.ts +0 -6
- package/dist/is-network-error.js +0 -17
|
@@ -69,6 +69,7 @@ const AudioForPreviewAssertedShowing = ({ src, playbackRate, logLevel, muted, vo
|
|
|
69
69
|
throw new Error('useMediaPlayback must be used inside a <BufferingContext>');
|
|
70
70
|
}
|
|
71
71
|
const isPlayerBuffering = Internals.useIsPlayerBuffering(bufferingContext);
|
|
72
|
+
const initialPlaying = useRef(playing && !isPlayerBuffering);
|
|
72
73
|
const initialIsPremounting = useRef(isPremounting);
|
|
73
74
|
const initialIsPostmounting = useRef(isPostmounting);
|
|
74
75
|
const initialGlobalPlaybackRate = useRef(globalPlaybackRate);
|
|
@@ -96,6 +97,7 @@ const AudioForPreviewAssertedShowing = ({ src, playbackRate, logLevel, muted, vo
|
|
|
96
97
|
isPremounting: initialIsPremounting.current,
|
|
97
98
|
globalPlaybackRate: initialGlobalPlaybackRate.current,
|
|
98
99
|
onVideoFrameCallback: null,
|
|
100
|
+
playing: initialPlaying.current,
|
|
99
101
|
});
|
|
100
102
|
mediaPlayerRef.current = player;
|
|
101
103
|
player
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { WrappedAudioBuffer } from 'mediabunny';
|
|
2
|
+
import type { PrewarmedAudioIteratorCache } from '../prewarm-iterator-for-looping';
|
|
2
3
|
import { type AllowWait } from './allow-wait';
|
|
3
4
|
export declare const HEALTHY_BUFFER_THRESHOLD_SECONDS = 1;
|
|
4
5
|
export type QueuedNode = {
|
|
@@ -6,7 +7,7 @@ export type QueuedNode = {
|
|
|
6
7
|
timestamp: number;
|
|
7
8
|
buffer: AudioBuffer;
|
|
8
9
|
};
|
|
9
|
-
export declare const makeAudioIterator: (
|
|
10
|
+
export declare const makeAudioIterator: (startFromSecond: number, cache: PrewarmedAudioIteratorCache) => {
|
|
10
11
|
destroy: () => void;
|
|
11
12
|
getNext: () => Promise<IteratorResult<WrappedAudioBuffer, void>>;
|
|
12
13
|
isDestroyed: () => boolean;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { roundTo4Digits } from '../helpers/round-to-4-digits';
|
|
2
2
|
import { allowWaitRoutine } from './allow-wait';
|
|
3
3
|
export const HEALTHY_BUFFER_THRESHOLD_SECONDS = 1;
|
|
4
|
-
export const makeAudioIterator = (
|
|
4
|
+
export const makeAudioIterator = (startFromSecond, cache) => {
|
|
5
5
|
let destroyed = false;
|
|
6
|
-
const iterator =
|
|
6
|
+
const iterator = cache.makeIteratorOrUsePrewarmed(startFromSecond);
|
|
7
7
|
const queuedAudioNodes = [];
|
|
8
8
|
const audioChunksForAfterResuming = [];
|
|
9
9
|
let mostRecentTimestamp = -Infinity;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { InputAudioTrack, WrappedAudioBuffer } from 'mediabunny';
|
|
2
2
|
import type { useBufferState } from 'remotion';
|
|
3
3
|
import type { Nonce } from './nonce-manager';
|
|
4
|
-
export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfNotPremounting, sharedAudioContext, }: {
|
|
4
|
+
export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfNotPremounting, sharedAudioContext, getIsLooping, getEndTime, getStartTime, updatePlaybackTime, }: {
|
|
5
5
|
audioTrack: InputAudioTrack;
|
|
6
6
|
delayPlaybackHandleIfNotPremounting: () => {
|
|
7
7
|
unblock: () => void;
|
|
8
8
|
};
|
|
9
9
|
sharedAudioContext: AudioContext;
|
|
10
|
+
getIsLooping: () => boolean;
|
|
11
|
+
getEndTime: () => number;
|
|
12
|
+
getStartTime: () => number;
|
|
13
|
+
updatePlaybackTime: (time: number) => void;
|
|
10
14
|
}) => {
|
|
11
15
|
startAudioIterator: ({ nonce, playbackRate, startFromSecond, getIsPlaying, scheduleAudioNode, }: {
|
|
12
16
|
startFromSecond: number;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { AudioBufferSink, InputDisposedError } from 'mediabunny';
|
|
2
2
|
import { isAlreadyQueued, makeAudioIterator, } from './audio/audio-preview-iterator';
|
|
3
|
-
|
|
3
|
+
import { makePrewarmedAudioIteratorCache } from './prewarm-iterator-for-looping';
|
|
4
|
+
export const audioIteratorManager = ({ audioTrack, delayPlaybackHandleIfNotPremounting, sharedAudioContext, getIsLooping, getEndTime, getStartTime, updatePlaybackTime, }) => {
|
|
4
5
|
let muted = false;
|
|
5
6
|
let currentVolume = 1;
|
|
6
7
|
const gainNode = sharedAudioContext.createGain();
|
|
7
8
|
gainNode.connect(sharedAudioContext.destination);
|
|
8
9
|
const audioSink = new AudioBufferSink(audioTrack);
|
|
10
|
+
const prewarmedAudioIteratorCache = makePrewarmedAudioIteratorCache(audioSink);
|
|
9
11
|
let audioBufferIterator = null;
|
|
10
12
|
let audioIteratorsCreated = 0;
|
|
13
|
+
let currentDelayHandle = null;
|
|
11
14
|
const scheduleAudioChunk = ({ buffer, mediaTimestamp, playbackRate, scheduleAudioNode, }) => {
|
|
12
15
|
if (!audioBufferIterator) {
|
|
13
16
|
throw new Error('Audio buffer iterator not found');
|
|
@@ -43,9 +46,11 @@ export const audioIteratorManager = ({ audioTrack, delayPlaybackHandleIfNotPremo
|
|
|
43
46
|
}
|
|
44
47
|
};
|
|
45
48
|
const startAudioIterator = async ({ nonce, playbackRate, startFromSecond, getIsPlaying, scheduleAudioNode, }) => {
|
|
49
|
+
updatePlaybackTime(startFromSecond);
|
|
46
50
|
audioBufferIterator?.destroy();
|
|
47
51
|
const delayHandle = delayPlaybackHandleIfNotPremounting();
|
|
48
|
-
|
|
52
|
+
currentDelayHandle = delayHandle;
|
|
53
|
+
const iterator = makeAudioIterator(startFromSecond, prewarmedAudioIteratorCache);
|
|
49
54
|
audioIteratorsCreated++;
|
|
50
55
|
audioBufferIterator = iterator;
|
|
51
56
|
try {
|
|
@@ -53,16 +58,13 @@ export const audioIteratorManager = ({ audioTrack, delayPlaybackHandleIfNotPremo
|
|
|
53
58
|
for (let i = 0; i < 3; i++) {
|
|
54
59
|
const result = await iterator.getNext();
|
|
55
60
|
if (iterator.isDestroyed()) {
|
|
56
|
-
delayHandle.unblock();
|
|
57
61
|
return;
|
|
58
62
|
}
|
|
59
63
|
if (nonce.isStale()) {
|
|
60
|
-
delayHandle.unblock();
|
|
61
64
|
return;
|
|
62
65
|
}
|
|
63
66
|
if (!result.value) {
|
|
64
67
|
// media ended
|
|
65
|
-
delayHandle.unblock();
|
|
66
68
|
return;
|
|
67
69
|
}
|
|
68
70
|
onAudioChunk({
|
|
@@ -83,6 +85,7 @@ export const audioIteratorManager = ({ audioTrack, delayPlaybackHandleIfNotPremo
|
|
|
83
85
|
}
|
|
84
86
|
finally {
|
|
85
87
|
delayHandle.unblock();
|
|
88
|
+
currentDelayHandle = null;
|
|
86
89
|
}
|
|
87
90
|
};
|
|
88
91
|
const pausePlayback = () => {
|
|
@@ -92,6 +95,14 @@ export const audioIteratorManager = ({ audioTrack, delayPlaybackHandleIfNotPremo
|
|
|
92
95
|
audioBufferIterator.moveQueuedChunksToPauseQueue();
|
|
93
96
|
};
|
|
94
97
|
const seek = async ({ newTime, nonce, fps, playbackRate, getIsPlaying, scheduleAudioNode, bufferState, }) => {
|
|
98
|
+
if (getIsLooping()) {
|
|
99
|
+
// If less than 1 second from the end away, we pre-warm a new iterator
|
|
100
|
+
if (getEndTime() - newTime < 1) {
|
|
101
|
+
prewarmedAudioIteratorCache.prewarmIteratorForLooping({
|
|
102
|
+
timeToSeek: getStartTime(),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
95
106
|
if (!audioBufferIterator) {
|
|
96
107
|
await startAudioIterator({
|
|
97
108
|
nonce,
|
|
@@ -194,8 +205,13 @@ export const audioIteratorManager = ({ audioTrack, delayPlaybackHandleIfNotPremo
|
|
|
194
205
|
pausePlayback,
|
|
195
206
|
getAudioBufferIterator: () => audioBufferIterator,
|
|
196
207
|
destroyIterator: () => {
|
|
208
|
+
prewarmedAudioIteratorCache.destroy();
|
|
197
209
|
audioBufferIterator?.destroy();
|
|
198
210
|
audioBufferIterator = null;
|
|
211
|
+
if (currentDelayHandle) {
|
|
212
|
+
currentDelayHandle.unblock();
|
|
213
|
+
currentDelayHandle = null;
|
|
214
|
+
}
|
|
199
215
|
},
|
|
200
216
|
seek,
|
|
201
217
|
getAudioIteratorsCreated: () => audioIteratorsCreated,
|
package/dist/esm/index.mjs
CHANGED
|
@@ -49,6 +49,25 @@ var getTimeInSeconds = ({
|
|
|
49
49
|
}
|
|
50
50
|
return timeInSeconds + (trimBefore ?? 0) / fps;
|
|
51
51
|
};
|
|
52
|
+
var calculateEndTime = ({
|
|
53
|
+
mediaDurationInSeconds,
|
|
54
|
+
ifNoMediaDuration,
|
|
55
|
+
src,
|
|
56
|
+
trimAfter,
|
|
57
|
+
trimBefore,
|
|
58
|
+
fps
|
|
59
|
+
}) => {
|
|
60
|
+
if (mediaDurationInSeconds === null && ifNoMediaDuration === "fail") {
|
|
61
|
+
throw new Error(`Could not determine duration of ${src}, but "loop" was set.`);
|
|
62
|
+
}
|
|
63
|
+
const mediaDuration = Internals.calculateMediaDuration({
|
|
64
|
+
trimAfter,
|
|
65
|
+
mediaDurationInFrames: mediaDurationInSeconds ? mediaDurationInSeconds * fps : Infinity,
|
|
66
|
+
playbackRate: 1,
|
|
67
|
+
trimBefore
|
|
68
|
+
}) / fps;
|
|
69
|
+
return mediaDuration + (trimBefore ?? 0) / fps;
|
|
70
|
+
};
|
|
52
71
|
|
|
53
72
|
// src/media-player.ts
|
|
54
73
|
import { ALL_FORMATS, Input, UrlSource } from "mediabunny";
|
|
@@ -80,9 +99,9 @@ var allowWaitRoutine = async (next, waitFn) => {
|
|
|
80
99
|
};
|
|
81
100
|
|
|
82
101
|
// src/audio/audio-preview-iterator.ts
|
|
83
|
-
var makeAudioIterator = (
|
|
102
|
+
var makeAudioIterator = (startFromSecond, cache) => {
|
|
84
103
|
let destroyed = false;
|
|
85
|
-
const iterator =
|
|
104
|
+
const iterator = cache.makeIteratorOrUsePrewarmed(startFromSecond);
|
|
86
105
|
const queuedAudioNodes = [];
|
|
87
106
|
const audioChunksForAfterResuming = [];
|
|
88
107
|
let mostRecentTimestamp = -Infinity;
|
|
@@ -254,19 +273,83 @@ var isAlreadyQueued = (time, queuedPeriod) => {
|
|
|
254
273
|
return time >= queuedPeriod.from && time < queuedPeriod.until;
|
|
255
274
|
};
|
|
256
275
|
|
|
276
|
+
// src/prewarm-iterator-for-looping.ts
|
|
277
|
+
var makePrewarmedVideoIteratorCache = (videoSink) => {
|
|
278
|
+
const prewarmedVideoIterators = new Map;
|
|
279
|
+
const prewarmIteratorForLooping = ({ timeToSeek }) => {
|
|
280
|
+
if (!prewarmedVideoIterators.has(timeToSeek)) {
|
|
281
|
+
prewarmedVideoIterators.set(timeToSeek, videoSink.canvases(timeToSeek));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const makeIteratorOrUsePrewarmed = (timeToSeek) => {
|
|
285
|
+
const prewarmedIterator = prewarmedVideoIterators.get(timeToSeek);
|
|
286
|
+
if (prewarmedIterator) {
|
|
287
|
+
prewarmedVideoIterators.delete(timeToSeek);
|
|
288
|
+
return prewarmedIterator;
|
|
289
|
+
}
|
|
290
|
+
const iterator = videoSink.canvases(timeToSeek);
|
|
291
|
+
return iterator;
|
|
292
|
+
};
|
|
293
|
+
const destroy = () => {
|
|
294
|
+
for (const iterator of prewarmedVideoIterators.values()) {
|
|
295
|
+
iterator.return();
|
|
296
|
+
}
|
|
297
|
+
prewarmedVideoIterators.clear();
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
prewarmIteratorForLooping,
|
|
301
|
+
makeIteratorOrUsePrewarmed,
|
|
302
|
+
destroy
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
var makePrewarmedAudioIteratorCache = (audioSink) => {
|
|
306
|
+
const prewarmedAudioIterators = new Map;
|
|
307
|
+
const prewarmIteratorForLooping = ({ timeToSeek }) => {
|
|
308
|
+
if (!prewarmedAudioIterators.has(timeToSeek)) {
|
|
309
|
+
prewarmedAudioIterators.set(timeToSeek, audioSink.buffers(timeToSeek));
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const makeIteratorOrUsePrewarmed = (timeToSeek) => {
|
|
313
|
+
const prewarmedIterator = prewarmedAudioIterators.get(timeToSeek);
|
|
314
|
+
if (prewarmedIterator) {
|
|
315
|
+
prewarmedAudioIterators.delete(timeToSeek);
|
|
316
|
+
return prewarmedIterator;
|
|
317
|
+
}
|
|
318
|
+
const iterator = audioSink.buffers(timeToSeek);
|
|
319
|
+
return iterator;
|
|
320
|
+
};
|
|
321
|
+
const destroy = () => {
|
|
322
|
+
for (const iterator of prewarmedAudioIterators.values()) {
|
|
323
|
+
iterator.return();
|
|
324
|
+
}
|
|
325
|
+
prewarmedAudioIterators.clear();
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
prewarmIteratorForLooping,
|
|
329
|
+
makeIteratorOrUsePrewarmed,
|
|
330
|
+
destroy
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
|
|
257
334
|
// src/audio-iterator-manager.ts
|
|
258
335
|
var audioIteratorManager = ({
|
|
259
336
|
audioTrack,
|
|
260
337
|
delayPlaybackHandleIfNotPremounting,
|
|
261
|
-
sharedAudioContext
|
|
338
|
+
sharedAudioContext,
|
|
339
|
+
getIsLooping,
|
|
340
|
+
getEndTime,
|
|
341
|
+
getStartTime,
|
|
342
|
+
updatePlaybackTime
|
|
262
343
|
}) => {
|
|
263
344
|
let muted = false;
|
|
264
345
|
let currentVolume = 1;
|
|
265
346
|
const gainNode = sharedAudioContext.createGain();
|
|
266
347
|
gainNode.connect(sharedAudioContext.destination);
|
|
267
348
|
const audioSink = new AudioBufferSink(audioTrack);
|
|
349
|
+
const prewarmedAudioIteratorCache = makePrewarmedAudioIteratorCache(audioSink);
|
|
268
350
|
let audioBufferIterator = null;
|
|
269
351
|
let audioIteratorsCreated = 0;
|
|
352
|
+
let currentDelayHandle = null;
|
|
270
353
|
const scheduleAudioChunk = ({
|
|
271
354
|
buffer,
|
|
272
355
|
mediaTimestamp,
|
|
@@ -316,24 +399,23 @@ var audioIteratorManager = ({
|
|
|
316
399
|
getIsPlaying,
|
|
317
400
|
scheduleAudioNode
|
|
318
401
|
}) => {
|
|
402
|
+
updatePlaybackTime(startFromSecond);
|
|
319
403
|
audioBufferIterator?.destroy();
|
|
320
404
|
const delayHandle = delayPlaybackHandleIfNotPremounting();
|
|
321
|
-
|
|
405
|
+
currentDelayHandle = delayHandle;
|
|
406
|
+
const iterator = makeAudioIterator(startFromSecond, prewarmedAudioIteratorCache);
|
|
322
407
|
audioIteratorsCreated++;
|
|
323
408
|
audioBufferIterator = iterator;
|
|
324
409
|
try {
|
|
325
410
|
for (let i = 0;i < 3; i++) {
|
|
326
411
|
const result = await iterator.getNext();
|
|
327
412
|
if (iterator.isDestroyed()) {
|
|
328
|
-
delayHandle.unblock();
|
|
329
413
|
return;
|
|
330
414
|
}
|
|
331
415
|
if (nonce.isStale()) {
|
|
332
|
-
delayHandle.unblock();
|
|
333
416
|
return;
|
|
334
417
|
}
|
|
335
418
|
if (!result.value) {
|
|
336
|
-
delayHandle.unblock();
|
|
337
419
|
return;
|
|
338
420
|
}
|
|
339
421
|
onAudioChunk({
|
|
@@ -350,6 +432,7 @@ var audioIteratorManager = ({
|
|
|
350
432
|
throw e;
|
|
351
433
|
} finally {
|
|
352
434
|
delayHandle.unblock();
|
|
435
|
+
currentDelayHandle = null;
|
|
353
436
|
}
|
|
354
437
|
};
|
|
355
438
|
const pausePlayback = () => {
|
|
@@ -367,6 +450,13 @@ var audioIteratorManager = ({
|
|
|
367
450
|
scheduleAudioNode,
|
|
368
451
|
bufferState
|
|
369
452
|
}) => {
|
|
453
|
+
if (getIsLooping()) {
|
|
454
|
+
if (getEndTime() - newTime < 1) {
|
|
455
|
+
prewarmedAudioIteratorCache.prewarmIteratorForLooping({
|
|
456
|
+
timeToSeek: getStartTime()
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
370
460
|
if (!audioBufferIterator) {
|
|
371
461
|
await startAudioIterator({
|
|
372
462
|
nonce,
|
|
@@ -467,8 +557,13 @@ var audioIteratorManager = ({
|
|
|
467
557
|
pausePlayback,
|
|
468
558
|
getAudioBufferIterator: () => audioBufferIterator,
|
|
469
559
|
destroyIterator: () => {
|
|
560
|
+
prewarmedAudioIteratorCache.destroy();
|
|
470
561
|
audioBufferIterator?.destroy();
|
|
471
562
|
audioBufferIterator = null;
|
|
563
|
+
if (currentDelayHandle) {
|
|
564
|
+
currentDelayHandle.unblock();
|
|
565
|
+
currentDelayHandle = null;
|
|
566
|
+
}
|
|
472
567
|
},
|
|
473
568
|
seek,
|
|
474
569
|
getAudioIteratorsCreated: () => audioIteratorsCreated,
|
|
@@ -570,9 +665,9 @@ import { CanvasSink } from "mediabunny";
|
|
|
570
665
|
import { Internals as Internals2 } from "remotion";
|
|
571
666
|
|
|
572
667
|
// src/video/video-preview-iterator.ts
|
|
573
|
-
var createVideoIterator = (timeToSeek,
|
|
668
|
+
var createVideoIterator = (timeToSeek, cache) => {
|
|
574
669
|
let destroyed = false;
|
|
575
|
-
const iterator =
|
|
670
|
+
const iterator = cache.makeIteratorOrUsePrewarmed(timeToSeek);
|
|
576
671
|
let lastReturnedFrame = null;
|
|
577
672
|
let iteratorEnded = false;
|
|
578
673
|
const getNextOrNullIfNotAvailable = async () => {
|
|
@@ -700,11 +795,15 @@ var videoIteratorManager = ({
|
|
|
700
795
|
drawDebugOverlay,
|
|
701
796
|
logLevel,
|
|
702
797
|
getOnVideoFrameCallback,
|
|
703
|
-
videoTrack
|
|
798
|
+
videoTrack,
|
|
799
|
+
getEndTime,
|
|
800
|
+
getStartTime,
|
|
801
|
+
getIsLooping
|
|
704
802
|
}) => {
|
|
705
803
|
let videoIteratorsCreated = 0;
|
|
706
804
|
let videoFrameIterator = null;
|
|
707
805
|
let framesRendered = 0;
|
|
806
|
+
let currentDelayHandle = null;
|
|
708
807
|
if (canvas) {
|
|
709
808
|
canvas.width = videoTrack.displayWidth;
|
|
710
809
|
canvas.height = videoTrack.displayHeight;
|
|
@@ -714,6 +813,7 @@ var videoIteratorManager = ({
|
|
|
714
813
|
fit: "contain",
|
|
715
814
|
alpha: true
|
|
716
815
|
});
|
|
816
|
+
const prewarmedVideoIteratorCache = makePrewarmedVideoIteratorCache(canvasSink);
|
|
717
817
|
const drawFrame = (frame) => {
|
|
718
818
|
if (context && canvas) {
|
|
719
819
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
@@ -729,12 +829,18 @@ var videoIteratorManager = ({
|
|
|
729
829
|
};
|
|
730
830
|
const startVideoIterator = async (timeToSeek, nonce) => {
|
|
731
831
|
videoFrameIterator?.destroy();
|
|
732
|
-
const iterator = createVideoIterator(timeToSeek,
|
|
832
|
+
const iterator = createVideoIterator(timeToSeek, prewarmedVideoIteratorCache);
|
|
733
833
|
videoIteratorsCreated++;
|
|
734
834
|
videoFrameIterator = iterator;
|
|
735
835
|
const delayHandle = delayPlaybackHandleIfNotPremounting();
|
|
736
|
-
|
|
737
|
-
|
|
836
|
+
currentDelayHandle = delayHandle;
|
|
837
|
+
let frameResult;
|
|
838
|
+
try {
|
|
839
|
+
frameResult = await iterator.getNext();
|
|
840
|
+
} finally {
|
|
841
|
+
delayHandle.unblock();
|
|
842
|
+
currentDelayHandle = null;
|
|
843
|
+
}
|
|
738
844
|
if (iterator.isDestroyed()) {
|
|
739
845
|
return;
|
|
740
846
|
}
|
|
@@ -753,6 +859,13 @@ var videoIteratorManager = ({
|
|
|
753
859
|
if (!videoFrameIterator) {
|
|
754
860
|
return;
|
|
755
861
|
}
|
|
862
|
+
if (getIsLooping()) {
|
|
863
|
+
if (getEndTime() - newTime < 1) {
|
|
864
|
+
prewarmedVideoIteratorCache.prewarmIteratorForLooping({
|
|
865
|
+
timeToSeek: getStartTime()
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
756
869
|
const videoSatisfyResult = await videoFrameIterator.tryToSatisfySeek(newTime);
|
|
757
870
|
if (videoSatisfyResult.type === "satisfied") {
|
|
758
871
|
drawFrame(videoSatisfyResult.frame);
|
|
@@ -761,17 +874,22 @@ var videoIteratorManager = ({
|
|
|
761
874
|
if (nonce.isStale()) {
|
|
762
875
|
return;
|
|
763
876
|
}
|
|
764
|
-
startVideoIterator(newTime, nonce)
|
|
877
|
+
await startVideoIterator(newTime, nonce);
|
|
765
878
|
};
|
|
766
879
|
return {
|
|
767
880
|
startVideoIterator,
|
|
768
881
|
getVideoIteratorsCreated: () => videoIteratorsCreated,
|
|
769
882
|
seek,
|
|
770
883
|
destroy: () => {
|
|
884
|
+
prewarmedVideoIteratorCache.destroy();
|
|
771
885
|
videoFrameIterator?.destroy();
|
|
772
886
|
if (context && canvas) {
|
|
773
887
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
774
888
|
}
|
|
889
|
+
if (currentDelayHandle) {
|
|
890
|
+
currentDelayHandle.unblock();
|
|
891
|
+
currentDelayHandle = null;
|
|
892
|
+
}
|
|
775
893
|
videoFrameIterator = null;
|
|
776
894
|
},
|
|
777
895
|
getVideoFrameIterator: () => videoFrameIterator,
|
|
@@ -823,7 +941,8 @@ class MediaPlayer {
|
|
|
823
941
|
bufferState,
|
|
824
942
|
isPremounting,
|
|
825
943
|
isPostmounting,
|
|
826
|
-
onVideoFrameCallback
|
|
944
|
+
onVideoFrameCallback,
|
|
945
|
+
playing
|
|
827
946
|
}) {
|
|
828
947
|
this.canvas = canvas ?? null;
|
|
829
948
|
this.src = src;
|
|
@@ -842,6 +961,7 @@ class MediaPlayer {
|
|
|
842
961
|
this.isPostmounting = isPostmounting;
|
|
843
962
|
this.nonceManager = makeNonceManager();
|
|
844
963
|
this.onVideoFrameCallback = onVideoFrameCallback;
|
|
964
|
+
this.playing = playing;
|
|
845
965
|
this.input = new Input({
|
|
846
966
|
source: new UrlSource(this.src),
|
|
847
967
|
formats: ALL_FORMATS
|
|
@@ -866,9 +986,24 @@ class MediaPlayer {
|
|
|
866
986
|
initialize(startTimeUnresolved) {
|
|
867
987
|
const promise = this._initialize(startTimeUnresolved);
|
|
868
988
|
this.initializationPromise = promise;
|
|
989
|
+
this.seekPromiseChain = promise;
|
|
869
990
|
return promise;
|
|
870
991
|
}
|
|
992
|
+
getStartTime() {
|
|
993
|
+
return (this.trimBefore ?? 0) / this.fps;
|
|
994
|
+
}
|
|
995
|
+
getEndTime() {
|
|
996
|
+
return calculateEndTime({
|
|
997
|
+
mediaDurationInSeconds: this.totalDuration,
|
|
998
|
+
ifNoMediaDuration: "fail",
|
|
999
|
+
src: this.src,
|
|
1000
|
+
trimAfter: this.trimAfter,
|
|
1001
|
+
trimBefore: this.trimBefore,
|
|
1002
|
+
fps: this.fps
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
871
1005
|
async _initialize(startTimeUnresolved) {
|
|
1006
|
+
const delayHandle = this.delayPlaybackHandleIfNotPremounting();
|
|
872
1007
|
try {
|
|
873
1008
|
if (this.input.disposed) {
|
|
874
1009
|
return { type: "disposed" };
|
|
@@ -914,7 +1049,10 @@ class MediaPlayer {
|
|
|
914
1049
|
canvas: this.canvas,
|
|
915
1050
|
getOnVideoFrameCallback: () => this.onVideoFrameCallback,
|
|
916
1051
|
logLevel: this.logLevel,
|
|
917
|
-
drawDebugOverlay: this.drawDebugOverlay
|
|
1052
|
+
drawDebugOverlay: this.drawDebugOverlay,
|
|
1053
|
+
getEndTime: () => this.getEndTime(),
|
|
1054
|
+
getStartTime: () => this.getStartTime(),
|
|
1055
|
+
getIsLooping: () => this.loop
|
|
918
1056
|
});
|
|
919
1057
|
}
|
|
920
1058
|
const startTime = getTimeInSeconds({
|
|
@@ -936,21 +1074,25 @@ class MediaPlayer {
|
|
|
936
1074
|
this.audioIteratorManager = audioIteratorManager({
|
|
937
1075
|
audioTrack,
|
|
938
1076
|
delayPlaybackHandleIfNotPremounting: this.delayPlaybackHandleIfNotPremounting,
|
|
939
|
-
sharedAudioContext: this.sharedAudioContext
|
|
1077
|
+
sharedAudioContext: this.sharedAudioContext,
|
|
1078
|
+
getIsLooping: () => this.loop,
|
|
1079
|
+
getEndTime: () => this.getEndTime(),
|
|
1080
|
+
getStartTime: () => this.getStartTime(),
|
|
1081
|
+
updatePlaybackTime: (time) => this.setPlaybackTime(time, this.playbackRate * this.globalPlaybackRate)
|
|
940
1082
|
});
|
|
941
1083
|
}
|
|
942
1084
|
const nonce = this.nonceManager.createAsyncOperation();
|
|
943
1085
|
try {
|
|
944
|
-
|
|
945
|
-
this.audioIteratorManager.startAudioIterator({
|
|
1086
|
+
await Promise.all([
|
|
1087
|
+
this.audioIteratorManager ? this.audioIteratorManager.startAudioIterator({
|
|
946
1088
|
nonce,
|
|
947
1089
|
playbackRate: this.playbackRate * this.globalPlaybackRate,
|
|
948
1090
|
startFromSecond: startTime,
|
|
949
1091
|
getIsPlaying: () => this.playing,
|
|
950
1092
|
scheduleAudioNode: this.scheduleAudioNode
|
|
951
|
-
})
|
|
952
|
-
|
|
953
|
-
|
|
1093
|
+
}) : Promise.resolve(),
|
|
1094
|
+
this.videoIteratorManager ? this.videoIteratorManager.startVideoIterator(startTime, nonce) : Promise.resolve()
|
|
1095
|
+
]);
|
|
954
1096
|
} catch (error) {
|
|
955
1097
|
if (this.isDisposalError()) {
|
|
956
1098
|
return { type: "disposed" };
|
|
@@ -966,6 +1108,8 @@ class MediaPlayer {
|
|
|
966
1108
|
}
|
|
967
1109
|
Internals3.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, "[MediaPlayer] Failed to initialize", error);
|
|
968
1110
|
throw error;
|
|
1111
|
+
} finally {
|
|
1112
|
+
delayHandle.unblock();
|
|
969
1113
|
}
|
|
970
1114
|
}
|
|
971
1115
|
async seekTo(time) {
|
|
@@ -996,21 +1140,26 @@ class MediaPlayer {
|
|
|
996
1140
|
if (currentPlaybackTime === newTime) {
|
|
997
1141
|
return;
|
|
998
1142
|
}
|
|
999
|
-
await
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1143
|
+
await Promise.all([
|
|
1144
|
+
this.videoIteratorManager?.seek({
|
|
1145
|
+
newTime,
|
|
1146
|
+
nonce
|
|
1147
|
+
}),
|
|
1148
|
+
this.audioIteratorManager?.seek({
|
|
1149
|
+
newTime,
|
|
1150
|
+
nonce,
|
|
1151
|
+
fps: this.fps,
|
|
1152
|
+
playbackRate: this.playbackRate * this.globalPlaybackRate,
|
|
1153
|
+
getIsPlaying: () => this.playing,
|
|
1154
|
+
scheduleAudioNode: this.scheduleAudioNode,
|
|
1155
|
+
bufferState: this.bufferState
|
|
1156
|
+
})
|
|
1157
|
+
]);
|
|
1012
1158
|
}
|
|
1013
1159
|
async play(time) {
|
|
1160
|
+
if (this.playing) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1014
1163
|
const newTime = getTimeInSeconds({
|
|
1015
1164
|
unloopedTimeInSeconds: time,
|
|
1016
1165
|
playbackRate: this.playbackRate,
|
|
@@ -1047,6 +1196,9 @@ class MediaPlayer {
|
|
|
1047
1196
|
return this.bufferState.delayPlayback();
|
|
1048
1197
|
};
|
|
1049
1198
|
pause() {
|
|
1199
|
+
if (!this.playing) {
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1050
1202
|
this.playing = false;
|
|
1051
1203
|
this.audioIteratorManager?.pausePlayback();
|
|
1052
1204
|
this.drawDebugOverlay();
|
|
@@ -1081,12 +1233,16 @@ class MediaPlayer {
|
|
|
1081
1233
|
this.audioIteratorManager?.destroyIterator();
|
|
1082
1234
|
}
|
|
1083
1235
|
setTrimBefore(trimBefore, unloopedTimeInSeconds) {
|
|
1084
|
-
this.trimBefore
|
|
1085
|
-
|
|
1236
|
+
if (this.trimBefore !== trimBefore) {
|
|
1237
|
+
this.trimBefore = trimBefore;
|
|
1238
|
+
this.updateAfterTrimChange(unloopedTimeInSeconds);
|
|
1239
|
+
}
|
|
1086
1240
|
}
|
|
1087
1241
|
setTrimAfter(trimAfter, unloopedTimeInSeconds) {
|
|
1088
|
-
this.trimAfter
|
|
1089
|
-
|
|
1242
|
+
if (this.trimAfter !== trimAfter) {
|
|
1243
|
+
this.trimAfter = trimAfter;
|
|
1244
|
+
this.updateAfterTrimChange(unloopedTimeInSeconds);
|
|
1245
|
+
}
|
|
1090
1246
|
}
|
|
1091
1247
|
setDebugOverlay(debugOverlay) {
|
|
1092
1248
|
this.debugOverlay = debugOverlay;
|
|
@@ -1437,6 +1593,7 @@ var AudioForPreviewAssertedShowing = ({
|
|
|
1437
1593
|
throw new Error("useMediaPlayback must be used inside a <BufferingContext>");
|
|
1438
1594
|
}
|
|
1439
1595
|
const isPlayerBuffering = Internals6.useIsPlayerBuffering(bufferingContext);
|
|
1596
|
+
const initialPlaying = useRef(playing && !isPlayerBuffering);
|
|
1440
1597
|
const initialIsPremounting = useRef(isPremounting);
|
|
1441
1598
|
const initialIsPostmounting = useRef(isPostmounting);
|
|
1442
1599
|
const initialGlobalPlaybackRate = useRef(globalPlaybackRate);
|
|
@@ -1463,7 +1620,8 @@ var AudioForPreviewAssertedShowing = ({
|
|
|
1463
1620
|
isPostmounting: initialIsPostmounting.current,
|
|
1464
1621
|
isPremounting: initialIsPremounting.current,
|
|
1465
1622
|
globalPlaybackRate: initialGlobalPlaybackRate.current,
|
|
1466
|
-
onVideoFrameCallback: null
|
|
1623
|
+
onVideoFrameCallback: null,
|
|
1624
|
+
playing: initialPlaying.current
|
|
1467
1625
|
});
|
|
1468
1626
|
mediaPlayerRef.current = player;
|
|
1469
1627
|
player.initialize(currentTimeRef.current).then((result) => {
|
|
@@ -3776,6 +3934,7 @@ var VideoForPreviewAssertedShowing = ({
|
|
|
3776
3934
|
throw new Error("useMediaPlayback must be used inside a <BufferingContext>");
|
|
3777
3935
|
}
|
|
3778
3936
|
const isPlayerBuffering = Internals15.useIsPlayerBuffering(buffering);
|
|
3937
|
+
const initialPlaying = useRef2(playing && !isPlayerBuffering);
|
|
3779
3938
|
const initialIsPremounting = useRef2(isPremounting);
|
|
3780
3939
|
const initialIsPostmounting = useRef2(isPostmounting);
|
|
3781
3940
|
const initialGlobalPlaybackRate = useRef2(globalPlaybackRate);
|
|
@@ -3802,7 +3961,8 @@ var VideoForPreviewAssertedShowing = ({
|
|
|
3802
3961
|
isPremounting: initialIsPremounting.current,
|
|
3803
3962
|
isPostmounting: initialIsPostmounting.current,
|
|
3804
3963
|
globalPlaybackRate: initialGlobalPlaybackRate.current,
|
|
3805
|
-
onVideoFrameCallback: initialOnVideoFrameRef.current ?? null
|
|
3964
|
+
onVideoFrameCallback: initialOnVideoFrameRef.current ?? null,
|
|
3965
|
+
playing: initialPlaying.current
|
|
3806
3966
|
});
|
|
3807
3967
|
mediaPlayerRef.current = player;
|
|
3808
3968
|
player.initialize(currentTimeRef.current).then((result) => {
|
|
@@ -9,3 +9,11 @@ export declare const getTimeInSeconds: ({ loop, mediaDurationInSeconds, unlooped
|
|
|
9
9
|
fps: number;
|
|
10
10
|
ifNoMediaDuration: "fail" | "infinity";
|
|
11
11
|
}) => number | null;
|
|
12
|
+
export declare const calculateEndTime: ({ mediaDurationInSeconds, ifNoMediaDuration, src, trimAfter, trimBefore, fps, }: {
|
|
13
|
+
mediaDurationInSeconds: number | null;
|
|
14
|
+
ifNoMediaDuration: "fail" | "infinity";
|
|
15
|
+
src: string;
|
|
16
|
+
trimAfter: number | undefined;
|
|
17
|
+
trimBefore: number | undefined;
|
|
18
|
+
fps: number;
|
|
19
|
+
}) => number;
|
|
@@ -23,3 +23,18 @@ export const getTimeInSeconds = ({ loop, mediaDurationInSeconds, unloopedTimeInS
|
|
|
23
23
|
}
|
|
24
24
|
return timeInSeconds + (trimBefore ?? 0) / fps;
|
|
25
25
|
};
|
|
26
|
+
export const calculateEndTime = ({ mediaDurationInSeconds, ifNoMediaDuration, src, trimAfter, trimBefore, fps, }) => {
|
|
27
|
+
if (mediaDurationInSeconds === null && ifNoMediaDuration === 'fail') {
|
|
28
|
+
throw new Error(`Could not determine duration of ${src}, but "loop" was set.`);
|
|
29
|
+
}
|
|
30
|
+
const mediaDuration = Internals.calculateMediaDuration({
|
|
31
|
+
trimAfter,
|
|
32
|
+
mediaDurationInFrames: mediaDurationInSeconds
|
|
33
|
+
? mediaDurationInSeconds * fps
|
|
34
|
+
: Infinity,
|
|
35
|
+
// Playback rate was already specified before
|
|
36
|
+
playbackRate: 1,
|
|
37
|
+
trimBefore,
|
|
38
|
+
}) / fps;
|
|
39
|
+
return mediaDuration + (trimBefore ?? 0) / fps;
|
|
40
|
+
};
|
package/dist/media-player.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare class MediaPlayer {
|
|
|
42
42
|
private isPremounting;
|
|
43
43
|
private isPostmounting;
|
|
44
44
|
private seekPromiseChain;
|
|
45
|
-
constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBefore, trimAfter, playbackRate, globalPlaybackRate, audioStreamIndex, fps, debugOverlay, bufferState, isPremounting, isPostmounting, onVideoFrameCallback, }: {
|
|
45
|
+
constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBefore, trimAfter, playbackRate, globalPlaybackRate, audioStreamIndex, fps, debugOverlay, bufferState, isPremounting, isPostmounting, onVideoFrameCallback, playing, }: {
|
|
46
46
|
canvas: HTMLCanvasElement | OffscreenCanvas | null;
|
|
47
47
|
src: string;
|
|
48
48
|
logLevel: LogLevel;
|
|
@@ -59,10 +59,13 @@ export declare class MediaPlayer {
|
|
|
59
59
|
isPremounting: boolean;
|
|
60
60
|
isPostmounting: boolean;
|
|
61
61
|
onVideoFrameCallback: null | ((frame: CanvasImageSource) => void);
|
|
62
|
+
playing: boolean;
|
|
62
63
|
});
|
|
63
64
|
private input;
|
|
64
65
|
private isDisposalError;
|
|
65
66
|
initialize(startTimeUnresolved: number): Promise<MediaPlayerInitResult>;
|
|
67
|
+
private getStartTime;
|
|
68
|
+
private getEndTime;
|
|
66
69
|
private _initialize;
|
|
67
70
|
seekTo(time: number): Promise<void>;
|
|
68
71
|
seekToDoNotCallDirectly(newTime: number, nonce: Nonce): Promise<void>;
|
package/dist/media-player.js
CHANGED
|
@@ -3,12 +3,12 @@ import { Internals } from 'remotion';
|
|
|
3
3
|
import { audioIteratorManager, } from './audio-iterator-manager';
|
|
4
4
|
import { calculatePlaybackTime } from './calculate-playbacktime';
|
|
5
5
|
import { drawPreviewOverlay } from './debug-overlay/preview-overlay';
|
|
6
|
-
import { getTimeInSeconds } from './get-time-in-seconds';
|
|
6
|
+
import { calculateEndTime, getTimeInSeconds } from './get-time-in-seconds';
|
|
7
7
|
import { isNetworkError } from './is-type-of-error';
|
|
8
8
|
import { makeNonceManager } from './nonce-manager';
|
|
9
9
|
import { videoIteratorManager } from './video-iterator-manager';
|
|
10
10
|
export class MediaPlayer {
|
|
11
|
-
constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBefore, trimAfter, playbackRate, globalPlaybackRate, audioStreamIndex, fps, debugOverlay, bufferState, isPremounting, isPostmounting, onVideoFrameCallback, }) {
|
|
11
|
+
constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBefore, trimAfter, playbackRate, globalPlaybackRate, audioStreamIndex, fps, debugOverlay, bufferState, isPremounting, isPostmounting, onVideoFrameCallback, playing, }) {
|
|
12
12
|
this.audioIteratorManager = null;
|
|
13
13
|
this.videoIteratorManager = null;
|
|
14
14
|
// this is the time difference between Web Audio timeline
|
|
@@ -71,6 +71,7 @@ export class MediaPlayer {
|
|
|
71
71
|
this.isPostmounting = isPostmounting;
|
|
72
72
|
this.nonceManager = makeNonceManager();
|
|
73
73
|
this.onVideoFrameCallback = onVideoFrameCallback;
|
|
74
|
+
this.playing = playing;
|
|
74
75
|
this.input = new Input({
|
|
75
76
|
source: new UrlSource(this.src),
|
|
76
77
|
formats: ALL_FORMATS,
|
|
@@ -95,9 +96,24 @@ export class MediaPlayer {
|
|
|
95
96
|
initialize(startTimeUnresolved) {
|
|
96
97
|
const promise = this._initialize(startTimeUnresolved);
|
|
97
98
|
this.initializationPromise = promise;
|
|
99
|
+
this.seekPromiseChain = promise;
|
|
98
100
|
return promise;
|
|
99
101
|
}
|
|
102
|
+
getStartTime() {
|
|
103
|
+
return (this.trimBefore ?? 0) / this.fps;
|
|
104
|
+
}
|
|
105
|
+
getEndTime() {
|
|
106
|
+
return calculateEndTime({
|
|
107
|
+
mediaDurationInSeconds: this.totalDuration,
|
|
108
|
+
ifNoMediaDuration: 'fail',
|
|
109
|
+
src: this.src,
|
|
110
|
+
trimAfter: this.trimAfter,
|
|
111
|
+
trimBefore: this.trimBefore,
|
|
112
|
+
fps: this.fps,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
100
115
|
async _initialize(startTimeUnresolved) {
|
|
116
|
+
const delayHandle = this.delayPlaybackHandleIfNotPremounting();
|
|
101
117
|
try {
|
|
102
118
|
if (this.input.disposed) {
|
|
103
119
|
return { type: 'disposed' };
|
|
@@ -145,6 +161,9 @@ export class MediaPlayer {
|
|
|
145
161
|
getOnVideoFrameCallback: () => this.onVideoFrameCallback,
|
|
146
162
|
logLevel: this.logLevel,
|
|
147
163
|
drawDebugOverlay: this.drawDebugOverlay,
|
|
164
|
+
getEndTime: () => this.getEndTime(),
|
|
165
|
+
getStartTime: () => this.getStartTime(),
|
|
166
|
+
getIsLooping: () => this.loop,
|
|
148
167
|
});
|
|
149
168
|
}
|
|
150
169
|
const startTime = getTimeInSeconds({
|
|
@@ -167,21 +186,28 @@ export class MediaPlayer {
|
|
|
167
186
|
audioTrack,
|
|
168
187
|
delayPlaybackHandleIfNotPremounting: this.delayPlaybackHandleIfNotPremounting,
|
|
169
188
|
sharedAudioContext: this.sharedAudioContext,
|
|
189
|
+
getIsLooping: () => this.loop,
|
|
190
|
+
getEndTime: () => this.getEndTime(),
|
|
191
|
+
getStartTime: () => this.getStartTime(),
|
|
192
|
+
updatePlaybackTime: (time) => this.setPlaybackTime(time, this.playbackRate * this.globalPlaybackRate),
|
|
170
193
|
});
|
|
171
194
|
}
|
|
172
195
|
const nonce = this.nonceManager.createAsyncOperation();
|
|
173
196
|
try {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
197
|
+
await Promise.all([
|
|
198
|
+
this.audioIteratorManager
|
|
199
|
+
? this.audioIteratorManager.startAudioIterator({
|
|
200
|
+
nonce,
|
|
201
|
+
playbackRate: this.playbackRate * this.globalPlaybackRate,
|
|
202
|
+
startFromSecond: startTime,
|
|
203
|
+
getIsPlaying: () => this.playing,
|
|
204
|
+
scheduleAudioNode: this.scheduleAudioNode,
|
|
205
|
+
})
|
|
206
|
+
: Promise.resolve(),
|
|
207
|
+
this.videoIteratorManager
|
|
208
|
+
? this.videoIteratorManager.startVideoIterator(startTime, nonce)
|
|
209
|
+
: Promise.resolve(),
|
|
210
|
+
]);
|
|
185
211
|
}
|
|
186
212
|
catch (error) {
|
|
187
213
|
if (this.isDisposalError()) {
|
|
@@ -200,6 +226,9 @@ export class MediaPlayer {
|
|
|
200
226
|
Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to initialize', error);
|
|
201
227
|
throw error;
|
|
202
228
|
}
|
|
229
|
+
finally {
|
|
230
|
+
delayHandle.unblock();
|
|
231
|
+
}
|
|
203
232
|
}
|
|
204
233
|
async seekTo(time) {
|
|
205
234
|
const newTime = getTimeInSeconds({
|
|
@@ -229,21 +258,26 @@ export class MediaPlayer {
|
|
|
229
258
|
if (currentPlaybackTime === newTime) {
|
|
230
259
|
return;
|
|
231
260
|
}
|
|
232
|
-
await
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
261
|
+
await Promise.all([
|
|
262
|
+
this.videoIteratorManager?.seek({
|
|
263
|
+
newTime,
|
|
264
|
+
nonce,
|
|
265
|
+
}),
|
|
266
|
+
this.audioIteratorManager?.seek({
|
|
267
|
+
newTime,
|
|
268
|
+
nonce,
|
|
269
|
+
fps: this.fps,
|
|
270
|
+
playbackRate: this.playbackRate * this.globalPlaybackRate,
|
|
271
|
+
getIsPlaying: () => this.playing,
|
|
272
|
+
scheduleAudioNode: this.scheduleAudioNode,
|
|
273
|
+
bufferState: this.bufferState,
|
|
274
|
+
}),
|
|
275
|
+
]);
|
|
245
276
|
}
|
|
246
277
|
async play(time) {
|
|
278
|
+
if (this.playing) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
247
281
|
const newTime = getTimeInSeconds({
|
|
248
282
|
unloopedTimeInSeconds: time,
|
|
249
283
|
playbackRate: this.playbackRate,
|
|
@@ -272,6 +306,9 @@ export class MediaPlayer {
|
|
|
272
306
|
this.drawDebugOverlay();
|
|
273
307
|
}
|
|
274
308
|
pause() {
|
|
309
|
+
if (!this.playing) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
275
312
|
this.playing = false;
|
|
276
313
|
this.audioIteratorManager?.pausePlayback();
|
|
277
314
|
this.drawDebugOverlay();
|
|
@@ -308,12 +345,16 @@ export class MediaPlayer {
|
|
|
308
345
|
this.audioIteratorManager?.destroyIterator();
|
|
309
346
|
}
|
|
310
347
|
setTrimBefore(trimBefore, unloopedTimeInSeconds) {
|
|
311
|
-
this.trimBefore
|
|
312
|
-
|
|
348
|
+
if (this.trimBefore !== trimBefore) {
|
|
349
|
+
this.trimBefore = trimBefore;
|
|
350
|
+
this.updateAfterTrimChange(unloopedTimeInSeconds);
|
|
351
|
+
}
|
|
313
352
|
}
|
|
314
353
|
setTrimAfter(trimAfter, unloopedTimeInSeconds) {
|
|
315
|
-
this.trimAfter
|
|
316
|
-
|
|
354
|
+
if (this.trimAfter !== trimAfter) {
|
|
355
|
+
this.trimAfter = trimAfter;
|
|
356
|
+
this.updateAfterTrimChange(unloopedTimeInSeconds);
|
|
357
|
+
}
|
|
317
358
|
}
|
|
318
359
|
setDebugOverlay(debugOverlay) {
|
|
319
360
|
this.debugOverlay = debugOverlay;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AudioBufferSink, CanvasSink, WrappedAudioBuffer, WrappedCanvas } from 'mediabunny';
|
|
2
|
+
export declare const makePrewarmedVideoIteratorCache: (videoSink: CanvasSink) => {
|
|
3
|
+
prewarmIteratorForLooping: ({ timeToSeek }: {
|
|
4
|
+
timeToSeek: number;
|
|
5
|
+
}) => void;
|
|
6
|
+
makeIteratorOrUsePrewarmed: (timeToSeek: number) => AsyncGenerator<WrappedCanvas, void, unknown>;
|
|
7
|
+
destroy: () => void;
|
|
8
|
+
};
|
|
9
|
+
export type PrewarmedVideoIteratorCache = ReturnType<typeof makePrewarmedVideoIteratorCache>;
|
|
10
|
+
export declare const makePrewarmedAudioIteratorCache: (audioSink: AudioBufferSink) => {
|
|
11
|
+
prewarmIteratorForLooping: ({ timeToSeek }: {
|
|
12
|
+
timeToSeek: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
makeIteratorOrUsePrewarmed: (timeToSeek: number) => AsyncGenerator<WrappedAudioBuffer, void, unknown>;
|
|
15
|
+
destroy: () => void;
|
|
16
|
+
};
|
|
17
|
+
export type PrewarmedAudioIteratorCache = ReturnType<typeof makePrewarmedAudioIteratorCache>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const makePrewarmedVideoIteratorCache = (videoSink) => {
|
|
2
|
+
const prewarmedVideoIterators = new Map();
|
|
3
|
+
const prewarmIteratorForLooping = ({ timeToSeek }) => {
|
|
4
|
+
if (!prewarmedVideoIterators.has(timeToSeek)) {
|
|
5
|
+
prewarmedVideoIterators.set(timeToSeek, videoSink.canvases(timeToSeek));
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
const makeIteratorOrUsePrewarmed = (timeToSeek) => {
|
|
9
|
+
const prewarmedIterator = prewarmedVideoIterators.get(timeToSeek);
|
|
10
|
+
if (prewarmedIterator) {
|
|
11
|
+
prewarmedVideoIterators.delete(timeToSeek);
|
|
12
|
+
return prewarmedIterator;
|
|
13
|
+
}
|
|
14
|
+
const iterator = videoSink.canvases(timeToSeek);
|
|
15
|
+
return iterator;
|
|
16
|
+
};
|
|
17
|
+
const destroy = () => {
|
|
18
|
+
for (const iterator of prewarmedVideoIterators.values()) {
|
|
19
|
+
iterator.return();
|
|
20
|
+
}
|
|
21
|
+
prewarmedVideoIterators.clear();
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
prewarmIteratorForLooping,
|
|
25
|
+
makeIteratorOrUsePrewarmed,
|
|
26
|
+
destroy,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export const makePrewarmedAudioIteratorCache = (audioSink) => {
|
|
30
|
+
const prewarmedAudioIterators = new Map();
|
|
31
|
+
const prewarmIteratorForLooping = ({ timeToSeek }) => {
|
|
32
|
+
if (!prewarmedAudioIterators.has(timeToSeek)) {
|
|
33
|
+
prewarmedAudioIterators.set(timeToSeek, audioSink.buffers(timeToSeek));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const makeIteratorOrUsePrewarmed = (timeToSeek) => {
|
|
37
|
+
const prewarmedIterator = prewarmedAudioIterators.get(timeToSeek);
|
|
38
|
+
if (prewarmedIterator) {
|
|
39
|
+
prewarmedAudioIterators.delete(timeToSeek);
|
|
40
|
+
return prewarmedIterator;
|
|
41
|
+
}
|
|
42
|
+
const iterator = audioSink.buffers(timeToSeek);
|
|
43
|
+
return iterator;
|
|
44
|
+
};
|
|
45
|
+
const destroy = () => {
|
|
46
|
+
for (const iterator of prewarmedAudioIterators.values()) {
|
|
47
|
+
iterator.return();
|
|
48
|
+
}
|
|
49
|
+
prewarmedAudioIterators.clear();
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
prewarmIteratorForLooping,
|
|
53
|
+
makeIteratorOrUsePrewarmed,
|
|
54
|
+
destroy,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -71,6 +71,7 @@ const VideoForPreviewAssertedShowing = ({ src: unpreloadedSrc, style, playbackRa
|
|
|
71
71
|
throw new Error('useMediaPlayback must be used inside a <BufferingContext>');
|
|
72
72
|
}
|
|
73
73
|
const isPlayerBuffering = Internals.useIsPlayerBuffering(buffering);
|
|
74
|
+
const initialPlaying = useRef(playing && !isPlayerBuffering);
|
|
74
75
|
const initialIsPremounting = useRef(isPremounting);
|
|
75
76
|
const initialIsPostmounting = useRef(isPostmounting);
|
|
76
77
|
const initialGlobalPlaybackRate = useRef(globalPlaybackRate);
|
|
@@ -98,6 +99,7 @@ const VideoForPreviewAssertedShowing = ({ src: unpreloadedSrc, style, playbackRa
|
|
|
98
99
|
isPostmounting: initialIsPostmounting.current,
|
|
99
100
|
globalPlaybackRate: initialGlobalPlaybackRate.current,
|
|
100
101
|
onVideoFrameCallback: initialOnVideoFrameRef.current ?? null,
|
|
102
|
+
playing: initialPlaying.current,
|
|
101
103
|
});
|
|
102
104
|
mediaPlayerRef.current = player;
|
|
103
105
|
player
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { WrappedCanvas } from 'mediabunny';
|
|
2
|
+
import type { PrewarmedVideoIteratorCache } from '../prewarm-iterator-for-looping';
|
|
3
|
+
export declare const createVideoIterator: (timeToSeek: number, cache: PrewarmedVideoIteratorCache) => {
|
|
3
4
|
destroy: () => void;
|
|
4
5
|
getNext: () => Promise<IteratorResult<WrappedCanvas, void>>;
|
|
5
6
|
isDestroyed: () => boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { roundTo4Digits } from '../helpers/round-to-4-digits';
|
|
2
|
-
export const createVideoIterator = (timeToSeek,
|
|
2
|
+
export const createVideoIterator = (timeToSeek, cache) => {
|
|
3
3
|
let destroyed = false;
|
|
4
|
-
const iterator =
|
|
4
|
+
const iterator = cache.makeIteratorOrUsePrewarmed(timeToSeek);
|
|
5
5
|
let lastReturnedFrame = null;
|
|
6
6
|
let iteratorEnded = false;
|
|
7
7
|
const getNextOrNullIfNotAvailable = async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { InputVideoTrack, WrappedCanvas } from 'mediabunny';
|
|
2
2
|
import type { LogLevel } from 'remotion';
|
|
3
3
|
import type { Nonce } from './nonce-manager';
|
|
4
|
-
export declare const videoIteratorManager: ({ delayPlaybackHandleIfNotPremounting, canvas, context, drawDebugOverlay, logLevel, getOnVideoFrameCallback, videoTrack, }: {
|
|
4
|
+
export declare const videoIteratorManager: ({ delayPlaybackHandleIfNotPremounting, canvas, context, drawDebugOverlay, logLevel, getOnVideoFrameCallback, videoTrack, getEndTime, getStartTime, getIsLooping, }: {
|
|
5
5
|
videoTrack: InputVideoTrack;
|
|
6
6
|
delayPlaybackHandleIfNotPremounting: () => {
|
|
7
7
|
unblock: () => void;
|
|
@@ -11,6 +11,9 @@ export declare const videoIteratorManager: ({ delayPlaybackHandleIfNotPremountin
|
|
|
11
11
|
getOnVideoFrameCallback: () => null | ((frame: CanvasImageSource) => void);
|
|
12
12
|
logLevel: LogLevel;
|
|
13
13
|
drawDebugOverlay: () => void;
|
|
14
|
+
getEndTime: () => number;
|
|
15
|
+
getStartTime: () => number;
|
|
16
|
+
getIsLooping: () => boolean;
|
|
14
17
|
}) => {
|
|
15
18
|
startVideoIterator: (timeToSeek: number, nonce: Nonce) => Promise<void>;
|
|
16
19
|
getVideoIteratorsCreated: () => number;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { CanvasSink } from 'mediabunny';
|
|
2
2
|
import { Internals } from 'remotion';
|
|
3
|
+
import { makePrewarmedVideoIteratorCache } from './prewarm-iterator-for-looping';
|
|
3
4
|
import { createVideoIterator, } from './video/video-preview-iterator';
|
|
4
|
-
export const videoIteratorManager = ({ delayPlaybackHandleIfNotPremounting, canvas, context, drawDebugOverlay, logLevel, getOnVideoFrameCallback, videoTrack, }) => {
|
|
5
|
+
export const videoIteratorManager = ({ delayPlaybackHandleIfNotPremounting, canvas, context, drawDebugOverlay, logLevel, getOnVideoFrameCallback, videoTrack, getEndTime, getStartTime, getIsLooping, }) => {
|
|
5
6
|
let videoIteratorsCreated = 0;
|
|
6
7
|
let videoFrameIterator = null;
|
|
7
8
|
let framesRendered = 0;
|
|
9
|
+
let currentDelayHandle = null;
|
|
8
10
|
if (canvas) {
|
|
9
11
|
canvas.width = videoTrack.displayWidth;
|
|
10
12
|
canvas.height = videoTrack.displayHeight;
|
|
@@ -14,6 +16,7 @@ export const videoIteratorManager = ({ delayPlaybackHandleIfNotPremounting, canv
|
|
|
14
16
|
fit: 'contain',
|
|
15
17
|
alpha: true,
|
|
16
18
|
});
|
|
19
|
+
const prewarmedVideoIteratorCache = makePrewarmedVideoIteratorCache(canvasSink);
|
|
17
20
|
const drawFrame = (frame) => {
|
|
18
21
|
if (context && canvas) {
|
|
19
22
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
@@ -29,12 +32,19 @@ export const videoIteratorManager = ({ delayPlaybackHandleIfNotPremounting, canv
|
|
|
29
32
|
};
|
|
30
33
|
const startVideoIterator = async (timeToSeek, nonce) => {
|
|
31
34
|
videoFrameIterator?.destroy();
|
|
32
|
-
const iterator = createVideoIterator(timeToSeek,
|
|
35
|
+
const iterator = createVideoIterator(timeToSeek, prewarmedVideoIteratorCache);
|
|
33
36
|
videoIteratorsCreated++;
|
|
34
37
|
videoFrameIterator = iterator;
|
|
35
38
|
const delayHandle = delayPlaybackHandleIfNotPremounting();
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
currentDelayHandle = delayHandle;
|
|
40
|
+
let frameResult;
|
|
41
|
+
try {
|
|
42
|
+
frameResult = await iterator.getNext();
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
delayHandle.unblock();
|
|
46
|
+
currentDelayHandle = null;
|
|
47
|
+
}
|
|
38
48
|
if (iterator.isDestroyed()) {
|
|
39
49
|
return;
|
|
40
50
|
}
|
|
@@ -54,6 +64,14 @@ export const videoIteratorManager = ({ delayPlaybackHandleIfNotPremounting, canv
|
|
|
54
64
|
if (!videoFrameIterator) {
|
|
55
65
|
return;
|
|
56
66
|
}
|
|
67
|
+
if (getIsLooping()) {
|
|
68
|
+
// If less than 1 second from the end away, we pre-warm a new iterator
|
|
69
|
+
if (getEndTime() - newTime < 1) {
|
|
70
|
+
prewarmedVideoIteratorCache.prewarmIteratorForLooping({
|
|
71
|
+
timeToSeek: getStartTime(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
57
75
|
// Should return immediately, so it's okay to not use Promise.all here
|
|
58
76
|
const videoSatisfyResult = await videoFrameIterator.tryToSatisfySeek(newTime);
|
|
59
77
|
// Doing this before the staleness check, because
|
|
@@ -66,20 +84,22 @@ export const videoIteratorManager = ({ delayPlaybackHandleIfNotPremounting, canv
|
|
|
66
84
|
if (nonce.isStale()) {
|
|
67
85
|
return;
|
|
68
86
|
}
|
|
69
|
-
|
|
70
|
-
startVideoIterator(newTime, nonce).catch(() => {
|
|
71
|
-
// Ignore errors, might be stale or disposed
|
|
72
|
-
});
|
|
87
|
+
await startVideoIterator(newTime, nonce);
|
|
73
88
|
};
|
|
74
89
|
return {
|
|
75
90
|
startVideoIterator,
|
|
76
91
|
getVideoIteratorsCreated: () => videoIteratorsCreated,
|
|
77
92
|
seek,
|
|
78
93
|
destroy: () => {
|
|
94
|
+
prewarmedVideoIteratorCache.destroy();
|
|
79
95
|
videoFrameIterator?.destroy();
|
|
80
96
|
if (context && canvas) {
|
|
81
97
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
82
98
|
}
|
|
99
|
+
if (currentDelayHandle) {
|
|
100
|
+
currentDelayHandle.unblock();
|
|
101
|
+
currentDelayHandle = null;
|
|
102
|
+
}
|
|
83
103
|
videoFrameIterator = null;
|
|
84
104
|
},
|
|
85
105
|
getVideoFrameIterator: () => videoFrameIterator,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remotion/media",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.391",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"module": "dist/esm/index.mjs",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"mediabunny": "1.25.8",
|
|
25
|
-
"remotion": "4.0.
|
|
25
|
+
"remotion": "4.0.391"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": ">=16.8.0",
|
|
29
29
|
"react-dom": ">=16.8.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@remotion/eslint-config-internal": "4.0.
|
|
32
|
+
"@remotion/eslint-config-internal": "4.0.391",
|
|
33
33
|
"@vitest/browser-webdriverio": "4.0.9",
|
|
34
34
|
"eslint": "9.19.0",
|
|
35
35
|
"react": "19.2.3",
|
package/dist/is-network-error.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility to check if error is network error
|
|
3
|
-
* @param error
|
|
4
|
-
* @returns
|
|
5
|
-
*/
|
|
6
|
-
export function isNetworkError(error) {
|
|
7
|
-
if (
|
|
8
|
-
// Chrome
|
|
9
|
-
error.message.includes('Failed to fetch') ||
|
|
10
|
-
// Safari
|
|
11
|
-
error.message.includes('Load failed') ||
|
|
12
|
-
// Firefox
|
|
13
|
-
error.message.includes('NetworkError when attempting to fetch resource')) {
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
return false;
|
|
17
|
-
}
|