@remotion/media 4.0.355 → 4.0.357

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.
Files changed (59) hide show
  1. package/dist/audio/audio-for-preview.d.ts +30 -0
  2. package/dist/audio/audio-for-preview.js +213 -0
  3. package/dist/audio/audio-for-rendering.js +63 -12
  4. package/dist/audio/audio.js +8 -50
  5. package/dist/audio/props.d.ts +12 -3
  6. package/dist/audio-extraction/audio-cache.d.ts +1 -1
  7. package/dist/audio-extraction/audio-cache.js +5 -1
  8. package/dist/audio-extraction/audio-iterator.d.ts +7 -3
  9. package/dist/audio-extraction/audio-iterator.js +35 -12
  10. package/dist/audio-extraction/audio-manager.d.ts +10 -38
  11. package/dist/audio-extraction/audio-manager.js +40 -11
  12. package/dist/audio-extraction/extract-audio.d.ts +11 -3
  13. package/dist/audio-extraction/extract-audio.js +37 -17
  14. package/dist/caches.d.ts +11 -45
  15. package/dist/convert-audiodata/apply-tonefrequency.d.ts +2 -0
  16. package/dist/convert-audiodata/apply-tonefrequency.js +43 -0
  17. package/dist/convert-audiodata/combine-audiodata.js +2 -23
  18. package/dist/convert-audiodata/convert-audiodata.d.ts +1 -5
  19. package/dist/convert-audiodata/convert-audiodata.js +16 -24
  20. package/dist/convert-audiodata/wsola.d.ts +13 -0
  21. package/dist/convert-audiodata/wsola.js +197 -0
  22. package/dist/esm/index.mjs +2265 -589
  23. package/dist/extract-frame-and-audio.d.ts +7 -7
  24. package/dist/extract-frame-and-audio.js +69 -26
  25. package/dist/get-sink-weak.d.ts +3 -8
  26. package/dist/get-sink-weak.js +3 -11
  27. package/dist/get-sink.d.ts +13 -0
  28. package/dist/get-sink.js +15 -0
  29. package/dist/get-time-in-seconds.d.ts +10 -0
  30. package/dist/get-time-in-seconds.js +25 -0
  31. package/dist/index.d.ts +13 -3
  32. package/dist/index.js +12 -2
  33. package/dist/is-network-error.d.ts +6 -0
  34. package/dist/is-network-error.js +17 -0
  35. package/dist/render-timestamp-range.d.ts +1 -0
  36. package/dist/render-timestamp-range.js +9 -0
  37. package/dist/video/media-player.d.ts +91 -0
  38. package/dist/video/media-player.js +484 -0
  39. package/dist/video/props.d.ts +37 -18
  40. package/dist/video/resolve-playback-time.d.ts +8 -0
  41. package/dist/video/resolve-playback-time.js +22 -0
  42. package/dist/video/timeout-utils.d.ts +2 -0
  43. package/dist/video/timeout-utils.js +18 -0
  44. package/dist/video/video-for-preview.d.ts +25 -0
  45. package/dist/video/video-for-preview.js +241 -0
  46. package/dist/video/video-for-rendering.d.ts +26 -2
  47. package/dist/video/video-for-rendering.js +95 -19
  48. package/dist/video/video.js +13 -18
  49. package/dist/video-extraction/extract-frame-via-broadcast-channel.d.ts +19 -6
  50. package/dist/video-extraction/extract-frame-via-broadcast-channel.js +67 -4
  51. package/dist/video-extraction/extract-frame.d.ts +21 -2
  52. package/dist/video-extraction/extract-frame.js +46 -9
  53. package/dist/video-extraction/get-frames-since-keyframe.d.ts +17 -10
  54. package/dist/video-extraction/get-frames-since-keyframe.js +77 -21
  55. package/dist/video-extraction/keyframe-bank.d.ts +3 -2
  56. package/dist/video-extraction/keyframe-bank.js +32 -12
  57. package/dist/video-extraction/keyframe-manager.d.ts +3 -8
  58. package/dist/video-extraction/keyframe-manager.js +25 -10
  59. package/package.json +4 -4
@@ -0,0 +1,484 @@
1
+ import { ALL_FORMATS, AudioBufferSink, CanvasSink, Input, UrlSource, } from 'mediabunny';
2
+ import { Internals } from 'remotion';
3
+ import { isNetworkError } from '../is-network-error';
4
+ import { resolvePlaybackTime } from './resolve-playback-time';
5
+ import { sleep, withTimeout } from './timeout-utils';
6
+ export const SEEK_THRESHOLD = 0.05;
7
+ const AUDIO_BUFFER_TOLERANCE_THRESHOLD = 0.1;
8
+ export class MediaPlayer {
9
+ constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBeforeSeconds, trimAfterSeconds, playbackRate, audioStreamIndex, }) {
10
+ this.canvasSink = null;
11
+ this.videoFrameIterator = null;
12
+ this.nextFrame = null;
13
+ this.audioSink = null;
14
+ this.audioBufferIterator = null;
15
+ this.queuedAudioNodes = new Set();
16
+ this.gainNode = null;
17
+ // audioDelay = mediaTimestamp + audioSyncAnchor - sharedAudioContext.currentTime
18
+ this.audioSyncAnchor = 0;
19
+ this.playing = false;
20
+ this.muted = false;
21
+ this.loop = false;
22
+ this.animationFrameId = null;
23
+ this.videoAsyncId = 0;
24
+ this.audioAsyncId = 0;
25
+ this.initialized = false;
26
+ // for remotion buffer state
27
+ this.isBuffering = false;
28
+ this.audioBufferHealth = 0;
29
+ this.audioIteratorStarted = false;
30
+ this.HEALTHY_BUFER_THRESHOLD_SECONDS = 1;
31
+ this.input = null;
32
+ this.render = () => {
33
+ if (this.isBuffering) {
34
+ this.maybeForceResumeFromBuffering();
35
+ }
36
+ if (this.shouldRenderFrame()) {
37
+ this.drawCurrentFrame();
38
+ }
39
+ if (this.playing) {
40
+ this.animationFrameId = requestAnimationFrame(this.render);
41
+ }
42
+ else {
43
+ this.animationFrameId = null;
44
+ }
45
+ };
46
+ this.startAudioIterator = async (startFromSecond) => {
47
+ if (!this.hasAudio())
48
+ return;
49
+ this.audioAsyncId++;
50
+ const currentAsyncId = this.audioAsyncId;
51
+ // Clean up existing audio iterator
52
+ await this.audioBufferIterator?.return();
53
+ this.audioIteratorStarted = false;
54
+ this.audioBufferHealth = 0;
55
+ try {
56
+ this.audioBufferIterator = this.audioSink.buffers(startFromSecond);
57
+ this.runAudioIterator(startFromSecond, currentAsyncId);
58
+ }
59
+ catch (error) {
60
+ Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to start audio iterator', error);
61
+ }
62
+ };
63
+ this.startVideoIterator = async (timeToSeek) => {
64
+ if (!this.canvasSink) {
65
+ return;
66
+ }
67
+ this.videoAsyncId++;
68
+ const currentAsyncId = this.videoAsyncId;
69
+ this.videoFrameIterator?.return().catch(() => undefined);
70
+ this.videoFrameIterator = this.canvasSink.canvases(timeToSeek);
71
+ try {
72
+ const firstFrame = (await this.videoFrameIterator.next()).value ?? null;
73
+ const secondFrame = (await this.videoFrameIterator.next()).value ?? null;
74
+ if (currentAsyncId !== this.videoAsyncId) {
75
+ return;
76
+ }
77
+ if (firstFrame && this.context) {
78
+ Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Drew initial frame ${firstFrame.timestamp.toFixed(3)}s`);
79
+ this.context.drawImage(firstFrame.canvas, 0, 0);
80
+ if (this.onVideoFrameCallback && this.canvas) {
81
+ this.onVideoFrameCallback(this.canvas);
82
+ }
83
+ }
84
+ this.nextFrame = secondFrame ?? null;
85
+ if (secondFrame) {
86
+ Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Buffered next frame ${secondFrame.timestamp.toFixed(3)}s`);
87
+ }
88
+ }
89
+ catch (error) {
90
+ Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to start video iterator', error);
91
+ }
92
+ };
93
+ this.updateNextFrame = async () => {
94
+ if (!this.videoFrameIterator) {
95
+ return;
96
+ }
97
+ try {
98
+ while (true) {
99
+ const newNextFrame = (await this.videoFrameIterator.next()).value ?? null;
100
+ if (!newNextFrame) {
101
+ break;
102
+ }
103
+ if (newNextFrame.timestamp <= this.getPlaybackTime()) {
104
+ continue;
105
+ }
106
+ else {
107
+ this.nextFrame = newNextFrame;
108
+ Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Buffered next frame ${newNextFrame.timestamp.toFixed(3)}s`);
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ catch (error) {
114
+ Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to update next frame', error);
115
+ }
116
+ };
117
+ this.bufferingStartedAtMs = null;
118
+ this.minBufferingTimeoutMs = 500;
119
+ this.runAudioIterator = async (startFromSecond, audioAsyncId) => {
120
+ if (!this.hasAudio() || !this.audioBufferIterator)
121
+ return;
122
+ try {
123
+ let totalBufferDuration = 0;
124
+ let isFirstBuffer = true;
125
+ this.audioIteratorStarted = true;
126
+ while (true) {
127
+ if (audioAsyncId !== this.audioAsyncId) {
128
+ return;
129
+ }
130
+ const BUFFERING_TIMEOUT_MS = 50;
131
+ let result;
132
+ try {
133
+ result = await withTimeout(this.audioBufferIterator.next(), BUFFERING_TIMEOUT_MS, 'Iterator timeout');
134
+ }
135
+ catch {
136
+ this.setBufferingState(true);
137
+ await sleep(10);
138
+ continue;
139
+ }
140
+ if (result.done || !result.value) {
141
+ break;
142
+ }
143
+ const { buffer, timestamp, duration } = result.value;
144
+ totalBufferDuration += duration;
145
+ this.audioBufferHealth = Math.max(0, totalBufferDuration / this.playbackRate);
146
+ this.maybeResumeFromBuffering(totalBufferDuration / this.playbackRate);
147
+ if (this.playing && !this.muted) {
148
+ if (isFirstBuffer) {
149
+ this.audioSyncAnchor =
150
+ this.sharedAudioContext.currentTime - timestamp;
151
+ isFirstBuffer = false;
152
+ }
153
+ // if timestamp is less than timeToSeek, skip
154
+ // context: for some reason, mediabunny returns buffer at 9.984s, when requested at 10s
155
+ if (timestamp < startFromSecond - AUDIO_BUFFER_TOLERANCE_THRESHOLD) {
156
+ continue;
157
+ }
158
+ this.scheduleAudioChunk(buffer, timestamp);
159
+ }
160
+ if (timestamp - this.getPlaybackTime() >= 1) {
161
+ await new Promise((resolve) => {
162
+ const check = () => {
163
+ if (timestamp - this.getPlaybackTime() < 1) {
164
+ resolve();
165
+ }
166
+ else {
167
+ requestAnimationFrame(check);
168
+ }
169
+ };
170
+ check();
171
+ });
172
+ }
173
+ }
174
+ }
175
+ catch (error) {
176
+ Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to run audio iterator', error);
177
+ }
178
+ };
179
+ this.canvas = canvas ?? null;
180
+ this.src = src;
181
+ this.logLevel = logLevel ?? window.remotion_logLevel;
182
+ this.sharedAudioContext = sharedAudioContext;
183
+ this.playbackRate = playbackRate;
184
+ this.loop = loop;
185
+ this.trimBeforeSeconds = trimBeforeSeconds;
186
+ this.trimAfterSeconds = trimAfterSeconds;
187
+ this.audioStreamIndex = audioStreamIndex ?? 0;
188
+ if (canvas) {
189
+ const context = canvas.getContext('2d', {
190
+ alpha: false,
191
+ desynchronized: true,
192
+ });
193
+ if (!context) {
194
+ throw new Error('Could not get 2D context from canvas');
195
+ }
196
+ this.context = context;
197
+ }
198
+ else {
199
+ this.context = null;
200
+ }
201
+ }
202
+ isReady() {
203
+ return this.initialized && Boolean(this.sharedAudioContext);
204
+ }
205
+ hasAudio() {
206
+ return Boolean(this.audioSink && this.sharedAudioContext && this.gainNode);
207
+ }
208
+ isCurrentlyBuffering() {
209
+ return this.isBuffering && Boolean(this.bufferingStartedAtMs);
210
+ }
211
+ async initialize(startTimeUnresolved) {
212
+ try {
213
+ const urlSource = new UrlSource(this.src);
214
+ const input = new Input({
215
+ source: urlSource,
216
+ formats: ALL_FORMATS,
217
+ });
218
+ this.input = input;
219
+ try {
220
+ await this.input.getFormat();
221
+ }
222
+ catch (error) {
223
+ const err = error;
224
+ if (isNetworkError(err)) {
225
+ throw error;
226
+ }
227
+ Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Failed to recognize format for ${this.src}`, error);
228
+ return { type: 'unknown-container-format' };
229
+ }
230
+ const [duration, videoTrack, audioTracks] = await Promise.all([
231
+ input.computeDuration(),
232
+ input.getPrimaryVideoTrack(),
233
+ input.getAudioTracks(),
234
+ ]);
235
+ this.totalDuration = duration;
236
+ const audioTrack = audioTracks[this.audioStreamIndex] ?? null;
237
+ if (!videoTrack && !audioTrack) {
238
+ return { type: 'no-tracks' };
239
+ }
240
+ if (videoTrack && this.canvas && this.context) {
241
+ const canDecode = await videoTrack.canDecode();
242
+ if (!canDecode) {
243
+ return { type: 'cannot-decode' };
244
+ }
245
+ this.canvasSink = new CanvasSink(videoTrack, {
246
+ poolSize: 2,
247
+ fit: 'contain',
248
+ });
249
+ this.canvas.width = videoTrack.displayWidth;
250
+ this.canvas.height = videoTrack.displayHeight;
251
+ }
252
+ if (audioTrack && this.sharedAudioContext) {
253
+ this.audioSink = new AudioBufferSink(audioTrack);
254
+ this.gainNode = this.sharedAudioContext.createGain();
255
+ this.gainNode.connect(this.sharedAudioContext.destination);
256
+ }
257
+ const startTime = resolvePlaybackTime({
258
+ absolutePlaybackTimeInSeconds: startTimeUnresolved,
259
+ playbackRate: this.playbackRate,
260
+ loop: this.loop,
261
+ trimBeforeInSeconds: this.trimBeforeSeconds,
262
+ trimAfterInSeconds: this.trimAfterSeconds,
263
+ mediaDurationInSeconds: this.totalDuration,
264
+ });
265
+ if (this.sharedAudioContext) {
266
+ this.audioSyncAnchor = this.sharedAudioContext.currentTime - startTime;
267
+ }
268
+ this.initialized = true;
269
+ await Promise.all([
270
+ this.startAudioIterator(startTime),
271
+ this.startVideoIterator(startTime),
272
+ ]);
273
+ this.startRenderLoop();
274
+ return { type: 'success' };
275
+ }
276
+ catch (error) {
277
+ const err = error;
278
+ if (isNetworkError(err)) {
279
+ Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Network/CORS error for ${this.src}`, err);
280
+ return { type: 'network-error' };
281
+ }
282
+ Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to initialize', error);
283
+ throw error;
284
+ }
285
+ }
286
+ cleanupAudioQueue() {
287
+ for (const node of this.queuedAudioNodes) {
288
+ node.stop();
289
+ }
290
+ this.queuedAudioNodes.clear();
291
+ }
292
+ async cleanAudioIteratorAndNodes() {
293
+ await this.audioBufferIterator?.return();
294
+ this.audioBufferIterator = null;
295
+ this.audioIteratorStarted = false;
296
+ this.audioBufferHealth = 0;
297
+ this.cleanupAudioQueue();
298
+ }
299
+ async seekTo(time) {
300
+ if (!this.isReady())
301
+ return;
302
+ const newTime = resolvePlaybackTime({
303
+ absolutePlaybackTimeInSeconds: time,
304
+ playbackRate: this.playbackRate,
305
+ loop: this.loop,
306
+ trimBeforeInSeconds: this.trimBeforeSeconds,
307
+ trimAfterInSeconds: this.trimAfterSeconds,
308
+ mediaDurationInSeconds: this.totalDuration,
309
+ });
310
+ const currentPlaybackTime = this.getPlaybackTime();
311
+ const isSignificantSeek = Math.abs(newTime - currentPlaybackTime) > SEEK_THRESHOLD;
312
+ if (isSignificantSeek) {
313
+ this.nextFrame = null;
314
+ this.audioSyncAnchor = this.sharedAudioContext.currentTime - newTime;
315
+ if (this.audioSink) {
316
+ await this.cleanAudioIteratorAndNodes();
317
+ }
318
+ await Promise.all([
319
+ this.startAudioIterator(newTime),
320
+ this.startVideoIterator(newTime),
321
+ ]);
322
+ }
323
+ if (!this.playing) {
324
+ this.render();
325
+ }
326
+ }
327
+ async play() {
328
+ if (!this.isReady())
329
+ return;
330
+ if (!this.playing) {
331
+ if (this.sharedAudioContext.state === 'suspended') {
332
+ await this.sharedAudioContext.resume();
333
+ }
334
+ this.playing = true;
335
+ this.startRenderLoop();
336
+ }
337
+ }
338
+ pause() {
339
+ this.playing = false;
340
+ this.cleanupAudioQueue();
341
+ this.stopRenderLoop();
342
+ }
343
+ setMuted(muted) {
344
+ this.muted = muted;
345
+ if (muted) {
346
+ this.cleanupAudioQueue();
347
+ }
348
+ }
349
+ setVolume(volume) {
350
+ if (!this.gainNode) {
351
+ return;
352
+ }
353
+ const appliedVolume = Math.max(0, volume);
354
+ this.gainNode.gain.value = appliedVolume;
355
+ }
356
+ setPlaybackRate(rate) {
357
+ this.playbackRate = rate;
358
+ }
359
+ setLoop(loop) {
360
+ this.loop = loop;
361
+ }
362
+ dispose() {
363
+ this.input?.dispose();
364
+ this.stopRenderLoop();
365
+ this.videoFrameIterator?.return();
366
+ this.cleanAudioIteratorAndNodes();
367
+ this.videoAsyncId++;
368
+ }
369
+ getPlaybackTime() {
370
+ const absoluteTime = this.sharedAudioContext.currentTime - this.audioSyncAnchor;
371
+ return resolvePlaybackTime({
372
+ absolutePlaybackTimeInSeconds: absoluteTime,
373
+ playbackRate: this.playbackRate,
374
+ loop: this.loop,
375
+ trimBeforeInSeconds: this.trimBeforeSeconds,
376
+ trimAfterInSeconds: this.trimAfterSeconds,
377
+ mediaDurationInSeconds: this.totalDuration,
378
+ });
379
+ }
380
+ scheduleAudioChunk(buffer, mediaTimestamp) {
381
+ const targetTime = mediaTimestamp + this.audioSyncAnchor;
382
+ const delay = targetTime - this.sharedAudioContext.currentTime;
383
+ const node = this.sharedAudioContext.createBufferSource();
384
+ node.buffer = buffer;
385
+ node.playbackRate.value = this.playbackRate;
386
+ node.connect(this.gainNode);
387
+ if (delay >= 0) {
388
+ node.start(targetTime);
389
+ }
390
+ else {
391
+ node.start(this.sharedAudioContext.currentTime, -delay);
392
+ }
393
+ this.queuedAudioNodes.add(node);
394
+ node.onended = () => this.queuedAudioNodes.delete(node);
395
+ }
396
+ onBufferingChange(callback) {
397
+ this.onBufferingChangeCallback = callback;
398
+ return () => {
399
+ if (this.onBufferingChangeCallback === callback) {
400
+ this.onBufferingChangeCallback = undefined;
401
+ }
402
+ };
403
+ }
404
+ onVideoFrame(callback) {
405
+ this.onVideoFrameCallback = callback;
406
+ if (this.initialized && callback && this.canvas) {
407
+ callback(this.canvas);
408
+ }
409
+ return () => {
410
+ if (this.onVideoFrameCallback === callback) {
411
+ this.onVideoFrameCallback = undefined;
412
+ }
413
+ };
414
+ }
415
+ canRenderVideo() {
416
+ return (!this.hasAudio() ||
417
+ (this.audioIteratorStarted &&
418
+ this.audioBufferHealth >= this.HEALTHY_BUFER_THRESHOLD_SECONDS));
419
+ }
420
+ startRenderLoop() {
421
+ if (this.animationFrameId !== null) {
422
+ return;
423
+ }
424
+ this.render();
425
+ }
426
+ stopRenderLoop() {
427
+ if (this.animationFrameId !== null) {
428
+ cancelAnimationFrame(this.animationFrameId);
429
+ this.animationFrameId = null;
430
+ }
431
+ }
432
+ shouldRenderFrame() {
433
+ return (!this.isBuffering &&
434
+ this.canRenderVideo() &&
435
+ this.nextFrame !== null &&
436
+ this.nextFrame.timestamp <= this.getPlaybackTime());
437
+ }
438
+ drawCurrentFrame() {
439
+ if (this.context && this.nextFrame) {
440
+ this.context.drawImage(this.nextFrame.canvas, 0, 0);
441
+ }
442
+ if (this.onVideoFrameCallback && this.canvas) {
443
+ this.onVideoFrameCallback(this.canvas);
444
+ }
445
+ this.nextFrame = null;
446
+ this.updateNextFrame();
447
+ }
448
+ setBufferingState(isBuffering) {
449
+ if (this.isBuffering !== isBuffering) {
450
+ this.isBuffering = isBuffering;
451
+ if (isBuffering) {
452
+ this.bufferingStartedAtMs = performance.now();
453
+ this.onBufferingChangeCallback?.(true);
454
+ }
455
+ else {
456
+ this.bufferingStartedAtMs = null;
457
+ this.onBufferingChangeCallback?.(false);
458
+ }
459
+ }
460
+ }
461
+ maybeResumeFromBuffering(currentBufferDuration) {
462
+ if (!this.isCurrentlyBuffering())
463
+ return;
464
+ const now = performance.now();
465
+ const bufferingDuration = now - this.bufferingStartedAtMs;
466
+ const minTimeElapsed = bufferingDuration >= this.minBufferingTimeoutMs;
467
+ const bufferHealthy = currentBufferDuration >= this.HEALTHY_BUFER_THRESHOLD_SECONDS;
468
+ if (minTimeElapsed && bufferHealthy) {
469
+ Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Resuming from buffering after ${bufferingDuration}ms - buffer recovered`);
470
+ this.setBufferingState(false);
471
+ }
472
+ }
473
+ maybeForceResumeFromBuffering() {
474
+ if (!this.isCurrentlyBuffering())
475
+ return;
476
+ const now = performance.now();
477
+ const bufferingDuration = now - this.bufferingStartedAtMs;
478
+ const forceTimeout = bufferingDuration > this.minBufferingTimeoutMs * 10;
479
+ if (forceTimeout) {
480
+ Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Force resuming from buffering after ${bufferingDuration}ms`);
481
+ this.setBufferingState(false);
482
+ }
483
+ }
484
+ }
@@ -1,24 +1,43 @@
1
1
  import type { LogLevel, LoopVolumeCurveBehavior, OnVideoFrame, VolumeProp } from 'remotion';
2
- export type VideoProps = {
2
+ export type FallbackOffthreadVideoProps = {
3
+ acceptableTimeShiftInSeconds?: number;
4
+ toneFrequency?: number;
5
+ transparent?: boolean;
6
+ toneMapped?: boolean;
7
+ onError?: (err: Error) => void;
8
+ };
9
+ type MandatoryVideoProps = {
3
10
  src: string;
4
- className?: string;
5
- trimBefore?: number;
6
- trimAfter?: number;
7
- volume?: VolumeProp;
8
- loopVolumeCurveBehavior?: LoopVolumeCurveBehavior;
9
- name?: string;
10
- pauseWhenBuffering?: boolean;
11
- showInTimeline?: boolean;
12
- onVideoFrame?: OnVideoFrame;
13
- playbackRate?: number;
14
- muted?: boolean;
15
- delayRenderRetries?: number;
16
- delayRenderTimeoutInMilliseconds?: number;
17
- style?: React.CSSProperties;
11
+ };
12
+ type OuterVideoProps = {
13
+ trimBefore: number | undefined;
14
+ trimAfter: number | undefined;
15
+ };
16
+ type OptionalVideoProps = {
17
+ className: string | undefined;
18
+ volume: VolumeProp;
19
+ loopVolumeCurveBehavior: LoopVolumeCurveBehavior;
20
+ name: string | undefined;
21
+ onVideoFrame: OnVideoFrame | undefined;
22
+ playbackRate: number;
23
+ muted: boolean;
24
+ delayRenderRetries: number | null;
25
+ delayRenderTimeoutInMilliseconds: number | null;
26
+ style: React.CSSProperties;
18
27
  /**
19
28
  * @deprecated For internal use only
20
29
  */
21
- stack?: string;
22
- logLevel?: LogLevel;
23
- loop?: boolean;
30
+ stack: string | undefined;
31
+ logLevel: LogLevel;
32
+ loop: boolean;
33
+ audioStreamIndex: number;
34
+ disallowFallbackToOffthreadVideo: boolean;
35
+ fallbackOffthreadVideoProps: FallbackOffthreadVideoProps;
36
+ trimAfter: number | undefined;
37
+ trimBefore: number | undefined;
38
+ toneFrequency: number;
39
+ showInTimeline: boolean;
24
40
  };
41
+ export type InnerVideoProps = MandatoryVideoProps & OuterVideoProps & OptionalVideoProps;
42
+ export type VideoProps = MandatoryVideoProps & Partial<OuterVideoProps> & Partial<OptionalVideoProps>;
43
+ export {};
@@ -0,0 +1,8 @@
1
+ export declare const resolvePlaybackTime: ({ absolutePlaybackTimeInSeconds, playbackRate, loop, trimBeforeInSeconds, trimAfterInSeconds, mediaDurationInSeconds, }: {
2
+ absolutePlaybackTimeInSeconds: number;
3
+ playbackRate: number;
4
+ loop: boolean;
5
+ trimBeforeInSeconds: number | undefined;
6
+ trimAfterInSeconds: number | undefined;
7
+ mediaDurationInSeconds: number | undefined;
8
+ }) => number;
@@ -0,0 +1,22 @@
1
+ export const resolvePlaybackTime = ({ absolutePlaybackTimeInSeconds, playbackRate, loop, trimBeforeInSeconds, trimAfterInSeconds, mediaDurationInSeconds, }) => {
2
+ const loopAfterPreliminary = loop
3
+ ? Math.min(trimAfterInSeconds ?? Infinity, mediaDurationInSeconds ?? Infinity)
4
+ : Infinity;
5
+ const loopAfterConsideringTrimBefore = loopAfterPreliminary - (trimBeforeInSeconds ?? 0);
6
+ const loopAfterConsideringPlaybackRate = loopAfterConsideringTrimBefore / playbackRate;
7
+ const timeConsideringLoop = absolutePlaybackTimeInSeconds % loopAfterConsideringPlaybackRate;
8
+ const time = timeConsideringLoop * playbackRate + (trimBeforeInSeconds ?? 0);
9
+ if (Number.isNaN(time)) {
10
+ // eslint-disable-next-line no-console
11
+ console.log({
12
+ absolutePlaybackTimeInSeconds,
13
+ playbackRate,
14
+ loop,
15
+ trimBeforeInSeconds,
16
+ trimAfterInSeconds,
17
+ mediaDurationInSeconds,
18
+ });
19
+ throw new Error('Time is NaN');
20
+ }
21
+ return time;
22
+ };
@@ -0,0 +1,2 @@
1
+ export declare const sleep: (ms: number) => Promise<unknown>;
2
+ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage?: string): Promise<T>;
@@ -0,0 +1,18 @@
1
+ /* eslint-disable no-promise-executor-return */
2
+ export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3
+ export function withTimeout(promise, timeoutMs, errorMessage = 'Operation timed out') {
4
+ let timeoutId = null;
5
+ const timeoutPromise = new Promise((_, reject) => {
6
+ timeoutId = window.setTimeout(() => {
7
+ reject(new Error(errorMessage));
8
+ }, timeoutMs);
9
+ });
10
+ return Promise.race([
11
+ promise.finally(() => {
12
+ if (timeoutId) {
13
+ clearTimeout(timeoutId);
14
+ }
15
+ }),
16
+ timeoutPromise,
17
+ ]);
18
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import type { LogLevel, LoopVolumeCurveBehavior, OnVideoFrame, VolumeProp } from 'remotion';
3
+ import type { FallbackOffthreadVideoProps } from './props';
4
+ type InnerVideoProps = {
5
+ readonly className: string | undefined;
6
+ readonly loop: boolean;
7
+ readonly src: string;
8
+ readonly logLevel: LogLevel;
9
+ readonly muted: boolean;
10
+ readonly name: string | undefined;
11
+ readonly volume: VolumeProp;
12
+ readonly loopVolumeCurveBehavior: LoopVolumeCurveBehavior;
13
+ readonly onVideoFrame: OnVideoFrame | undefined;
14
+ readonly playbackRate: number;
15
+ readonly style: React.CSSProperties;
16
+ readonly showInTimeline: boolean;
17
+ readonly trimAfter: number | undefined;
18
+ readonly trimBefore: number | undefined;
19
+ readonly stack: string | null;
20
+ readonly disallowFallbackToOffthreadVideo: boolean;
21
+ readonly fallbackOffthreadVideoProps: FallbackOffthreadVideoProps;
22
+ readonly audioStreamIndex: number;
23
+ };
24
+ export declare const VideoForPreview: React.FC<InnerVideoProps>;
25
+ export {};