@remotion/media 4.0.364 → 4.0.365

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.
@@ -37,9 +37,6 @@ export declare class MediaPlayer {
37
37
  private trimAfter;
38
38
  private initialized;
39
39
  private totalDuration;
40
- private isBuffering;
41
- private onBufferingChangeCallback?;
42
- private mediaEnded;
43
40
  private debugOverlay;
44
41
  private onVideoFrameCallback?;
45
42
  private initializationPromise;
@@ -61,7 +58,6 @@ export declare class MediaPlayer {
61
58
  private input;
62
59
  private isReady;
63
60
  private hasAudio;
64
- private isCurrentlyBuffering;
65
61
  private isDisposalError;
66
62
  initialize(startTimeUnresolved: number): Promise<MediaPlayerInitResult>;
67
63
  private _initialize;
@@ -70,7 +66,7 @@ export declare class MediaPlayer {
70
66
  private seekPromiseChain;
71
67
  seekTo(time: number): Promise<void>;
72
68
  seekToDoNotCallDirectly(time: number, nonce: number): Promise<void>;
73
- play(): Promise<void>;
69
+ play(time: number): Promise<void>;
74
70
  pause(): void;
75
71
  setMuted(muted: boolean): void;
76
72
  setVolume(volume: number): void;
@@ -80,16 +76,12 @@ export declare class MediaPlayer {
80
76
  setLoop(loop: boolean): void;
81
77
  dispose(): Promise<void>;
82
78
  private getPlaybackTime;
79
+ private setPlaybackTime;
80
+ private audioChunksForAfterResuming;
83
81
  private scheduleAudioChunk;
84
- onBufferingChange(callback: (isBuffering: boolean) => void): () => void;
85
82
  onVideoFrame(callback: (frame: CanvasImageSource) => void): () => void;
86
83
  private drawFrame;
87
84
  private startAudioIterator;
88
85
  private drawDebugOverlay;
89
86
  private startVideoIterator;
90
- private bufferingStartedAtMs;
91
- private minBufferingTimeoutMs;
92
- private setBufferingState;
93
- private maybeResumeFromBuffering;
94
- private runAudioIterator;
95
87
  }
@@ -1,18 +1,17 @@
1
1
  import { ALL_FORMATS, AudioBufferSink, CanvasSink, Input, UrlSource, } from 'mediabunny';
2
2
  import { Internals } from 'remotion';
3
- import { HEALTHY_BUFFER_THRESHOLD_SECONDS, makeAudioIterator, } from './audio/audio-preview-iterator';
3
+ import { isAlreadyQueued, makeAudioIterator, } from './audio/audio-preview-iterator';
4
4
  import { drawPreviewOverlay } from './debug-overlay/preview-overlay';
5
5
  import { getTimeInSeconds } from './get-time-in-seconds';
6
6
  import { isNetworkError } from './is-network-error';
7
- import { sleep, TimeoutError, withTimeout } from './video/timeout-utils';
8
7
  import { createVideoIterator, } from './video/video-preview-iterator';
9
- const AUDIO_BUFFER_TOLERANCE_THRESHOLD = 0.1;
10
8
  export class MediaPlayer {
11
9
  constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBefore, trimAfter, playbackRate, audioStreamIndex, fps, debugOverlay, bufferState, }) {
12
10
  this.canvasSink = null;
13
11
  this.videoFrameIterator = null;
14
12
  this.debugStats = {
15
13
  videoIteratorsCreated: 0,
14
+ audioIteratorsCreated: 0,
16
15
  framesRendered: 0,
17
16
  };
18
17
  this.audioSink = null;
@@ -26,14 +25,12 @@ export class MediaPlayer {
26
25
  this.muted = false;
27
26
  this.loop = false;
28
27
  this.initialized = false;
29
- // for remotion buffer state
30
- this.isBuffering = false;
31
- this.mediaEnded = false;
32
28
  this.debugOverlay = false;
33
29
  this.initializationPromise = null;
34
30
  this.input = null;
35
31
  this.currentSeekNonce = 0;
36
32
  this.seekPromiseChain = Promise.resolve();
33
+ this.audioChunksForAfterResuming = [];
37
34
  this.drawFrame = (frame) => {
38
35
  if (!this.context) {
39
36
  throw new Error('Context not initialized');
@@ -47,22 +44,38 @@ export class MediaPlayer {
47
44
  }
48
45
  Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Drew frame ${frame.timestamp.toFixed(3)}s`);
49
46
  };
50
- this.startAudioIterator = (startFromSecond) => {
47
+ this.startAudioIterator = async (startFromSecond, nonce) => {
51
48
  if (!this.hasAudio())
52
49
  return;
53
- // Clean up existing audio iterator
54
50
  this.audioBufferIterator?.destroy();
55
- try {
56
- const iterator = makeAudioIterator(this.audioSink, startFromSecond);
57
- this.audioBufferIterator = iterator;
58
- this.runAudioIterator(startFromSecond, iterator);
59
- }
60
- catch (error) {
61
- if (this.isDisposalError()) {
51
+ this.audioChunksForAfterResuming = [];
52
+ const delayHandle = this.bufferState.delayPlayback();
53
+ const iterator = makeAudioIterator(this.audioSink, startFromSecond);
54
+ this.debugStats.audioIteratorsCreated++;
55
+ this.audioBufferIterator = iterator;
56
+ // Schedule up to 3 buffers ahead of the current time
57
+ for (let i = 0; i < 3; i++) {
58
+ const result = await iterator.getNext();
59
+ if (iterator.isDestroyed()) {
60
+ delayHandle.unblock();
61
+ return;
62
+ }
63
+ if (nonce !== this.currentSeekNonce) {
64
+ delayHandle.unblock();
65
+ return;
66
+ }
67
+ if (!result.value) {
68
+ // media ended
69
+ delayHandle.unblock();
62
70
  return;
63
71
  }
64
- Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to start audio iterator', error);
72
+ const { buffer, timestamp } = result.value;
73
+ this.audioChunksForAfterResuming.push({
74
+ buffer,
75
+ timestamp,
76
+ });
65
77
  }
78
+ delayHandle.unblock();
66
79
  };
67
80
  this.startVideoIterator = async (timeToSeek, nonce) => {
68
81
  if (!this.canvasSink) {
@@ -72,9 +85,9 @@ export class MediaPlayer {
72
85
  const iterator = createVideoIterator(timeToSeek, this.canvasSink);
73
86
  this.debugStats.videoIteratorsCreated++;
74
87
  this.videoFrameIterator = iterator;
75
- const delayHandle = this.bufferState?.delayPlayback();
88
+ const delayHandle = this.bufferState.delayPlayback();
76
89
  const frameResult = await iterator.getNext();
77
- delayHandle?.unblock();
90
+ delayHandle.unblock();
78
91
  if (iterator.isDestroyed()) {
79
92
  return;
80
93
  }
@@ -84,89 +97,11 @@ export class MediaPlayer {
84
97
  if (this.videoFrameIterator.isDestroyed()) {
85
98
  return;
86
99
  }
87
- if (frameResult.value) {
88
- this.audioSyncAnchor =
89
- this.sharedAudioContext.currentTime - frameResult.value.timestamp;
90
- this.drawFrame(frameResult.value);
91
- }
92
- else {
100
+ if (!frameResult.value) {
93
101
  // media ended
94
- }
95
- };
96
- this.bufferingStartedAtMs = null;
97
- this.minBufferingTimeoutMs = 500;
98
- this.runAudioIterator = async (startFromSecond, audioIterator) => {
99
- if (!this.hasAudio())
100
102
  return;
101
- try {
102
- let totalBufferDuration = 0;
103
- let isFirstBuffer = true;
104
- audioIterator.setAudioIteratorStarted(true);
105
- while (true) {
106
- if (audioIterator.isDestroyed()) {
107
- return;
108
- }
109
- const BUFFERING_TIMEOUT_MS = 50;
110
- let result;
111
- try {
112
- result = await withTimeout(audioIterator.getNext(), BUFFERING_TIMEOUT_MS, 'Iterator timeout');
113
- }
114
- catch (error) {
115
- if (error instanceof TimeoutError && !this.mediaEnded) {
116
- this.setBufferingState(true);
117
- }
118
- await sleep(10);
119
- continue;
120
- }
121
- // media has ended
122
- if (result.done || !result.value) {
123
- this.mediaEnded = true;
124
- break;
125
- }
126
- const { buffer, timestamp, duration } = result.value;
127
- totalBufferDuration += duration;
128
- audioIterator.setAudioBufferHealth(Math.max(0, totalBufferDuration / this.playbackRate));
129
- this.maybeResumeFromBuffering(totalBufferDuration / this.playbackRate);
130
- if (this.playing) {
131
- if (isFirstBuffer) {
132
- this.audioSyncAnchor =
133
- this.sharedAudioContext.currentTime - timestamp;
134
- isFirstBuffer = false;
135
- }
136
- // if timestamp is less than timeToSeek, skip
137
- // context: for some reason, mediabunny returns buffer at 9.984s, when requested at 10s
138
- if (timestamp < startFromSecond - AUDIO_BUFFER_TOLERANCE_THRESHOLD) {
139
- continue;
140
- }
141
- this.scheduleAudioChunk(buffer, timestamp);
142
- }
143
- const playbackTime = this.getPlaybackTime();
144
- if (playbackTime === null) {
145
- continue;
146
- }
147
- if (timestamp - playbackTime >= 1) {
148
- await new Promise((resolve) => {
149
- const check = () => {
150
- const currentPlaybackTime = this.getPlaybackTime();
151
- if (currentPlaybackTime !== null &&
152
- timestamp - currentPlaybackTime < 1) {
153
- resolve();
154
- }
155
- else {
156
- requestAnimationFrame(check);
157
- }
158
- };
159
- check();
160
- });
161
- }
162
- }
163
- }
164
- catch (error) {
165
- if (this.isDisposalError()) {
166
- return;
167
- }
168
- Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to run audio iterator', error);
169
103
  }
104
+ this.drawFrame(frameResult.value);
170
105
  };
171
106
  this.canvas = canvas ?? null;
172
107
  this.src = src;
@@ -202,9 +137,6 @@ export class MediaPlayer {
202
137
  hasAudio() {
203
138
  return Boolean(this.audioSink && this.sharedAudioContext && this.gainNode);
204
139
  }
205
- isCurrentlyBuffering() {
206
- return this.isBuffering && Boolean(this.bufferingStartedAtMs);
207
- }
208
140
  isDisposalError() {
209
141
  return this.input?.disposed === true;
210
142
  }
@@ -282,11 +214,12 @@ export class MediaPlayer {
282
214
  return { type: 'success', durationInSeconds: this.totalDuration };
283
215
  }
284
216
  if (this.sharedAudioContext) {
285
- this.audioSyncAnchor = this.sharedAudioContext.currentTime - startTime;
217
+ this.setPlaybackTime(startTime);
286
218
  }
287
219
  this.initialized = true;
288
220
  try {
289
- this.startAudioIterator(startTime);
221
+ // intentionally not awaited
222
+ this.startAudioIterator(startTime, this.currentSeekNonce);
290
223
  await this.startVideoIterator(startTime, this.currentSeekNonce);
291
224
  }
292
225
  catch (error) {
@@ -349,32 +282,95 @@ export class MediaPlayer {
349
282
  if (currentPlaybackTime === newTime) {
350
283
  return;
351
284
  }
352
- const satisfyResult = await this.videoFrameIterator?.tryToSatisfySeek(newTime);
353
- if (satisfyResult?.type === 'satisfied') {
354
- this.drawFrame(satisfyResult.frame);
355
- return;
285
+ const newAudioSyncAnchor = this.sharedAudioContext.currentTime - newTime;
286
+ const diff = Math.abs(newAudioSyncAnchor - this.audioSyncAnchor);
287
+ if (diff > 0.1) {
288
+ this.setPlaybackTime(newTime);
356
289
  }
357
- if (this.currentSeekNonce !== nonce) {
358
- return;
290
+ // Should return immediately, so it's okay to not use Promise.all here
291
+ const videoSatisfyResult = await this.videoFrameIterator?.tryToSatisfySeek(newTime);
292
+ if (videoSatisfyResult?.type === 'satisfied') {
293
+ this.drawFrame(videoSatisfyResult.frame);
294
+ }
295
+ else if (videoSatisfyResult && this.currentSeekNonce === nonce) {
296
+ this.startVideoIterator(newTime, nonce);
297
+ }
298
+ const queuedPeriod = this.audioBufferIterator?.getQueuedPeriod();
299
+ const currentTimeIsAlreadyQueued = isAlreadyQueued(newTime, queuedPeriod);
300
+ const toBeScheduled = [];
301
+ if (!currentTimeIsAlreadyQueued) {
302
+ const audioSatisfyResult = await this.audioBufferIterator?.tryToSatisfySeek(newTime);
303
+ if (this.currentSeekNonce !== nonce) {
304
+ return;
305
+ }
306
+ if (!audioSatisfyResult) {
307
+ return;
308
+ }
309
+ if (audioSatisfyResult.type === 'not-satisfied') {
310
+ await this.startAudioIterator(newTime, nonce);
311
+ return;
312
+ }
313
+ toBeScheduled.push(...audioSatisfyResult.buffers);
314
+ }
315
+ // TODO: What is this is beyond the end of the video
316
+ const nextTime = newTime +
317
+ // start of next frame
318
+ (1 / this.fps) * this.playbackRate +
319
+ // need the full duration of the next frame to be queued
320
+ (1 / this.fps) * this.playbackRate;
321
+ const nextIsAlreadyQueued = isAlreadyQueued(nextTime, queuedPeriod);
322
+ if (!nextIsAlreadyQueued) {
323
+ const audioSatisfyResult = await this.audioBufferIterator?.tryToSatisfySeek(nextTime);
324
+ if (this.currentSeekNonce !== nonce) {
325
+ return;
326
+ }
327
+ if (!audioSatisfyResult) {
328
+ return;
329
+ }
330
+ if (audioSatisfyResult.type === 'not-satisfied') {
331
+ await this.startAudioIterator(nextTime, nonce);
332
+ return;
333
+ }
334
+ toBeScheduled.push(...audioSatisfyResult.buffers);
335
+ }
336
+ for (const buffer of toBeScheduled) {
337
+ if (this.playing) {
338
+ this.scheduleAudioChunk(buffer.buffer, buffer.timestamp);
339
+ }
340
+ else {
341
+ this.audioChunksForAfterResuming.push({
342
+ buffer: buffer.buffer,
343
+ timestamp: buffer.timestamp,
344
+ });
345
+ }
359
346
  }
360
- this.mediaEnded = false;
361
- this.audioSyncAnchor = this.sharedAudioContext.currentTime - newTime;
362
- this.startAudioIterator(newTime);
363
- this.startVideoIterator(newTime, nonce);
364
347
  }
365
- async play() {
348
+ async play(time) {
366
349
  if (!this.isReady())
367
350
  return;
368
- if (!this.playing) {
369
- if (this.sharedAudioContext.state === 'suspended') {
370
- await this.sharedAudioContext.resume();
371
- }
372
- this.playing = true;
351
+ this.setPlaybackTime(time);
352
+ this.playing = true;
353
+ for (const chunk of this.audioChunksForAfterResuming) {
354
+ this.scheduleAudioChunk(chunk.buffer, chunk.timestamp);
373
355
  }
356
+ if (this.sharedAudioContext.state === 'suspended') {
357
+ await this.sharedAudioContext.resume();
358
+ }
359
+ this.audioChunksForAfterResuming.length = 0;
360
+ this.drawDebugOverlay();
374
361
  }
375
362
  pause() {
376
363
  this.playing = false;
377
- this.audioBufferIterator?.cleanupAudioQueue();
364
+ const toQueue = this.audioBufferIterator?.removeAndReturnAllQueuedAudioNodes();
365
+ if (toQueue) {
366
+ for (const chunk of toQueue) {
367
+ this.audioChunksForAfterResuming.push({
368
+ buffer: chunk.buffer,
369
+ timestamp: chunk.timestamp,
370
+ });
371
+ }
372
+ }
373
+ this.drawDebugOverlay();
378
374
  }
379
375
  setMuted(muted) {
380
376
  this.muted = muted;
@@ -426,28 +422,27 @@ export class MediaPlayer {
426
422
  getPlaybackTime() {
427
423
  return this.sharedAudioContext.currentTime - this.audioSyncAnchor;
428
424
  }
425
+ setPlaybackTime(time) {
426
+ this.audioSyncAnchor = this.sharedAudioContext.currentTime - time;
427
+ }
429
428
  scheduleAudioChunk(buffer, mediaTimestamp) {
430
- const targetTime = mediaTimestamp + this.audioSyncAnchor;
431
- const delay = targetTime - this.sharedAudioContext.currentTime;
429
+ // TODO: Might already be scheduled, and then the playback rate changes
430
+ // TODO: Playbackrate does not yet work
431
+ const targetTime = (mediaTimestamp - (this.trimBefore ?? 0) / this.fps) / this.playbackRate;
432
+ const delay = targetTime + this.audioSyncAnchor - this.sharedAudioContext.currentTime;
432
433
  const node = this.sharedAudioContext.createBufferSource();
433
434
  node.buffer = buffer;
434
435
  node.playbackRate.value = this.playbackRate;
435
436
  node.connect(this.gainNode);
436
437
  if (delay >= 0) {
437
- node.start(targetTime);
438
+ node.start(targetTime + this.audioSyncAnchor);
438
439
  }
439
440
  else {
440
441
  node.start(this.sharedAudioContext.currentTime, -delay);
441
442
  }
442
- this.audioBufferIterator?.addQueuedAudioNode(node);
443
- node.onended = () => this.audioBufferIterator?.removeQueuedAudioNode(node);
444
- }
445
- onBufferingChange(callback) {
446
- this.onBufferingChangeCallback = callback;
447
- return () => {
448
- if (this.onBufferingChangeCallback === callback) {
449
- this.onBufferingChangeCallback = undefined;
450
- }
443
+ this.audioBufferIterator.addQueuedAudioNode(node, mediaTimestamp, buffer);
444
+ node.onended = () => {
445
+ return this.audioBufferIterator.removeQueuedAudioNode(node);
451
446
  };
452
447
  }
453
448
  onVideoFrame(callback) {
@@ -465,32 +460,16 @@ export class MediaPlayer {
465
460
  if (!this.debugOverlay)
466
461
  return;
467
462
  if (this.context && this.canvas) {
468
- drawPreviewOverlay(this.context, this.debugStats, this.sharedAudioContext.state, this.sharedAudioContext.currentTime);
469
- }
470
- }
471
- setBufferingState(isBuffering) {
472
- if (this.isBuffering !== isBuffering) {
473
- this.isBuffering = isBuffering;
474
- if (isBuffering) {
475
- this.bufferingStartedAtMs = performance.now();
476
- this.onBufferingChangeCallback?.(true);
477
- }
478
- else {
479
- this.bufferingStartedAtMs = null;
480
- this.onBufferingChangeCallback?.(false);
481
- }
482
- }
483
- }
484
- maybeResumeFromBuffering(currentBufferDuration) {
485
- if (!this.isCurrentlyBuffering())
486
- return;
487
- const now = performance.now();
488
- const bufferingDuration = now - this.bufferingStartedAtMs;
489
- const minTimeElapsed = bufferingDuration >= this.minBufferingTimeoutMs;
490
- const bufferHealthy = currentBufferDuration >= HEALTHY_BUFFER_THRESHOLD_SECONDS;
491
- if (minTimeElapsed && bufferHealthy) {
492
- Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Resuming from buffering after ${bufferingDuration}ms - buffer recovered`);
493
- this.setBufferingState(false);
463
+ drawPreviewOverlay({
464
+ context: this.context,
465
+ stats: this.debugStats,
466
+ audioTime: this.sharedAudioContext.currentTime,
467
+ audioContextState: this.sharedAudioContext.state,
468
+ audioSyncAnchor: this.audioSyncAnchor,
469
+ audioIterator: this.audioBufferIterator,
470
+ audioChunksForAfterResuming: this.audioChunksForAfterResuming,
471
+ playing: this.playing,
472
+ });
494
473
  }
495
474
  }
496
475
  }
@@ -60,6 +60,11 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
60
60
  const currentTimeRef = useRef(currentTime);
61
61
  currentTimeRef.current = currentTime;
62
62
  const preloadedSrc = usePreload(src);
63
+ const buffering = useContext(Internals.BufferingContextReact);
64
+ if (!buffering) {
65
+ throw new Error('useMediaPlayback must be used inside a <BufferingContext>');
66
+ }
67
+ const isPlayerBuffering = Internals.useIsPlayerBuffering(buffering);
63
68
  useEffect(() => {
64
69
  if (!canvasRef.current)
65
70
  return;
@@ -167,15 +172,13 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
167
172
  const mediaPlayer = mediaPlayerRef.current;
168
173
  if (!mediaPlayer)
169
174
  return;
170
- if (playing) {
171
- mediaPlayer.play().catch((error) => {
172
- Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] Failed to play', error);
173
- });
175
+ if (playing && !isPlayerBuffering) {
176
+ mediaPlayer.play(currentTimeRef.current);
174
177
  }
175
178
  else {
176
179
  mediaPlayer.pause();
177
180
  }
178
- }, [playing, logLevel, mediaPlayerReady]);
181
+ }, [isPlayerBuffering, playing, logLevel, mediaPlayerReady]);
179
182
  useLayoutEffect(() => {
180
183
  const mediaPlayer = mediaPlayerRef.current;
181
184
  if (!mediaPlayer || !mediaPlayerReady)
@@ -183,30 +186,6 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
183
186
  mediaPlayer.seekTo(currentTime);
184
187
  Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[VideoForPreview] Updating target time to ${currentTime.toFixed(3)}s`);
185
188
  }, [currentTime, logLevel, mediaPlayerReady]);
186
- useEffect(() => {
187
- const mediaPlayer = mediaPlayerRef.current;
188
- if (!mediaPlayer || !mediaPlayerReady)
189
- return;
190
- let currentBlock = null;
191
- const unsubscribe = mediaPlayer.onBufferingChange((newBufferingState) => {
192
- if (newBufferingState && !currentBlock) {
193
- currentBlock = buffer.delayPlayback();
194
- Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] MediaPlayer buffering - blocking Remotion playback');
195
- }
196
- else if (!newBufferingState && currentBlock) {
197
- currentBlock.unblock();
198
- currentBlock = null;
199
- Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] MediaPlayer unbuffering - unblocking Remotion playback');
200
- }
201
- });
202
- return () => {
203
- unsubscribe();
204
- if (currentBlock) {
205
- currentBlock.unblock();
206
- currentBlock = null;
207
- }
208
- };
209
- }, [mediaPlayerReady, buffer, logLevel]);
210
189
  const effectiveMuted = isSequenceHidden || muted || mediaMuted || userPreferredVolume <= 0;
211
190
  useEffect(() => {
212
191
  const mediaPlayer = mediaPlayerRef.current;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotion/media",
3
- "version": "4.0.364",
3
+ "version": "4.0.365",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/esm/index.mjs",
@@ -21,8 +21,8 @@
21
21
  "make": "tsc -d && bun --env-file=../.env.bundle bundle.ts"
22
22
  },
23
23
  "dependencies": {
24
- "mediabunny": "1.24.1",
25
- "remotion": "4.0.364",
24
+ "mediabunny": "1.24.2",
25
+ "remotion": "4.0.365",
26
26
  "webdriverio": "9.19.2"
27
27
  },
28
28
  "peerDependencies": {
@@ -30,7 +30,7 @@
30
30
  "react-dom": ">=16.8.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@remotion/eslint-config-internal": "4.0.364",
33
+ "@remotion/eslint-config-internal": "4.0.365",
34
34
  "@vitest/browser": "^3.2.4",
35
35
  "eslint": "9.19.0",
36
36
  "react": "19.0.0",