@remotion/media 4.0.362 → 4.0.364

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.
@@ -10,171 +10,7 @@ import {
10
10
  useCurrentFrame as useCurrentFrame2
11
11
  } from "remotion";
12
12
 
13
- // src/show-in-timeline.ts
14
- import { useMemo } from "react";
15
- import { Internals, useVideoConfig } from "remotion";
16
- var useLoopDisplay = ({
17
- loop,
18
- mediaDurationInSeconds,
19
- playbackRate,
20
- trimAfter,
21
- trimBefore
22
- }) => {
23
- const { durationInFrames: compDuration, fps } = useVideoConfig();
24
- const loopDisplay = useMemo(() => {
25
- if (!loop || !mediaDurationInSeconds) {
26
- return;
27
- }
28
- const durationInFrames = Internals.calculateMediaDuration({
29
- mediaDurationInFrames: mediaDurationInSeconds * fps,
30
- playbackRate,
31
- trimAfter,
32
- trimBefore
33
- });
34
- const maxTimes = compDuration / durationInFrames;
35
- return {
36
- numberOfTimes: maxTimes,
37
- startOffset: 0,
38
- durationInFrames
39
- };
40
- }, [
41
- compDuration,
42
- fps,
43
- loop,
44
- mediaDurationInSeconds,
45
- playbackRate,
46
- trimAfter,
47
- trimBefore
48
- ]);
49
- return loopDisplay;
50
- };
51
-
52
- // src/use-media-in-timeline.ts
53
- import { useContext, useEffect, useState } from "react";
54
- import { Internals as Internals2, useCurrentFrame } from "remotion";
55
- var useMediaInTimeline = ({
56
- volume,
57
- mediaVolume,
58
- src,
59
- mediaType,
60
- playbackRate,
61
- displayName,
62
- stack,
63
- showInTimeline,
64
- premountDisplay,
65
- postmountDisplay,
66
- loopDisplay,
67
- trimBefore,
68
- trimAfter
69
- }) => {
70
- const parentSequence = useContext(Internals2.SequenceContext);
71
- const startsAt = Internals2.useMediaStartsAt();
72
- const { registerSequence, unregisterSequence } = useContext(Internals2.SequenceManager);
73
- const [sequenceId] = useState(() => String(Math.random()));
74
- const [mediaId] = useState(() => String(Math.random()));
75
- const frame = useCurrentFrame();
76
- const {
77
- volumes,
78
- duration,
79
- doesVolumeChange,
80
- nonce,
81
- rootId,
82
- isStudio,
83
- finalDisplayName
84
- } = Internals2.useBasicMediaInTimeline({
85
- volume,
86
- mediaVolume,
87
- mediaType,
88
- src,
89
- displayName,
90
- trimBefore,
91
- trimAfter,
92
- playbackRate
93
- });
94
- useEffect(() => {
95
- if (!src) {
96
- throw new Error("No src passed");
97
- }
98
- if (!isStudio && window.process?.env?.NODE_ENV !== "test") {
99
- return;
100
- }
101
- if (!showInTimeline) {
102
- return;
103
- }
104
- const loopIteration = loopDisplay ? Math.floor(frame / loopDisplay.durationInFrames) : 0;
105
- if (loopDisplay) {
106
- registerSequence({
107
- type: "sequence",
108
- premountDisplay,
109
- postmountDisplay,
110
- parent: parentSequence?.id ?? null,
111
- displayName: finalDisplayName,
112
- rootId,
113
- showInTimeline: true,
114
- nonce,
115
- loopDisplay,
116
- stack,
117
- from: 0,
118
- duration,
119
- id: sequenceId
120
- });
121
- }
122
- registerSequence({
123
- type: mediaType,
124
- src,
125
- id: mediaId,
126
- duration: loopDisplay?.durationInFrames ?? duration,
127
- from: loopDisplay ? loopIteration * loopDisplay.durationInFrames : 0,
128
- parent: loopDisplay ? sequenceId : parentSequence?.id ?? null,
129
- displayName: finalDisplayName,
130
- rootId,
131
- volume: volumes,
132
- showInTimeline: true,
133
- nonce,
134
- startMediaFrom: 0 - startsAt,
135
- doesVolumeChange,
136
- loopDisplay: undefined,
137
- playbackRate,
138
- stack,
139
- premountDisplay: null,
140
- postmountDisplay: null
141
- });
142
- return () => {
143
- if (loopDisplay) {
144
- unregisterSequence(sequenceId);
145
- }
146
- unregisterSequence(mediaId);
147
- };
148
- }, [
149
- doesVolumeChange,
150
- duration,
151
- finalDisplayName,
152
- isStudio,
153
- loopDisplay,
154
- mediaId,
155
- mediaType,
156
- nonce,
157
- parentSequence?.id,
158
- playbackRate,
159
- postmountDisplay,
160
- premountDisplay,
161
- registerSequence,
162
- rootId,
163
- sequenceId,
164
- showInTimeline,
165
- src,
166
- stack,
167
- startsAt,
168
- unregisterSequence,
169
- volumes,
170
- frame
171
- ]);
172
- return {
173
- id: mediaId
174
- };
175
- };
176
-
177
- // src/video/media-player.ts
13
+ // src/media-player.ts
178
14
  import {
179
15
  ALL_FORMATS,
180
16
  AudioBufferSink,
@@ -182,10 +18,71 @@ import {
182
18
  Input,
183
19
  UrlSource
184
20
  } from "mediabunny";
185
- import { Internals as Internals4 } from "remotion";
21
+ import { Internals as Internals2 } from "remotion";
22
+
23
+ // src/audio/audio-preview-iterator.ts
24
+ var HEALTHY_BUFFER_THRESHOLD_SECONDS = 1;
25
+ var makeAudioIterator = (audioSink, startFromSecond) => {
26
+ let destroyed = false;
27
+ const iterator = audioSink.buffers(startFromSecond);
28
+ let audioIteratorStarted = false;
29
+ let audioBufferHealth = 0;
30
+ const queuedAudioNodes = new Set;
31
+ const cleanupAudioQueue = () => {
32
+ for (const node of queuedAudioNodes) {
33
+ node.stop();
34
+ }
35
+ queuedAudioNodes.clear();
36
+ };
37
+ return {
38
+ cleanupAudioQueue,
39
+ destroy: () => {
40
+ cleanupAudioQueue();
41
+ destroyed = true;
42
+ iterator.return().catch(() => {
43
+ return;
44
+ });
45
+ },
46
+ isReadyToPlay: () => {
47
+ return audioIteratorStarted && audioBufferHealth > 0;
48
+ },
49
+ setAudioIteratorStarted: (started) => {
50
+ audioIteratorStarted = started;
51
+ },
52
+ getNext: () => {
53
+ return iterator.next();
54
+ },
55
+ setAudioBufferHealth: (health) => {
56
+ audioBufferHealth = health;
57
+ },
58
+ isDestroyed: () => {
59
+ return destroyed;
60
+ },
61
+ addQueuedAudioNode: (node) => {
62
+ queuedAudioNodes.add(node);
63
+ },
64
+ removeQueuedAudioNode: (node) => {
65
+ queuedAudioNodes.delete(node);
66
+ }
67
+ };
68
+ };
69
+
70
+ // src/debug-overlay/preview-overlay.ts
71
+ var drawPreviewOverlay = (context, stats, audioContextState, audioSyncAnchor) => {
72
+ context.fillStyle = "rgba(0, 0, 0, 1)";
73
+ context.fillRect(20, 20, 600, 180);
74
+ context.fillStyle = "white";
75
+ context.font = "24px sans-serif";
76
+ context.textBaseline = "top";
77
+ context.fillText(`Debug overlay`, 30, 30);
78
+ context.fillText(`Video iterators created: ${stats.videoIteratorsCreated}`, 30, 60);
79
+ context.fillText(`Frames rendered: ${stats.framesRendered}`, 30, 90);
80
+ context.fillText(`Audio context state: ${audioContextState}`, 30, 120);
81
+ context.fillText(`Audio time: ${audioSyncAnchor.toFixed(3)}s`, 30, 150);
82
+ };
186
83
 
187
84
  // src/get-time-in-seconds.ts
188
- import { Internals as Internals3 } from "remotion";
85
+ import { Internals } from "remotion";
189
86
  var getTimeInSeconds = ({
190
87
  loop,
191
88
  mediaDurationInSeconds,
@@ -200,7 +97,7 @@ var getTimeInSeconds = ({
200
97
  if (mediaDurationInSeconds === null && loop && ifNoMediaDuration === "fail") {
201
98
  throw new Error(`Could not determine duration of ${src}, but "loop" was set.`);
202
99
  }
203
- const loopDuration = loop ? Internals3.calculateMediaDuration({
100
+ const loopDuration = loop ? Internals.calculateMediaDuration({
204
101
  trimAfter,
205
102
  mediaDurationInFrames: mediaDurationInSeconds ? mediaDurationInSeconds * fps : Infinity,
206
103
  playbackRate: 1,
@@ -250,8 +147,135 @@ function withTimeout(promise, timeoutMs, errorMessage = "Operation timed out") {
250
147
  ]);
251
148
  }
252
149
 
253
- // src/video/media-player.ts
254
- var SEEK_THRESHOLD = 0.05;
150
+ // src/helpers/round-to-4-digits.ts
151
+ var roundTo4Digits = (timestamp) => {
152
+ return Math.round(timestamp * 1000) / 1000;
153
+ };
154
+
155
+ // src/video/video-preview-iterator.ts
156
+ var createVideoIterator = (timeToSeek, videoSink) => {
157
+ let destroyed = false;
158
+ const iterator = videoSink.canvases(timeToSeek);
159
+ let lastReturnedFrame = null;
160
+ let iteratorEnded = false;
161
+ const getNextOrNullIfNotAvailable = async () => {
162
+ const next = iterator.next();
163
+ const result = await Promise.race([
164
+ next,
165
+ new Promise((resolve) => {
166
+ Promise.resolve().then(() => resolve());
167
+ })
168
+ ]);
169
+ if (!result) {
170
+ return {
171
+ type: "need-to-wait-for-it",
172
+ waitPromise: async () => {
173
+ const res = await next;
174
+ if (res.value) {
175
+ lastReturnedFrame = res.value;
176
+ } else {
177
+ iteratorEnded = true;
178
+ }
179
+ return res.value;
180
+ }
181
+ };
182
+ }
183
+ if (result.value) {
184
+ lastReturnedFrame = result.value;
185
+ } else {
186
+ iteratorEnded = true;
187
+ }
188
+ return {
189
+ type: "got-frame-or-end",
190
+ frame: result.value ?? null
191
+ };
192
+ };
193
+ const destroy = () => {
194
+ destroyed = true;
195
+ lastReturnedFrame = null;
196
+ iterator.return().catch(() => {
197
+ return;
198
+ });
199
+ };
200
+ const tryToSatisfySeek = async (time) => {
201
+ if (lastReturnedFrame) {
202
+ const frameTimestamp = roundTo4Digits(lastReturnedFrame.timestamp);
203
+ if (roundTo4Digits(time) < frameTimestamp) {
204
+ return {
205
+ type: "not-satisfied",
206
+ reason: `iterator is too far, most recently returned ${frameTimestamp}`
207
+ };
208
+ }
209
+ const frameEndTimestamp = roundTo4Digits(lastReturnedFrame.timestamp + lastReturnedFrame.duration);
210
+ const timestamp = roundTo4Digits(time);
211
+ if (frameTimestamp <= timestamp && frameEndTimestamp > timestamp) {
212
+ return {
213
+ type: "satisfied",
214
+ frame: lastReturnedFrame
215
+ };
216
+ }
217
+ }
218
+ if (iteratorEnded) {
219
+ if (lastReturnedFrame) {
220
+ return {
221
+ type: "satisfied",
222
+ frame: lastReturnedFrame
223
+ };
224
+ }
225
+ return {
226
+ type: "not-satisfied",
227
+ reason: "iterator ended"
228
+ };
229
+ }
230
+ while (true) {
231
+ const frame = await getNextOrNullIfNotAvailable();
232
+ if (frame.type === "need-to-wait-for-it") {
233
+ return {
234
+ type: "not-satisfied",
235
+ reason: "iterator did not have frame ready"
236
+ };
237
+ }
238
+ if (frame.type === "got-frame-or-end") {
239
+ if (frame.frame === null) {
240
+ iteratorEnded = true;
241
+ if (lastReturnedFrame) {
242
+ return {
243
+ type: "satisfied",
244
+ frame: lastReturnedFrame
245
+ };
246
+ }
247
+ return {
248
+ type: "not-satisfied",
249
+ reason: "iterator ended and did not have frame ready"
250
+ };
251
+ }
252
+ const frameTimestamp = roundTo4Digits(frame.frame.timestamp);
253
+ const frameEndTimestamp = roundTo4Digits(frame.frame.timestamp + frame.frame.duration);
254
+ const timestamp = roundTo4Digits(time);
255
+ if (frameTimestamp <= timestamp && frameEndTimestamp > timestamp) {
256
+ return {
257
+ type: "satisfied",
258
+ frame: frame.frame
259
+ };
260
+ }
261
+ continue;
262
+ }
263
+ throw new Error("Unreachable");
264
+ }
265
+ };
266
+ return {
267
+ destroy,
268
+ getNext: () => {
269
+ return iterator.next();
270
+ },
271
+ isDestroyed: () => {
272
+ return destroyed;
273
+ },
274
+ tryToSatisfySeek
275
+ };
276
+ };
277
+
278
+ // src/media-player.ts
255
279
  var AUDIO_BUFFER_TOLERANCE_THRESHOLD = 0.1;
256
280
 
257
281
  class MediaPlayer {
@@ -263,10 +287,12 @@ class MediaPlayer {
263
287
  audioStreamIndex;
264
288
  canvasSink = null;
265
289
  videoFrameIterator = null;
266
- nextFrame = null;
290
+ debugStats = {
291
+ videoIteratorsCreated: 0,
292
+ framesRendered: 0
293
+ };
267
294
  audioSink = null;
268
295
  audioBufferIterator = null;
269
- queuedAudioNodes = new Set;
270
296
  gainNode = null;
271
297
  currentVolume = 1;
272
298
  sharedAudioContext;
@@ -277,18 +303,15 @@ class MediaPlayer {
277
303
  fps;
278
304
  trimBefore;
279
305
  trimAfter;
280
- animationFrameId = null;
281
- videoAsyncId = 0;
282
- audioAsyncId = 0;
283
306
  initialized = false;
284
307
  totalDuration;
285
308
  isBuffering = false;
286
309
  onBufferingChangeCallback;
287
- audioBufferHealth = 0;
288
- audioIteratorStarted = false;
289
- HEALTHY_BUFER_THRESHOLD_SECONDS = 1;
290
310
  mediaEnded = false;
311
+ debugOverlay = false;
291
312
  onVideoFrameCallback;
313
+ initializationPromise = null;
314
+ bufferState;
292
315
  constructor({
293
316
  canvas,
294
317
  src,
@@ -299,7 +322,9 @@ class MediaPlayer {
299
322
  trimAfter,
300
323
  playbackRate,
301
324
  audioStreamIndex,
302
- fps
325
+ fps,
326
+ debugOverlay,
327
+ bufferState
303
328
  }) {
304
329
  this.canvas = canvas ?? null;
305
330
  this.src = src;
@@ -311,6 +336,8 @@ class MediaPlayer {
311
336
  this.trimAfter = trimAfter;
312
337
  this.audioStreamIndex = audioStreamIndex ?? 0;
313
338
  this.fps = fps;
339
+ this.debugOverlay = debugOverlay;
340
+ this.bufferState = bufferState;
314
341
  if (canvas) {
315
342
  const context = canvas.getContext("2d", {
316
343
  alpha: true,
@@ -326,7 +353,7 @@ class MediaPlayer {
326
353
  }
327
354
  input = null;
328
355
  isReady() {
329
- return this.initialized && Boolean(this.sharedAudioContext);
356
+ return this.initialized && Boolean(this.sharedAudioContext) && !this.input?.disposed;
330
357
  }
331
358
  hasAudio() {
332
359
  return Boolean(this.audioSink && this.sharedAudioContext && this.gainNode);
@@ -334,7 +361,15 @@ class MediaPlayer {
334
361
  isCurrentlyBuffering() {
335
362
  return this.isBuffering && Boolean(this.bufferingStartedAtMs);
336
363
  }
337
- async initialize(startTimeUnresolved) {
364
+ isDisposalError() {
365
+ return this.input?.disposed === true;
366
+ }
367
+ initialize(startTimeUnresolved) {
368
+ const promise = this._initialize(startTimeUnresolved);
369
+ this.initializationPromise = promise;
370
+ return promise;
371
+ }
372
+ async _initialize(startTimeUnresolved) {
338
373
  try {
339
374
  const urlSource = new UrlSource(this.src);
340
375
  const input = new Input({
@@ -342,14 +377,20 @@ class MediaPlayer {
342
377
  formats: ALL_FORMATS
343
378
  });
344
379
  this.input = input;
380
+ if (input.disposed) {
381
+ return { type: "disposed" };
382
+ }
345
383
  try {
346
- await this.input.getFormat();
384
+ await input.getFormat();
347
385
  } catch (error) {
386
+ if (this.isDisposalError()) {
387
+ return { type: "disposed" };
388
+ }
348
389
  const err = error;
349
390
  if (isNetworkError(err)) {
350
391
  throw error;
351
392
  }
352
- Internals4.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, `[MediaPlayer] Failed to recognize format for ${this.src}`, error);
393
+ Internals2.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, `[MediaPlayer] Failed to recognize format for ${this.src}`, error);
353
394
  return { type: "unknown-container-format" };
354
395
  }
355
396
  const [durationInSeconds, videoTrack, audioTracks] = await Promise.all([
@@ -399,19 +440,23 @@ class MediaPlayer {
399
440
  this.audioSyncAnchor = this.sharedAudioContext.currentTime - startTime;
400
441
  }
401
442
  this.initialized = true;
402
- await Promise.all([
403
- this.startAudioIterator(startTime),
404
- this.startVideoIterator(startTime)
405
- ]);
406
- this.startRenderLoop();
443
+ try {
444
+ this.startAudioIterator(startTime);
445
+ await this.startVideoIterator(startTime, this.currentSeekNonce);
446
+ } catch (error) {
447
+ if (this.isDisposalError()) {
448
+ return { type: "disposed" };
449
+ }
450
+ Internals2.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, "[MediaPlayer] Failed to start audio and video iterators", error);
451
+ }
407
452
  return { type: "success", durationInSeconds };
408
453
  } catch (error) {
409
454
  const err = error;
410
455
  if (isNetworkError(err)) {
411
- Internals4.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, `[MediaPlayer] Network/CORS error for ${this.src}`, err);
456
+ Internals2.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, `[MediaPlayer] Network/CORS error for ${this.src}`, err);
412
457
  return { type: "network-error" };
413
458
  }
414
- Internals4.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, "[MediaPlayer] Failed to initialize", error);
459
+ Internals2.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, "[MediaPlayer] Failed to initialize", error);
415
460
  throw error;
416
461
  }
417
462
  }
@@ -420,20 +465,19 @@ class MediaPlayer {
420
465
  this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
421
466
  }
422
467
  }
423
- cleanupAudioQueue() {
424
- for (const node of this.queuedAudioNodes) {
425
- node.stop();
426
- }
427
- this.queuedAudioNodes.clear();
428
- }
429
- async cleanAudioIteratorAndNodes() {
430
- await this.audioBufferIterator?.return();
431
- this.audioBufferIterator = null;
432
- this.audioIteratorStarted = false;
433
- this.audioBufferHealth = 0;
434
- this.cleanupAudioQueue();
435
- }
468
+ currentSeekNonce = 0;
469
+ seekPromiseChain = Promise.resolve();
436
470
  async seekTo(time) {
471
+ this.currentSeekNonce++;
472
+ const nonce = this.currentSeekNonce;
473
+ await this.seekPromiseChain;
474
+ this.seekPromiseChain = this.seekToDoNotCallDirectly(time, nonce);
475
+ await this.seekPromiseChain;
476
+ }
477
+ async seekToDoNotCallDirectly(time, nonce) {
478
+ if (nonce !== this.currentSeekNonce) {
479
+ return;
480
+ }
437
481
  if (!this.isReady())
438
482
  return;
439
483
  const newTime = getTimeInSeconds({
@@ -448,29 +492,29 @@ class MediaPlayer {
448
492
  src: this.src
449
493
  });
450
494
  if (newTime === null) {
451
- this.videoAsyncId++;
452
- this.nextFrame = null;
495
+ this.videoFrameIterator?.destroy();
496
+ this.videoFrameIterator = null;
453
497
  this.clearCanvas();
454
- await this.cleanAudioIteratorAndNodes();
498
+ this.audioBufferIterator?.destroy();
499
+ this.audioBufferIterator = null;
455
500
  return;
456
501
  }
457
502
  const currentPlaybackTime = this.getPlaybackTime();
458
- const isSignificantSeek = currentPlaybackTime === null || Math.abs(newTime - currentPlaybackTime) > SEEK_THRESHOLD;
459
- if (isSignificantSeek) {
460
- this.nextFrame = null;
461
- this.audioSyncAnchor = this.sharedAudioContext.currentTime - newTime;
462
- this.mediaEnded = false;
463
- if (this.audioSink) {
464
- await this.cleanAudioIteratorAndNodes();
465
- }
466
- await Promise.all([
467
- this.startAudioIterator(newTime),
468
- this.startVideoIterator(newTime)
469
- ]);
503
+ if (currentPlaybackTime === newTime) {
504
+ return;
470
505
  }
471
- if (!this.playing) {
472
- this.render();
506
+ const satisfyResult = await this.videoFrameIterator?.tryToSatisfySeek(newTime);
507
+ if (satisfyResult?.type === "satisfied") {
508
+ this.drawFrame(satisfyResult.frame);
509
+ return;
473
510
  }
511
+ if (this.currentSeekNonce !== nonce) {
512
+ return;
513
+ }
514
+ this.mediaEnded = false;
515
+ this.audioSyncAnchor = this.sharedAudioContext.currentTime - newTime;
516
+ this.startAudioIterator(newTime);
517
+ this.startVideoIterator(newTime, nonce);
474
518
  }
475
519
  async play() {
476
520
  if (!this.isReady())
@@ -480,13 +524,11 @@ class MediaPlayer {
480
524
  await this.sharedAudioContext.resume();
481
525
  }
482
526
  this.playing = true;
483
- this.startRenderLoop();
484
527
  }
485
528
  }
486
529
  pause() {
487
530
  this.playing = false;
488
- this.cleanupAudioQueue();
489
- this.stopRenderLoop();
531
+ this.audioBufferIterator?.cleanupAudioQueue();
490
532
  }
491
533
  setMuted(muted) {
492
534
  this.muted = muted;
@@ -504,6 +546,9 @@ class MediaPlayer {
504
546
  this.gainNode.gain.value = appliedVolume;
505
547
  }
506
548
  }
549
+ setDebugOverlay(debugOverlay) {
550
+ this.debugOverlay = debugOverlay;
551
+ }
507
552
  setPlaybackRate(rate) {
508
553
  this.playbackRate = rate;
509
554
  }
@@ -513,12 +558,18 @@ class MediaPlayer {
513
558
  setLoop(loop) {
514
559
  this.loop = loop;
515
560
  }
516
- dispose() {
561
+ async dispose() {
562
+ this.initialized = false;
563
+ if (this.initializationPromise) {
564
+ try {
565
+ await this.initializationPromise;
566
+ } catch {}
567
+ }
517
568
  this.input?.dispose();
518
- this.stopRenderLoop();
519
- this.videoFrameIterator?.return();
520
- this.cleanAudioIteratorAndNodes();
521
- this.videoAsyncId++;
569
+ this.videoFrameIterator?.destroy();
570
+ this.videoFrameIterator = null;
571
+ this.audioBufferIterator?.destroy();
572
+ this.audioBufferIterator = null;
522
573
  }
523
574
  getPlaybackTime() {
524
575
  return this.sharedAudioContext.currentTime - this.audioSyncAnchor;
@@ -535,8 +586,8 @@ class MediaPlayer {
535
586
  } else {
536
587
  node.start(this.sharedAudioContext.currentTime, -delay);
537
588
  }
538
- this.queuedAudioNodes.add(node);
539
- node.onended = () => this.queuedAudioNodes.delete(node);
589
+ this.audioBufferIterator?.addQueuedAudioNode(node);
590
+ node.onended = () => this.audioBufferIterator?.removeQueuedAudioNode(node);
540
591
  }
541
592
  onBufferingChange(callback) {
542
593
  this.onBufferingChangeCallback = callback;
@@ -557,225 +608,322 @@ class MediaPlayer {
557
608
  }
558
609
  };
559
610
  }
560
- canRenderVideo() {
561
- return !this.hasAudio() || this.audioIteratorStarted && this.audioBufferHealth >= this.HEALTHY_BUFER_THRESHOLD_SECONDS;
562
- }
563
- startRenderLoop() {
564
- if (this.animationFrameId !== null) {
565
- return;
566
- }
567
- this.render();
568
- }
569
- stopRenderLoop() {
570
- if (this.animationFrameId !== null) {
571
- cancelAnimationFrame(this.animationFrameId);
572
- this.animationFrameId = null;
573
- }
574
- }
575
- render = () => {
576
- if (this.isBuffering) {
577
- this.maybeForceResumeFromBuffering();
578
- }
579
- if (this.shouldRenderFrame()) {
580
- this.drawCurrentFrame();
581
- }
582
- if (this.playing) {
583
- this.animationFrameId = requestAnimationFrame(this.render);
584
- } else {
585
- this.animationFrameId = null;
586
- }
587
- };
588
- shouldRenderFrame() {
589
- const playbackTime = this.getPlaybackTime();
590
- if (playbackTime === null) {
591
- return false;
592
- }
593
- return !this.isBuffering && this.canRenderVideo() && this.nextFrame !== null && this.nextFrame.timestamp <= playbackTime;
594
- }
595
- drawCurrentFrame() {
596
- if (this.context && this.nextFrame) {
597
- this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
598
- this.context.drawImage(this.nextFrame.canvas, 0, 0);
611
+ drawFrame = (frame) => {
612
+ if (!this.context) {
613
+ throw new Error("Context not initialized");
599
614
  }
615
+ this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
616
+ this.context.drawImage(frame.canvas, 0, 0);
617
+ this.debugStats.framesRendered++;
618
+ this.drawDebugOverlay();
600
619
  if (this.onVideoFrameCallback && this.canvas) {
601
620
  this.onVideoFrameCallback(this.canvas);
602
621
  }
603
- this.nextFrame = null;
604
- this.updateNextFrame();
605
- }
606
- startAudioIterator = async (startFromSecond) => {
622
+ Internals2.Log.trace({ logLevel: this.logLevel, tag: "@remotion/media" }, `[MediaPlayer] Drew frame ${frame.timestamp.toFixed(3)}s`);
623
+ };
624
+ startAudioIterator = (startFromSecond) => {
607
625
  if (!this.hasAudio())
608
626
  return;
609
- this.audioAsyncId++;
610
- const currentAsyncId = this.audioAsyncId;
611
- await this.audioBufferIterator?.return();
612
- this.audioIteratorStarted = false;
613
- this.audioBufferHealth = 0;
627
+ this.audioBufferIterator?.destroy();
614
628
  try {
615
- this.audioBufferIterator = this.audioSink.buffers(startFromSecond);
616
- this.runAudioIterator(startFromSecond, currentAsyncId);
629
+ const iterator = makeAudioIterator(this.audioSink, startFromSecond);
630
+ this.audioBufferIterator = iterator;
631
+ this.runAudioIterator(startFromSecond, iterator);
617
632
  } catch (error) {
618
- Internals4.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, "[MediaPlayer] Failed to start audio iterator", error);
633
+ if (this.isDisposalError()) {
634
+ return;
635
+ }
636
+ Internals2.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, "[MediaPlayer] Failed to start audio iterator", error);
619
637
  }
620
638
  };
621
- startVideoIterator = async (timeToSeek) => {
639
+ drawDebugOverlay() {
640
+ if (!this.debugOverlay)
641
+ return;
642
+ if (this.context && this.canvas) {
643
+ drawPreviewOverlay(this.context, this.debugStats, this.sharedAudioContext.state, this.sharedAudioContext.currentTime);
644
+ }
645
+ }
646
+ startVideoIterator = async (timeToSeek, nonce) => {
622
647
  if (!this.canvasSink) {
623
648
  return;
624
649
  }
625
- this.videoAsyncId++;
626
- const currentAsyncId = this.videoAsyncId;
627
- this.videoFrameIterator?.return().catch(() => {
650
+ this.videoFrameIterator?.destroy();
651
+ const iterator = createVideoIterator(timeToSeek, this.canvasSink);
652
+ this.debugStats.videoIteratorsCreated++;
653
+ this.videoFrameIterator = iterator;
654
+ const delayHandle = this.bufferState?.delayPlayback();
655
+ const frameResult = await iterator.getNext();
656
+ delayHandle?.unblock();
657
+ if (iterator.isDestroyed()) {
658
+ return;
659
+ }
660
+ if (nonce !== this.currentSeekNonce) {
628
661
  return;
629
- });
630
- this.videoFrameIterator = this.canvasSink.canvases(timeToSeek);
631
- try {
632
- const firstFrame = (await this.videoFrameIterator.next()).value ?? null;
633
- const secondFrame = (await this.videoFrameIterator.next()).value ?? null;
634
- if (currentAsyncId !== this.videoAsyncId) {
635
- return;
636
- }
637
- if (firstFrame && this.context) {
638
- Internals4.Log.trace({ logLevel: this.logLevel, tag: "@remotion/media" }, `[MediaPlayer] Drew initial frame ${firstFrame.timestamp.toFixed(3)}s`);
639
- this.context.drawImage(firstFrame.canvas, 0, 0);
640
- if (this.onVideoFrameCallback && this.canvas) {
641
- this.onVideoFrameCallback(this.canvas);
642
- }
643
- }
644
- this.nextFrame = secondFrame ?? null;
645
- if (secondFrame) {
646
- Internals4.Log.trace({ logLevel: this.logLevel, tag: "@remotion/media" }, `[MediaPlayer] Buffered next frame ${secondFrame.timestamp.toFixed(3)}s`);
647
- }
648
- } catch (error) {
649
- Internals4.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, "[MediaPlayer] Failed to start video iterator", error);
650
662
  }
663
+ if (this.videoFrameIterator.isDestroyed()) {
664
+ return;
665
+ }
666
+ if (frameResult.value) {
667
+ this.audioSyncAnchor = this.sharedAudioContext.currentTime - frameResult.value.timestamp;
668
+ this.drawFrame(frameResult.value);
669
+ } else {}
651
670
  };
652
- updateNextFrame = async () => {
653
- if (!this.videoFrameIterator) {
671
+ bufferingStartedAtMs = null;
672
+ minBufferingTimeoutMs = 500;
673
+ setBufferingState(isBuffering) {
674
+ if (this.isBuffering !== isBuffering) {
675
+ this.isBuffering = isBuffering;
676
+ if (isBuffering) {
677
+ this.bufferingStartedAtMs = performance.now();
678
+ this.onBufferingChangeCallback?.(true);
679
+ } else {
680
+ this.bufferingStartedAtMs = null;
681
+ this.onBufferingChangeCallback?.(false);
682
+ }
683
+ }
684
+ }
685
+ maybeResumeFromBuffering(currentBufferDuration) {
686
+ if (!this.isCurrentlyBuffering())
654
687
  return;
688
+ const now = performance.now();
689
+ const bufferingDuration = now - this.bufferingStartedAtMs;
690
+ const minTimeElapsed = bufferingDuration >= this.minBufferingTimeoutMs;
691
+ const bufferHealthy = currentBufferDuration >= HEALTHY_BUFFER_THRESHOLD_SECONDS;
692
+ if (minTimeElapsed && bufferHealthy) {
693
+ Internals2.Log.trace({ logLevel: this.logLevel, tag: "@remotion/media" }, `[MediaPlayer] Resuming from buffering after ${bufferingDuration}ms - buffer recovered`);
694
+ this.setBufferingState(false);
655
695
  }
696
+ }
697
+ runAudioIterator = async (startFromSecond, audioIterator) => {
698
+ if (!this.hasAudio())
699
+ return;
656
700
  try {
701
+ let totalBufferDuration = 0;
702
+ let isFirstBuffer = true;
703
+ audioIterator.setAudioIteratorStarted(true);
657
704
  while (true) {
658
- const newNextFrame = (await this.videoFrameIterator.next()).value ?? null;
659
- if (!newNextFrame) {
705
+ if (audioIterator.isDestroyed()) {
706
+ return;
707
+ }
708
+ const BUFFERING_TIMEOUT_MS = 50;
709
+ let result;
710
+ try {
711
+ result = await withTimeout(audioIterator.getNext(), BUFFERING_TIMEOUT_MS, "Iterator timeout");
712
+ } catch (error) {
713
+ if (error instanceof TimeoutError && !this.mediaEnded) {
714
+ this.setBufferingState(true);
715
+ }
716
+ await sleep(10);
717
+ continue;
718
+ }
719
+ if (result.done || !result.value) {
660
720
  this.mediaEnded = true;
661
721
  break;
662
722
  }
723
+ const { buffer, timestamp, duration } = result.value;
724
+ totalBufferDuration += duration;
725
+ audioIterator.setAudioBufferHealth(Math.max(0, totalBufferDuration / this.playbackRate));
726
+ this.maybeResumeFromBuffering(totalBufferDuration / this.playbackRate);
727
+ if (this.playing) {
728
+ if (isFirstBuffer) {
729
+ this.audioSyncAnchor = this.sharedAudioContext.currentTime - timestamp;
730
+ isFirstBuffer = false;
731
+ }
732
+ if (timestamp < startFromSecond - AUDIO_BUFFER_TOLERANCE_THRESHOLD) {
733
+ continue;
734
+ }
735
+ this.scheduleAudioChunk(buffer, timestamp);
736
+ }
663
737
  const playbackTime = this.getPlaybackTime();
664
738
  if (playbackTime === null) {
665
739
  continue;
666
740
  }
667
- if (newNextFrame.timestamp <= playbackTime) {
668
- continue;
669
- } else {
670
- this.nextFrame = newNextFrame;
671
- Internals4.Log.trace({ logLevel: this.logLevel, tag: "@remotion/media" }, `[MediaPlayer] Buffered next frame ${newNextFrame.timestamp.toFixed(3)}s`);
672
- break;
741
+ if (timestamp - playbackTime >= 1) {
742
+ await new Promise((resolve) => {
743
+ const check = () => {
744
+ const currentPlaybackTime = this.getPlaybackTime();
745
+ if (currentPlaybackTime !== null && timestamp - currentPlaybackTime < 1) {
746
+ resolve();
747
+ } else {
748
+ requestAnimationFrame(check);
749
+ }
750
+ };
751
+ check();
752
+ });
673
753
  }
674
754
  }
675
755
  } catch (error) {
676
- Internals4.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, "[MediaPlayer] Failed to update next frame", error);
756
+ if (this.isDisposalError()) {
757
+ return;
758
+ }
759
+ Internals2.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, "[MediaPlayer] Failed to run audio iterator", error);
677
760
  }
678
761
  };
679
- bufferingStartedAtMs = null;
680
- minBufferingTimeoutMs = 500;
681
- setBufferingState(isBuffering) {
682
- if (this.isBuffering !== isBuffering) {
683
- this.isBuffering = isBuffering;
684
- if (isBuffering) {
685
- this.bufferingStartedAtMs = performance.now();
686
- this.onBufferingChangeCallback?.(true);
687
- } else {
688
- this.bufferingStartedAtMs = null;
689
- this.onBufferingChangeCallback?.(false);
690
- }
762
+ }
763
+
764
+ // src/show-in-timeline.ts
765
+ import { useMemo } from "react";
766
+ import { Internals as Internals3, useVideoConfig } from "remotion";
767
+ var useLoopDisplay = ({
768
+ loop,
769
+ mediaDurationInSeconds,
770
+ playbackRate,
771
+ trimAfter,
772
+ trimBefore
773
+ }) => {
774
+ const { durationInFrames: compDuration, fps } = useVideoConfig();
775
+ const loopDisplay = useMemo(() => {
776
+ if (!loop || !mediaDurationInSeconds) {
777
+ return;
778
+ }
779
+ const durationInFrames = Internals3.calculateMediaDuration({
780
+ mediaDurationInFrames: mediaDurationInSeconds * fps,
781
+ playbackRate,
782
+ trimAfter,
783
+ trimBefore
784
+ });
785
+ const maxTimes = compDuration / durationInFrames;
786
+ return {
787
+ numberOfTimes: maxTimes,
788
+ startOffset: 0,
789
+ durationInFrames
790
+ };
791
+ }, [
792
+ compDuration,
793
+ fps,
794
+ loop,
795
+ mediaDurationInSeconds,
796
+ playbackRate,
797
+ trimAfter,
798
+ trimBefore
799
+ ]);
800
+ return loopDisplay;
801
+ };
802
+
803
+ // src/use-media-in-timeline.ts
804
+ import { useContext, useEffect, useState } from "react";
805
+ import { Internals as Internals4, useCurrentFrame } from "remotion";
806
+ var useMediaInTimeline = ({
807
+ volume,
808
+ mediaVolume,
809
+ src,
810
+ mediaType,
811
+ playbackRate,
812
+ displayName,
813
+ stack,
814
+ showInTimeline,
815
+ premountDisplay,
816
+ postmountDisplay,
817
+ loopDisplay,
818
+ trimBefore,
819
+ trimAfter
820
+ }) => {
821
+ const parentSequence = useContext(Internals4.SequenceContext);
822
+ const startsAt = Internals4.useMediaStartsAt();
823
+ const { registerSequence, unregisterSequence } = useContext(Internals4.SequenceManager);
824
+ const [sequenceId] = useState(() => String(Math.random()));
825
+ const [mediaId] = useState(() => String(Math.random()));
826
+ const frame = useCurrentFrame();
827
+ const {
828
+ volumes,
829
+ duration,
830
+ doesVolumeChange,
831
+ nonce,
832
+ rootId,
833
+ isStudio,
834
+ finalDisplayName
835
+ } = Internals4.useBasicMediaInTimeline({
836
+ volume,
837
+ mediaVolume,
838
+ mediaType,
839
+ src,
840
+ displayName,
841
+ trimBefore,
842
+ trimAfter,
843
+ playbackRate
844
+ });
845
+ useEffect(() => {
846
+ if (!src) {
847
+ throw new Error("No src passed");
691
848
  }
692
- }
693
- maybeResumeFromBuffering(currentBufferDuration) {
694
- if (!this.isCurrentlyBuffering())
849
+ if (!isStudio && window.process?.env?.NODE_ENV !== "test") {
695
850
  return;
696
- const now = performance.now();
697
- const bufferingDuration = now - this.bufferingStartedAtMs;
698
- const minTimeElapsed = bufferingDuration >= this.minBufferingTimeoutMs;
699
- const bufferHealthy = currentBufferDuration >= this.HEALTHY_BUFER_THRESHOLD_SECONDS;
700
- if (minTimeElapsed && bufferHealthy) {
701
- Internals4.Log.trace({ logLevel: this.logLevel, tag: "@remotion/media" }, `[MediaPlayer] Resuming from buffering after ${bufferingDuration}ms - buffer recovered`);
702
- this.setBufferingState(false);
703
851
  }
704
- }
705
- maybeForceResumeFromBuffering() {
706
- if (!this.isCurrentlyBuffering())
852
+ if (!showInTimeline) {
707
853
  return;
708
- const now = performance.now();
709
- const bufferingDuration = now - this.bufferingStartedAtMs;
710
- const forceTimeout = bufferingDuration > this.minBufferingTimeoutMs * 10;
711
- if (forceTimeout) {
712
- Internals4.Log.trace({ logLevel: this.logLevel, tag: "@remotion/media" }, `[MediaPlayer] Force resuming from buffering after ${bufferingDuration}ms`);
713
- this.setBufferingState(false);
714
854
  }
715
- }
716
- runAudioIterator = async (startFromSecond, audioAsyncId) => {
717
- if (!this.hasAudio() || !this.audioBufferIterator)
718
- return;
719
- try {
720
- let totalBufferDuration = 0;
721
- let isFirstBuffer = true;
722
- this.audioIteratorStarted = true;
723
- while (true) {
724
- if (audioAsyncId !== this.audioAsyncId) {
725
- return;
726
- }
727
- const BUFFERING_TIMEOUT_MS = 50;
728
- let result;
729
- try {
730
- result = await withTimeout(this.audioBufferIterator.next(), BUFFERING_TIMEOUT_MS, "Iterator timeout");
731
- } catch (error) {
732
- if (error instanceof TimeoutError && !this.mediaEnded) {
733
- this.setBufferingState(true);
734
- }
735
- await sleep(10);
736
- continue;
737
- }
738
- if (result.done || !result.value) {
739
- this.mediaEnded = true;
740
- break;
741
- }
742
- const { buffer, timestamp, duration } = result.value;
743
- totalBufferDuration += duration;
744
- this.audioBufferHealth = Math.max(0, totalBufferDuration / this.playbackRate);
745
- this.maybeResumeFromBuffering(totalBufferDuration / this.playbackRate);
746
- if (this.playing) {
747
- if (isFirstBuffer) {
748
- this.audioSyncAnchor = this.sharedAudioContext.currentTime - timestamp;
749
- isFirstBuffer = false;
750
- }
751
- if (timestamp < startFromSecond - AUDIO_BUFFER_TOLERANCE_THRESHOLD) {
752
- continue;
753
- }
754
- this.scheduleAudioChunk(buffer, timestamp);
755
- }
756
- const playbackTime = this.getPlaybackTime();
757
- if (playbackTime === null) {
758
- continue;
759
- }
760
- if (timestamp - playbackTime >= 1) {
761
- await new Promise((resolve) => {
762
- const check = () => {
763
- const currentPlaybackTime = this.getPlaybackTime();
764
- if (currentPlaybackTime !== null && timestamp - currentPlaybackTime < 1) {
765
- resolve();
766
- } else {
767
- requestAnimationFrame(check);
768
- }
769
- };
770
- check();
771
- });
772
- }
773
- }
774
- } catch (error) {
775
- Internals4.Log.error({ logLevel: this.logLevel, tag: "@remotion/media" }, "[MediaPlayer] Failed to run audio iterator", error);
855
+ const loopIteration = loopDisplay ? Math.floor(frame / loopDisplay.durationInFrames) : 0;
856
+ if (loopDisplay) {
857
+ registerSequence({
858
+ type: "sequence",
859
+ premountDisplay,
860
+ postmountDisplay,
861
+ parent: parentSequence?.id ?? null,
862
+ displayName: finalDisplayName,
863
+ rootId,
864
+ showInTimeline: true,
865
+ nonce,
866
+ loopDisplay,
867
+ stack,
868
+ from: 0,
869
+ duration,
870
+ id: sequenceId
871
+ });
776
872
  }
873
+ registerSequence({
874
+ type: mediaType,
875
+ src,
876
+ id: mediaId,
877
+ duration: loopDisplay?.durationInFrames ?? duration,
878
+ from: loopDisplay ? loopIteration * loopDisplay.durationInFrames : 0,
879
+ parent: loopDisplay ? sequenceId : parentSequence?.id ?? null,
880
+ displayName: finalDisplayName,
881
+ rootId,
882
+ volume: volumes,
883
+ showInTimeline: true,
884
+ nonce,
885
+ startMediaFrom: 0 - startsAt,
886
+ doesVolumeChange,
887
+ loopDisplay: undefined,
888
+ playbackRate,
889
+ stack,
890
+ premountDisplay: null,
891
+ postmountDisplay: null
892
+ });
893
+ return () => {
894
+ if (loopDisplay) {
895
+ unregisterSequence(sequenceId);
896
+ }
897
+ unregisterSequence(mediaId);
898
+ };
899
+ }, [
900
+ doesVolumeChange,
901
+ duration,
902
+ finalDisplayName,
903
+ isStudio,
904
+ loopDisplay,
905
+ mediaId,
906
+ mediaType,
907
+ nonce,
908
+ parentSequence?.id,
909
+ playbackRate,
910
+ postmountDisplay,
911
+ premountDisplay,
912
+ registerSequence,
913
+ rootId,
914
+ sequenceId,
915
+ showInTimeline,
916
+ src,
917
+ stack,
918
+ startsAt,
919
+ unregisterSequence,
920
+ volumes,
921
+ frame
922
+ ]);
923
+ return {
924
+ id: mediaId
777
925
  };
778
- }
926
+ };
779
927
 
780
928
  // src/audio/audio-for-preview.tsx
781
929
  import { jsx } from "react/jsx-runtime";
@@ -878,7 +1026,9 @@ var NewAudioForPreview = ({
878
1026
  fps: videoConfig.fps,
879
1027
  canvas: null,
880
1028
  playbackRate,
881
- audioStreamIndex: audioStreamIndex ?? 0
1029
+ audioStreamIndex: audioStreamIndex ?? 0,
1030
+ debugOverlay: false,
1031
+ bufferState: buffer
882
1032
  });
883
1033
  mediaPlayerRef.current = player;
884
1034
  player.initialize(currentTimeRef.current).then((result) => {
@@ -950,7 +1100,8 @@ var NewAudioForPreview = ({
950
1100
  playbackRate,
951
1101
  videoConfig.fps,
952
1102
  audioStreamIndex,
953
- disallowFallbackToHtml5Audio
1103
+ disallowFallbackToHtml5Audio,
1104
+ buffer
954
1105
  ]);
955
1106
  useEffect2(() => {
956
1107
  const audioPlayer = mediaPlayerRef.current;
@@ -1297,7 +1448,7 @@ var warnAboutMatroskaOnce = (src, logLevel) => {
1297
1448
  warned[src] = true;
1298
1449
  Internals6.Log.warn({ logLevel, tag: "@remotion/media" }, `Audio from ${src} will need to be read from the beginning. https://www.remotion.dev/docs/media/support#matroska-limitation`);
1299
1450
  };
1300
- var makeAudioIterator = ({
1451
+ var makeAudioIterator2 = ({
1301
1452
  audioSampleSink,
1302
1453
  isMatroska,
1303
1454
  startTimestamp,
@@ -1422,7 +1573,7 @@ var makeAudioManager = () => {
1422
1573
  actualMatroskaTimestamps,
1423
1574
  logLevel
1424
1575
  }) => {
1425
- const iterator = makeAudioIterator({
1576
+ const iterator = makeAudioIterator2({
1426
1577
  audioSampleSink,
1427
1578
  isMatroska,
1428
1579
  startTimestamp: timeInSeconds,
@@ -1584,20 +1735,43 @@ import {
1584
1735
 
1585
1736
  // src/video-extraction/keyframe-bank.ts
1586
1737
  import { Internals as Internals8 } from "remotion";
1587
- var roundTo4Digits = (timestamp) => {
1588
- return Math.round(timestamp * 1000) / 1000;
1589
- };
1590
1738
  var makeKeyframeBank = ({
1591
1739
  startTimestampInSeconds,
1592
1740
  endTimestampInSeconds,
1593
1741
  sampleIterator,
1594
- logLevel: parentLogLevel
1742
+ logLevel: parentLogLevel,
1743
+ src
1595
1744
  }) => {
1596
1745
  Internals8.Log.verbose({ logLevel: parentLogLevel, tag: "@remotion/media" }, `Creating keyframe bank from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
1597
1746
  const frames = {};
1598
1747
  const frameTimestamps = [];
1599
1748
  let lastUsed = Date.now();
1600
1749
  let allocationSize = 0;
1750
+ const deleteFramesBeforeTimestamp = ({
1751
+ logLevel,
1752
+ timestampInSeconds
1753
+ }) => {
1754
+ const deletedTimestamps = [];
1755
+ for (const frameTimestamp of frameTimestamps.slice()) {
1756
+ const isLast = frameTimestamp === frameTimestamps[frameTimestamps.length - 1];
1757
+ if (isLast) {
1758
+ continue;
1759
+ }
1760
+ if (frameTimestamp < timestampInSeconds) {
1761
+ if (!frames[frameTimestamp]) {
1762
+ continue;
1763
+ }
1764
+ allocationSize -= frames[frameTimestamp].allocationSize();
1765
+ frameTimestamps.splice(frameTimestamps.indexOf(frameTimestamp), 1);
1766
+ frames[frameTimestamp].close();
1767
+ delete frames[frameTimestamp];
1768
+ deletedTimestamps.push(frameTimestamp);
1769
+ }
1770
+ }
1771
+ if (deletedTimestamps.length > 0) {
1772
+ Internals8.Log.verbose({ logLevel, tag: "@remotion/media" }, `Deleted ${deletedTimestamps.length} frame${deletedTimestamps.length === 1 ? "" : "s"} ${renderTimestampRange(deletedTimestamps)} for src ${src} because it is lower than ${timestampInSeconds}. Remaining: ${renderTimestampRange(frameTimestamps)}`);
1773
+ }
1774
+ };
1601
1775
  const hasDecodedEnoughForTimestamp = (timestamp) => {
1602
1776
  const lastFrameTimestamp = frameTimestamps[frameTimestamps.length - 1];
1603
1777
  if (!lastFrameTimestamp) {
@@ -1615,8 +1789,8 @@ var makeKeyframeBank = ({
1615
1789
  allocationSize += frame.allocationSize();
1616
1790
  lastUsed = Date.now();
1617
1791
  };
1618
- const ensureEnoughFramesForTimestamp = async (timestamp) => {
1619
- while (!hasDecodedEnoughForTimestamp(timestamp)) {
1792
+ const ensureEnoughFramesForTimestamp = async (timestampInSeconds) => {
1793
+ while (!hasDecodedEnoughForTimestamp(timestampInSeconds)) {
1620
1794
  const sample = await sampleIterator.next();
1621
1795
  if (sample.value) {
1622
1796
  addFrame(sample.value);
@@ -1624,6 +1798,10 @@ var makeKeyframeBank = ({
1624
1798
  if (sample.done) {
1625
1799
  break;
1626
1800
  }
1801
+ deleteFramesBeforeTimestamp({
1802
+ logLevel: parentLogLevel,
1803
+ timestampInSeconds: timestampInSeconds - SAFE_BACK_WINDOW_IN_SECONDS
1804
+ });
1627
1805
  }
1628
1806
  lastUsed = Date.now();
1629
1807
  };
@@ -1658,6 +1836,7 @@ var makeKeyframeBank = ({
1658
1836
  }
1659
1837
  return null;
1660
1838
  });
1839
+ let framesDeleted = 0;
1661
1840
  for (const frameTimestamp of frameTimestamps) {
1662
1841
  if (!frames[frameTimestamp]) {
1663
1842
  continue;
@@ -1665,34 +1844,10 @@ var makeKeyframeBank = ({
1665
1844
  allocationSize -= frames[frameTimestamp].allocationSize();
1666
1845
  frames[frameTimestamp].close();
1667
1846
  delete frames[frameTimestamp];
1847
+ framesDeleted++;
1668
1848
  }
1669
1849
  frameTimestamps.length = 0;
1670
- };
1671
- const deleteFramesBeforeTimestamp = ({
1672
- logLevel,
1673
- src,
1674
- timestampInSeconds
1675
- }) => {
1676
- const deletedTimestamps = [];
1677
- for (const frameTimestamp of frameTimestamps.slice()) {
1678
- const isLast = frameTimestamp === frameTimestamps[frameTimestamps.length - 1];
1679
- if (isLast) {
1680
- continue;
1681
- }
1682
- if (frameTimestamp < timestampInSeconds) {
1683
- if (!frames[frameTimestamp]) {
1684
- continue;
1685
- }
1686
- allocationSize -= frames[frameTimestamp].allocationSize();
1687
- frameTimestamps.splice(frameTimestamps.indexOf(frameTimestamp), 1);
1688
- frames[frameTimestamp].close();
1689
- delete frames[frameTimestamp];
1690
- deletedTimestamps.push(frameTimestamp);
1691
- }
1692
- }
1693
- if (deletedTimestamps.length > 0) {
1694
- Internals8.Log.verbose({ logLevel, tag: "@remotion/media" }, `Deleted ${deletedTimestamps.length} frame${deletedTimestamps.length === 1 ? "" : "s"} ${renderTimestampRange(deletedTimestamps)} for src ${src} because it is lower than ${timestampInSeconds}. Remaining: ${renderTimestampRange(frameTimestamps)}`);
1695
- }
1850
+ return { framesDeleted };
1696
1851
  };
1697
1852
  const getOpenFrameCount = () => {
1698
1853
  return {
@@ -1711,13 +1866,11 @@ var makeKeyframeBank = ({
1711
1866
  queue = queue.then(() => getFrameFromTimestamp(timestamp));
1712
1867
  return queue;
1713
1868
  },
1714
- prepareForDeletion: (logLevel) => {
1715
- queue = queue.then(() => prepareForDeletion(logLevel));
1716
- return queue;
1717
- },
1869
+ prepareForDeletion,
1718
1870
  hasTimestampInSecond,
1719
1871
  addFrame,
1720
1872
  deleteFramesBeforeTimestamp,
1873
+ src,
1721
1874
  getOpenFrameCount,
1722
1875
  getLastUsed
1723
1876
  };
@@ -1829,7 +1982,8 @@ var getFramesSinceKeyframe = async ({
1829
1982
  packetSink,
1830
1983
  videoSampleSink,
1831
1984
  startPacket,
1832
- logLevel
1985
+ logLevel,
1986
+ src
1833
1987
  }) => {
1834
1988
  const nextKeyPacket = await packetSink.getNextKeyPacket(startPacket, {
1835
1989
  verifyKeyPackets: true
@@ -1839,7 +1993,8 @@ var getFramesSinceKeyframe = async ({
1839
1993
  startTimestampInSeconds: startPacket.timestamp,
1840
1994
  endTimestampInSeconds: nextKeyPacket ? nextKeyPacket.timestamp : Infinity,
1841
1995
  sampleIterator,
1842
- logLevel
1996
+ logLevel,
1997
+ src
1843
1998
  });
1844
1999
  return keyframeBank;
1845
2000
  };
@@ -1891,6 +2046,7 @@ var makeKeyframeManager = () => {
1891
2046
  const getTheKeyframeBankMostInThePast = async () => {
1892
2047
  let mostInThePast = null;
1893
2048
  let mostInThePastBank = null;
2049
+ let numberOfBanks = 0;
1894
2050
  for (const src in sources) {
1895
2051
  for (const b in sources[src]) {
1896
2052
  const bank = await sources[src][b];
@@ -1899,26 +2055,38 @@ var makeKeyframeManager = () => {
1899
2055
  mostInThePast = lastUsed;
1900
2056
  mostInThePastBank = { src, bank };
1901
2057
  }
2058
+ numberOfBanks++;
1902
2059
  }
1903
2060
  }
1904
2061
  if (!mostInThePastBank) {
1905
2062
  throw new Error("No keyframe bank found");
1906
2063
  }
1907
- return mostInThePastBank;
2064
+ return { mostInThePastBank, numberOfBanks };
1908
2065
  };
1909
2066
  const deleteOldestKeyframeBank = async (logLevel) => {
1910
- const { bank: mostInThePastBank, src: mostInThePastSrc } = await getTheKeyframeBankMostInThePast();
2067
+ const {
2068
+ mostInThePastBank: { bank: mostInThePastBank, src: mostInThePastSrc },
2069
+ numberOfBanks
2070
+ } = await getTheKeyframeBankMostInThePast();
2071
+ if (numberOfBanks < 2) {
2072
+ return { finish: true };
2073
+ }
1911
2074
  if (mostInThePastBank) {
1912
- await mostInThePastBank.prepareForDeletion(logLevel);
2075
+ const { framesDeleted } = mostInThePastBank.prepareForDeletion(logLevel);
1913
2076
  delete sources[mostInThePastSrc][mostInThePastBank.startTimestampInSeconds];
1914
- Internals9.Log.verbose({ logLevel, tag: "@remotion/media" }, `Deleted frames for src ${mostInThePastSrc} from ${mostInThePastBank.startTimestampInSeconds}sec to ${mostInThePastBank.endTimestampInSeconds}sec to free up memory.`);
2077
+ Internals9.Log.verbose({ logLevel, tag: "@remotion/media" }, `Deleted ${framesDeleted} frames for src ${mostInThePastSrc} from ${mostInThePastBank.startTimestampInSeconds}sec to ${mostInThePastBank.endTimestampInSeconds}sec to free up memory.`);
1915
2078
  }
2079
+ return { finish: false };
1916
2080
  };
1917
2081
  const ensureToStayUnderMaxCacheSize = async (logLevel) => {
1918
2082
  let cacheStats = await getTotalCacheStats();
1919
2083
  const maxCacheSize = getMaxVideoCacheSize(logLevel);
1920
2084
  while (cacheStats.totalSize > maxCacheSize) {
1921
- await deleteOldestKeyframeBank(logLevel);
2085
+ const { finish } = await deleteOldestKeyframeBank(logLevel);
2086
+ if (finish) {
2087
+ break;
2088
+ }
2089
+ Internals9.Log.verbose({ logLevel, tag: "@remotion/media" }, "Deleted oldest keyframe bank to stay under max cache size", (cacheStats.totalSize / 1024 / 1024).toFixed(1), "out of", (maxCacheSize / 1024 / 1024).toFixed(1));
1922
2090
  cacheStats = await getTotalCacheStats();
1923
2091
  }
1924
2092
  };
@@ -1936,14 +2104,13 @@ var makeKeyframeManager = () => {
1936
2104
  const bank = await sources[src][startTimeInSeconds];
1937
2105
  const { endTimestampInSeconds, startTimestampInSeconds } = bank;
1938
2106
  if (endTimestampInSeconds < threshold) {
1939
- await bank.prepareForDeletion(logLevel);
2107
+ bank.prepareForDeletion(logLevel);
1940
2108
  Internals9.Log.verbose({ logLevel, tag: "@remotion/media" }, `[Video] Cleared frames for src ${src} from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
1941
2109
  delete sources[src][startTimeInSeconds];
1942
2110
  } else {
1943
2111
  bank.deleteFramesBeforeTimestamp({
1944
2112
  timestampInSeconds: threshold,
1945
- logLevel,
1946
- src
2113
+ logLevel
1947
2114
  });
1948
2115
  }
1949
2116
  }
@@ -1973,7 +2140,8 @@ var makeKeyframeManager = () => {
1973
2140
  packetSink,
1974
2141
  videoSampleSink,
1975
2142
  startPacket,
1976
- logLevel
2143
+ logLevel,
2144
+ src
1977
2145
  });
1978
2146
  addKeyframeBank({ src, bank: newKeyframeBank, startTimestampInSeconds });
1979
2147
  return newKeyframeBank;
@@ -1988,7 +2156,8 @@ var makeKeyframeManager = () => {
1988
2156
  packetSink,
1989
2157
  videoSampleSink,
1990
2158
  startPacket,
1991
- logLevel
2159
+ logLevel,
2160
+ src
1992
2161
  });
1993
2162
  addKeyframeBank({ src, bank: replacementKeybank, startTimestampInSeconds });
1994
2163
  return replacementKeybank;
@@ -2062,7 +2231,7 @@ var getTotalCacheStats = async () => {
2062
2231
  };
2063
2232
  };
2064
2233
  var getUncachedMaxCacheSize = (logLevel) => {
2065
- if (window.remotion_mediaCacheSizeInBytes !== undefined && window.remotion_mediaCacheSizeInBytes !== null) {
2234
+ if (typeof window !== "undefined" && window.remotion_mediaCacheSizeInBytes !== undefined && window.remotion_mediaCacheSizeInBytes !== null) {
2066
2235
  if (window.remotion_mediaCacheSizeInBytes < 240 * 1024 * 1024) {
2067
2236
  cancelRender(new Error(`The minimum value for the "mediaCacheSizeInBytes" prop is 240MB (${240 * 1024 * 1024}), got: ${window.remotion_mediaCacheSizeInBytes}`));
2068
2237
  }
@@ -2072,7 +2241,7 @@ var getUncachedMaxCacheSize = (logLevel) => {
2072
2241
  Internals10.Log.verbose({ logLevel, tag: "@remotion/media" }, `Using cache size set using "mediaCacheSizeInBytes": ${(window.remotion_mediaCacheSizeInBytes / 1024 / 1024).toFixed(1)} MB`);
2073
2242
  return window.remotion_mediaCacheSizeInBytes;
2074
2243
  }
2075
- if (window.remotion_initialMemoryAvailable !== undefined && window.remotion_initialMemoryAvailable !== null) {
2244
+ if (typeof window !== "undefined" && window.remotion_initialMemoryAvailable !== undefined && window.remotion_initialMemoryAvailable !== null) {
2076
2245
  const value = window.remotion_initialMemoryAvailable / 2;
2077
2246
  if (value < 500 * 1024 * 1024) {
2078
2247
  Internals10.Log.verbose({ logLevel, tag: "@remotion/media" }, `Using cache size set based on minimum value of 500MB (which is more than half of the available system memory!)`);
@@ -2446,7 +2615,7 @@ var extractFrameAndAudio = async ({
2446
2615
  };
2447
2616
 
2448
2617
  // src/video-extraction/extract-frame-via-broadcast-channel.ts
2449
- if (window.remotion_broadcastChannel && window.remotion_isMainTab) {
2618
+ if (typeof window !== "undefined" && window.remotion_broadcastChannel && window.remotion_isMainTab) {
2450
2619
  window.remotion_broadcastChannel.addEventListener("message", async (event) => {
2451
2620
  const data = event.data;
2452
2621
  if (data.type === "request") {
@@ -2654,7 +2823,7 @@ var AudioForRendering = ({
2654
2823
  loopVolumeCurveBehavior,
2655
2824
  delayRenderRetries,
2656
2825
  delayRenderTimeoutInMilliseconds,
2657
- logLevel = window.remotion_logLevel,
2826
+ logLevel,
2658
2827
  loop,
2659
2828
  fallbackHtml5AudioProps,
2660
2829
  audioStreamIndex,
@@ -2712,7 +2881,7 @@ var AudioForRendering = ({
2712
2881
  timeInSeconds: timestamp,
2713
2882
  durationInSeconds,
2714
2883
  playbackRate: playbackRate ?? 1,
2715
- logLevel,
2884
+ logLevel: logLevel ?? window.remotion_logLevel,
2716
2885
  includeAudio: shouldRenderAudio,
2717
2886
  includeVideo: false,
2718
2887
  isClientSideRendering: environment.isClientSideRendering,
@@ -2726,7 +2895,10 @@ var AudioForRendering = ({
2726
2895
  if (disallowFallbackToHtml5Audio) {
2727
2896
  cancelRender2(new Error(`Unknown container format ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
2728
2897
  }
2729
- Internals12.Log.warn({ logLevel, tag: "@remotion/media" }, `Unknown container format for ${src} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <Html5Audio>`);
2898
+ Internals12.Log.warn({
2899
+ logLevel: logLevel ?? window.remotion_logLevel,
2900
+ tag: "@remotion/media"
2901
+ }, `Unknown container format for ${src} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <Html5Audio>`);
2730
2902
  setReplaceWithHtml5Audio(true);
2731
2903
  return;
2732
2904
  }
@@ -2734,7 +2906,10 @@ var AudioForRendering = ({
2734
2906
  if (disallowFallbackToHtml5Audio) {
2735
2907
  cancelRender2(new Error(`Cannot decode ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
2736
2908
  }
2737
- Internals12.Log.warn({ logLevel, tag: "@remotion/media" }, `Cannot decode ${src}, falling back to <Html5Audio>`);
2909
+ Internals12.Log.warn({
2910
+ logLevel: logLevel ?? window.remotion_logLevel,
2911
+ tag: "@remotion/media"
2912
+ }, `Cannot decode ${src}, falling back to <Html5Audio>`);
2738
2913
  setReplaceWithHtml5Audio(true);
2739
2914
  return;
2740
2915
  }
@@ -2745,7 +2920,10 @@ var AudioForRendering = ({
2745
2920
  if (disallowFallbackToHtml5Audio) {
2746
2921
  cancelRender2(new Error(`Cannot decode ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
2747
2922
  }
2748
- Internals12.Log.warn({ logLevel, tag: "@remotion/media" }, `Network error fetching ${src}, falling back to <Html5Audio>`);
2923
+ Internals12.Log.warn({
2924
+ logLevel: logLevel ?? window.remotion_logLevel,
2925
+ tag: "@remotion/media"
2926
+ }, `Network error fetching ${src}, falling back to <Html5Audio>`);
2749
2927
  setReplaceWithHtml5Audio(true);
2750
2928
  return;
2751
2929
  }
@@ -2861,7 +3039,14 @@ Internals13.addSequenceStackTraces(Audio);
2861
3039
  import { Internals as Internals16, useRemotionEnvironment as useRemotionEnvironment4 } from "remotion";
2862
3040
 
2863
3041
  // src/video/video-for-preview.tsx
2864
- import { useContext as useContext4, useEffect as useEffect3, useMemo as useMemo4, useRef as useRef2, useState as useState4 } from "react";
3042
+ import {
3043
+ useContext as useContext4,
3044
+ useEffect as useEffect3,
3045
+ useLayoutEffect as useLayoutEffect2,
3046
+ useMemo as useMemo4,
3047
+ useRef as useRef2,
3048
+ useState as useState4
3049
+ } from "react";
2865
3050
  import { Html5Video, Internals as Internals14, useBufferState as useBufferState2, useCurrentFrame as useCurrentFrame4 } from "remotion";
2866
3051
  import { jsx as jsx4 } from "react/jsx-runtime";
2867
3052
  var {
@@ -2895,7 +3080,8 @@ var VideoForPreview = ({
2895
3080
  stack,
2896
3081
  disallowFallbackToOffthreadVideo,
2897
3082
  fallbackOffthreadVideoProps,
2898
- audioStreamIndex
3083
+ audioStreamIndex,
3084
+ debugOverlay
2899
3085
  }) => {
2900
3086
  const src = usePreload2(unpreloadedSrc);
2901
3087
  const canvasRef = useRef2(null);
@@ -2947,9 +3133,6 @@ var VideoForPreview = ({
2947
3133
  if (!videoConfig) {
2948
3134
  throw new Error("No video config found");
2949
3135
  }
2950
- if (!src) {
2951
- throw new TypeError("No `src` was passed to <NewVideoForPreview>.");
2952
- }
2953
3136
  const currentTime = frame / videoConfig.fps;
2954
3137
  const currentTimeRef = useRef2(currentTime);
2955
3138
  currentTimeRef.current = currentTime;
@@ -2972,10 +3155,15 @@ var VideoForPreview = ({
2972
3155
  trimBefore,
2973
3156
  fps: videoConfig.fps,
2974
3157
  playbackRate,
2975
- audioStreamIndex
3158
+ audioStreamIndex,
3159
+ debugOverlay,
3160
+ bufferState: buffer
2976
3161
  });
2977
3162
  mediaPlayerRef.current = player;
2978
3163
  player.initialize(currentTimeRef.current).then((result) => {
3164
+ if (result.type === "disposed") {
3165
+ return;
3166
+ }
2979
3167
  if (result.type === "unknown-container-format") {
2980
3168
  if (disallowFallbackToOffthreadVideo) {
2981
3169
  throw new Error(`Unknown container format ${preloadedSrc}, and 'disallowFallbackToOffthreadVideo' was set.`);
@@ -3013,16 +3201,16 @@ var VideoForPreview = ({
3013
3201
  setMediaDurationInSeconds(result.durationInSeconds);
3014
3202
  }
3015
3203
  }).catch((error) => {
3016
- Internals14.Log.error({ logLevel, tag: "@remotion/media" }, "[NewVideoForPreview] Failed to initialize MediaPlayer", error);
3204
+ Internals14.Log.error({ logLevel, tag: "@remotion/media" }, "[VideoForPreview] Failed to initialize MediaPlayer", error);
3017
3205
  setShouldFallbackToNativeVideo(true);
3018
3206
  });
3019
3207
  } catch (error) {
3020
- Internals14.Log.error({ logLevel, tag: "@remotion/media" }, "[NewVideoForPreview] MediaPlayer initialization failed", error);
3208
+ Internals14.Log.error({ logLevel, tag: "@remotion/media" }, "[VideoForPreview] MediaPlayer initialization failed", error);
3021
3209
  setShouldFallbackToNativeVideo(true);
3022
3210
  }
3023
3211
  return () => {
3024
3212
  if (mediaPlayerRef.current) {
3025
- Internals14.Log.trace({ logLevel, tag: "@remotion/media" }, `[NewVideoForPreview] Disposing MediaPlayer`);
3213
+ Internals14.Log.trace({ logLevel, tag: "@remotion/media" }, `[VideoForPreview] Disposing MediaPlayer`);
3026
3214
  mediaPlayerRef.current.dispose();
3027
3215
  mediaPlayerRef.current = null;
3028
3216
  }
@@ -3039,7 +3227,9 @@ var VideoForPreview = ({
3039
3227
  videoConfig.fps,
3040
3228
  playbackRate,
3041
3229
  disallowFallbackToOffthreadVideo,
3042
- audioStreamIndex
3230
+ audioStreamIndex,
3231
+ debugOverlay,
3232
+ buffer
3043
3233
  ]);
3044
3234
  const classNameValue = useMemo4(() => {
3045
3235
  return [Internals14.OBJECTFIT_CONTAIN_CLASS_NAME, className].filter(Internals14.truthy).join(" ");
@@ -3050,18 +3240,18 @@ var VideoForPreview = ({
3050
3240
  return;
3051
3241
  if (playing) {
3052
3242
  mediaPlayer.play().catch((error) => {
3053
- Internals14.Log.error({ logLevel, tag: "@remotion/media" }, "[NewVideoForPreview] Failed to play", error);
3243
+ Internals14.Log.error({ logLevel, tag: "@remotion/media" }, "[VideoForPreview] Failed to play", error);
3054
3244
  });
3055
3245
  } else {
3056
3246
  mediaPlayer.pause();
3057
3247
  }
3058
3248
  }, [playing, logLevel, mediaPlayerReady]);
3059
- useEffect3(() => {
3249
+ useLayoutEffect2(() => {
3060
3250
  const mediaPlayer = mediaPlayerRef.current;
3061
3251
  if (!mediaPlayer || !mediaPlayerReady)
3062
3252
  return;
3063
3253
  mediaPlayer.seekTo(currentTime);
3064
- Internals14.Log.trace({ logLevel, tag: "@remotion/media" }, `[NewVideoForPreview] Updating target time to ${currentTime.toFixed(3)}s`);
3254
+ Internals14.Log.trace({ logLevel, tag: "@remotion/media" }, `[VideoForPreview] Updating target time to ${currentTime.toFixed(3)}s`);
3065
3255
  }, [currentTime, logLevel, mediaPlayerReady]);
3066
3256
  useEffect3(() => {
3067
3257
  const mediaPlayer = mediaPlayerRef.current;
@@ -3071,11 +3261,11 @@ var VideoForPreview = ({
3071
3261
  const unsubscribe = mediaPlayer.onBufferingChange((newBufferingState) => {
3072
3262
  if (newBufferingState && !currentBlock) {
3073
3263
  currentBlock = buffer.delayPlayback();
3074
- Internals14.Log.trace({ logLevel, tag: "@remotion/media" }, "[NewVideoForPreview] MediaPlayer buffering - blocking Remotion playback");
3264
+ Internals14.Log.trace({ logLevel, tag: "@remotion/media" }, "[VideoForPreview] MediaPlayer buffering - blocking Remotion playback");
3075
3265
  } else if (!newBufferingState && currentBlock) {
3076
3266
  currentBlock.unblock();
3077
3267
  currentBlock = null;
3078
- Internals14.Log.trace({ logLevel, tag: "@remotion/media" }, "[NewVideoForPreview] MediaPlayer unbuffering - unblocking Remotion playback");
3268
+ Internals14.Log.trace({ logLevel, tag: "@remotion/media" }, "[VideoForPreview] MediaPlayer unbuffering - unblocking Remotion playback");
3079
3269
  }
3080
3270
  });
3081
3271
  return () => {
@@ -3100,6 +3290,13 @@ var VideoForPreview = ({
3100
3290
  }
3101
3291
  mediaPlayer.setVolume(userPreferredVolume);
3102
3292
  }, [userPreferredVolume, mediaPlayerReady]);
3293
+ useEffect3(() => {
3294
+ const mediaPlayer = mediaPlayerRef.current;
3295
+ if (!mediaPlayer || !mediaPlayerReady) {
3296
+ return;
3297
+ }
3298
+ mediaPlayer.setDebugOverlay(debugOverlay);
3299
+ }, [debugOverlay, mediaPlayerReady]);
3103
3300
  const effectivePlaybackRate = useMemo4(() => playbackRate * globalPlaybackRate, [playbackRate, globalPlaybackRate]);
3104
3301
  useEffect3(() => {
3105
3302
  const mediaPlayer = mediaPlayerRef.current;
@@ -3168,7 +3365,7 @@ var VideoForPreview = ({
3168
3365
  // src/video/video-for-rendering.tsx
3169
3366
  import {
3170
3367
  useContext as useContext5,
3171
- useLayoutEffect as useLayoutEffect2,
3368
+ useLayoutEffect as useLayoutEffect3,
3172
3369
  useMemo as useMemo5,
3173
3370
  useRef as useRef3,
3174
3371
  useState as useState5
@@ -3183,26 +3380,6 @@ import {
3183
3380
  useRemotionEnvironment as useRemotionEnvironment3,
3184
3381
  useVideoConfig as useVideoConfig2
3185
3382
  } from "remotion";
3186
-
3187
- // ../core/src/calculate-media-duration.ts
3188
- var calculateMediaDuration = ({
3189
- trimAfter,
3190
- mediaDurationInFrames,
3191
- playbackRate,
3192
- trimBefore
3193
- }) => {
3194
- let duration = mediaDurationInFrames;
3195
- if (typeof trimAfter !== "undefined") {
3196
- duration = trimAfter;
3197
- }
3198
- if (typeof trimBefore !== "undefined") {
3199
- duration -= trimBefore;
3200
- }
3201
- const actualDuration = duration / playbackRate;
3202
- return Math.floor(actualDuration);
3203
- };
3204
-
3205
- // src/video/video-for-rendering.tsx
3206
3383
  import { jsx as jsx5 } from "react/jsx-runtime";
3207
3384
  var VideoForRendering = ({
3208
3385
  volume: volumeProp,
@@ -3245,7 +3422,9 @@ var VideoForRendering = ({
3245
3422
  const { delayRender, continueRender } = useDelayRender2();
3246
3423
  const canvasRef = useRef3(null);
3247
3424
  const [replaceWithOffthreadVideo, setReplaceWithOffthreadVideo] = useState5(false);
3248
- useLayoutEffect2(() => {
3425
+ const audioEnabled = Internals15.useAudioEnabled();
3426
+ const videoEnabled = Internals15.useVideoEnabled();
3427
+ useLayoutEffect3(() => {
3249
3428
  if (!canvasRef.current) {
3250
3429
  return;
3251
3430
  }
@@ -3259,7 +3438,7 @@ var VideoForRendering = ({
3259
3438
  timeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? undefined
3260
3439
  });
3261
3440
  const shouldRenderAudio = (() => {
3262
- if (!window.remotion_audioEnabled) {
3441
+ if (!audioEnabled) {
3263
3442
  return false;
3264
3443
  }
3265
3444
  if (muted) {
@@ -3274,7 +3453,7 @@ var VideoForRendering = ({
3274
3453
  playbackRate,
3275
3454
  logLevel,
3276
3455
  includeAudio: shouldRenderAudio,
3277
- includeVideo: window.remotion_videoEnabled,
3456
+ includeVideo: videoEnabled,
3278
3457
  isClientSideRendering: environment.isClientSideRendering,
3279
3458
  loop,
3280
3459
  audioStreamIndex,
@@ -3344,7 +3523,7 @@ var VideoForRendering = ({
3344
3523
  context.canvas.style.aspectRatio = `${context.canvas.width} / ${context.canvas.height}`;
3345
3524
  context.drawImage(imageBitmap, 0, 0);
3346
3525
  imageBitmap.close();
3347
- } else if (window.remotion_videoEnabled) {
3526
+ } else if (videoEnabled) {
3348
3527
  const context = canvasRef.current?.getContext("2d", {
3349
3528
  alpha: true
3350
3529
  });
@@ -3412,7 +3591,9 @@ var VideoForRendering = ({
3412
3591
  disallowFallbackToOffthreadVideo,
3413
3592
  toneFrequency,
3414
3593
  trimAfterValue,
3415
- trimBeforeValue
3594
+ trimBeforeValue,
3595
+ audioEnabled,
3596
+ videoEnabled
3416
3597
  ]);
3417
3598
  const classNameValue = useMemo5(() => {
3418
3599
  return [Internals15.OBJECTFIT_CONTAIN_CLASS_NAME, className].filter(Internals15.truthy).join(" ");
@@ -3458,7 +3639,7 @@ var VideoForRendering = ({
3458
3639
  }
3459
3640
  return /* @__PURE__ */ jsx5(Loop, {
3460
3641
  layout: "none",
3461
- durationInFrames: calculateMediaDuration({
3642
+ durationInFrames: Internals15.calculateMediaDuration({
3462
3643
  trimAfter: trimAfterValue,
3463
3644
  mediaDurationInFrames: replaceWithOffthreadVideo.durationInSeconds * fps,
3464
3645
  playbackRate,
@@ -3500,7 +3681,8 @@ var InnerVideo = ({
3500
3681
  volume,
3501
3682
  stack,
3502
3683
  toneFrequency,
3503
- showInTimeline
3684
+ showInTimeline,
3685
+ debugOverlay
3504
3686
  }) => {
3505
3687
  const environment = useRemotionEnvironment4();
3506
3688
  if (typeof src !== "string") {
@@ -3561,7 +3743,8 @@ var InnerVideo = ({
3561
3743
  trimBefore: trimBeforeValue,
3562
3744
  stack: stack ?? null,
3563
3745
  disallowFallbackToOffthreadVideo,
3564
- fallbackOffthreadVideoProps
3746
+ fallbackOffthreadVideoProps,
3747
+ debugOverlay: debugOverlay ?? false
3565
3748
  });
3566
3749
  };
3567
3750
  var Video = ({
@@ -3585,7 +3768,8 @@ var Video = ({
3585
3768
  trimBefore,
3586
3769
  volume,
3587
3770
  stack,
3588
- toneFrequency
3771
+ toneFrequency,
3772
+ debugOverlay
3589
3773
  }) => {
3590
3774
  return /* @__PURE__ */ jsx6(InnerVideo, {
3591
3775
  audioStreamIndex: audioStreamIndex ?? 0,
@@ -3594,7 +3778,7 @@ var Video = ({
3594
3778
  delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null,
3595
3779
  disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false,
3596
3780
  fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {},
3597
- logLevel: logLevel ?? window.remotion_logLevel,
3781
+ logLevel: logLevel ?? (typeof window !== "undefined" ? window.remotion_logLevel : "info"),
3598
3782
  loop: loop ?? false,
3599
3783
  loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? "repeat",
3600
3784
  muted: muted ?? false,
@@ -3608,7 +3792,8 @@ var Video = ({
3608
3792
  trimBefore,
3609
3793
  volume: volume ?? 1,
3610
3794
  toneFrequency: toneFrequency ?? 1,
3611
- stack
3795
+ stack,
3796
+ debugOverlay: debugOverlay ?? false
3612
3797
  });
3613
3798
  };
3614
3799
  Internals16.addSequenceStackTraces(Video);