@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.
@@ -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 { AudioBufferSink, WrappedAudioBuffer } from 'mediabunny';
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: (audioSink: AudioBufferSink, startFromSecond: number) => {
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 = (audioSink, startFromSecond) => {
4
+ export const makeAudioIterator = (startFromSecond, cache) => {
5
5
  let destroyed = false;
6
- const iterator = audioSink.buffers(startFromSecond);
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
- export const audioIteratorManager = ({ audioTrack, delayPlaybackHandleIfNotPremounting, sharedAudioContext, }) => {
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
- const iterator = makeAudioIterator(audioSink, startFromSecond);
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,
@@ -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 = (audioSink, startFromSecond) => {
102
+ var makeAudioIterator = (startFromSecond, cache) => {
84
103
  let destroyed = false;
85
- const iterator = audioSink.buffers(startFromSecond);
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
- const iterator = makeAudioIterator(audioSink, startFromSecond);
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, videoSink) => {
668
+ var createVideoIterator = (timeToSeek, cache) => {
574
669
  let destroyed = false;
575
- const iterator = videoSink.canvases(timeToSeek);
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, canvasSink);
832
+ const iterator = createVideoIterator(timeToSeek, prewarmedVideoIteratorCache);
733
833
  videoIteratorsCreated++;
734
834
  videoFrameIterator = iterator;
735
835
  const delayHandle = delayPlaybackHandleIfNotPremounting();
736
- const frameResult = await iterator.getNext();
737
- delayHandle.unblock();
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).catch(() => {});
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
- if (this.audioIteratorManager) {
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
- await this.videoIteratorManager?.startVideoIterator(startTime, nonce);
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 this.videoIteratorManager?.seek({
1000
- newTime,
1001
- nonce
1002
- });
1003
- await this.audioIteratorManager?.seek({
1004
- newTime,
1005
- nonce,
1006
- fps: this.fps,
1007
- playbackRate: this.playbackRate * this.globalPlaybackRate,
1008
- getIsPlaying: () => this.playing,
1009
- scheduleAudioNode: this.scheduleAudioNode,
1010
- bufferState: this.bufferState
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 = trimBefore;
1085
- this.updateAfterTrimChange(unloopedTimeInSeconds);
1236
+ if (this.trimBefore !== trimBefore) {
1237
+ this.trimBefore = trimBefore;
1238
+ this.updateAfterTrimChange(unloopedTimeInSeconds);
1239
+ }
1086
1240
  }
1087
1241
  setTrimAfter(trimAfter, unloopedTimeInSeconds) {
1088
- this.trimAfter = trimAfter;
1089
- this.updateAfterTrimChange(unloopedTimeInSeconds);
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
+ };
@@ -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>;
@@ -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
- // intentionally not awaited
175
- if (this.audioIteratorManager) {
176
- this.audioIteratorManager.startAudioIterator({
177
- nonce,
178
- playbackRate: this.playbackRate * this.globalPlaybackRate,
179
- startFromSecond: startTime,
180
- getIsPlaying: () => this.playing,
181
- scheduleAudioNode: this.scheduleAudioNode,
182
- });
183
- }
184
- await this.videoIteratorManager?.startVideoIterator(startTime, nonce);
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 this.videoIteratorManager?.seek({
233
- newTime,
234
- nonce,
235
- });
236
- await this.audioIteratorManager?.seek({
237
- newTime,
238
- nonce,
239
- fps: this.fps,
240
- playbackRate: this.playbackRate * this.globalPlaybackRate,
241
- getIsPlaying: () => this.playing,
242
- scheduleAudioNode: this.scheduleAudioNode,
243
- bufferState: this.bufferState,
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 = trimBefore;
312
- this.updateAfterTrimChange(unloopedTimeInSeconds);
348
+ if (this.trimBefore !== trimBefore) {
349
+ this.trimBefore = trimBefore;
350
+ this.updateAfterTrimChange(unloopedTimeInSeconds);
351
+ }
313
352
  }
314
353
  setTrimAfter(trimAfter, unloopedTimeInSeconds) {
315
- this.trimAfter = trimAfter;
316
- this.updateAfterTrimChange(unloopedTimeInSeconds);
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 { CanvasSink, WrappedCanvas } from 'mediabunny';
2
- export declare const createVideoIterator: (timeToSeek: number, videoSink: CanvasSink) => {
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, videoSink) => {
2
+ export const createVideoIterator = (timeToSeek, cache) => {
3
3
  let destroyed = false;
4
- const iterator = videoSink.canvases(timeToSeek);
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, canvasSink);
35
+ const iterator = createVideoIterator(timeToSeek, prewarmedVideoIteratorCache);
33
36
  videoIteratorsCreated++;
34
37
  videoFrameIterator = iterator;
35
38
  const delayHandle = delayPlaybackHandleIfNotPremounting();
36
- const frameResult = await iterator.getNext();
37
- delayHandle.unblock();
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
- // Intentionally not awaited, letting audio start as well
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.388",
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.388"
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.388",
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",
@@ -1,6 +0,0 @@
1
- /**
2
- * Utility to check if error is network error
3
- * @param error
4
- * @returns
5
- */
6
- export declare function isNetworkError(error: Error): boolean;
@@ -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
- }