@remotion/media 4.0.356 → 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 (55) 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 +32 -15
  4. package/dist/audio/audio.js +7 -49
  5. package/dist/audio/props.d.ts +8 -14
  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 +4 -1
  9. package/dist/audio-extraction/audio-iterator.js +22 -10
  10. package/dist/audio-extraction/audio-manager.d.ts +8 -37
  11. package/dist/audio-extraction/audio-manager.js +35 -8
  12. package/dist/audio-extraction/extract-audio.d.ts +9 -2
  13. package/dist/audio-extraction/extract-audio.js +28 -15
  14. package/dist/caches.d.ts +9 -44
  15. package/dist/convert-audiodata/apply-tonefrequency.js +0 -1
  16. package/dist/convert-audiodata/combine-audiodata.js +2 -23
  17. package/dist/convert-audiodata/convert-audiodata.d.ts +1 -5
  18. package/dist/convert-audiodata/convert-audiodata.js +16 -24
  19. package/dist/convert-audiodata/wsola.js +1 -1
  20. package/dist/esm/index.mjs +2681 -2162
  21. package/dist/extract-frame-and-audio.d.ts +6 -7
  22. package/dist/extract-frame-and-audio.js +28 -19
  23. package/dist/get-sink-weak.d.ts +1 -1
  24. package/dist/get-sink-weak.js +3 -11
  25. package/dist/get-sink.d.ts +13 -0
  26. package/dist/get-sink.js +15 -0
  27. package/dist/get-time-in-seconds.d.ts +10 -0
  28. package/dist/get-time-in-seconds.js +25 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +1 -0
  31. package/dist/is-network-error.d.ts +6 -0
  32. package/dist/is-network-error.js +17 -0
  33. package/dist/render-timestamp-range.d.ts +1 -0
  34. package/dist/render-timestamp-range.js +9 -0
  35. package/dist/video/media-player.d.ts +28 -7
  36. package/dist/video/media-player.js +123 -58
  37. package/dist/video/props.d.ts +1 -0
  38. package/dist/video/resolve-playback-time.d.ts +8 -0
  39. package/dist/video/resolve-playback-time.js +22 -0
  40. package/dist/video/video-for-preview.d.ts +8 -0
  41. package/dist/video/video-for-preview.js +113 -90
  42. package/dist/video/video-for-rendering.d.ts +3 -0
  43. package/dist/video/video-for-rendering.js +58 -25
  44. package/dist/video/video.js +6 -10
  45. package/dist/video-extraction/extract-frame-via-broadcast-channel.d.ts +18 -6
  46. package/dist/video-extraction/extract-frame-via-broadcast-channel.js +21 -7
  47. package/dist/video-extraction/extract-frame.d.ts +20 -2
  48. package/dist/video-extraction/extract-frame.js +40 -9
  49. package/dist/video-extraction/get-frames-since-keyframe.d.ts +5 -3
  50. package/dist/video-extraction/get-frames-since-keyframe.js +7 -4
  51. package/dist/video-extraction/keyframe-bank.d.ts +3 -2
  52. package/dist/video-extraction/keyframe-bank.js +32 -12
  53. package/dist/video-extraction/keyframe-manager.d.ts +3 -8
  54. package/dist/video-extraction/keyframe-manager.js +25 -10
  55. package/package.json +4 -4
@@ -1,50 +1,53 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { ALL_FORMATS, Input, UrlSource } from 'mediabunny';
3
2
  import { useContext, useEffect, useMemo, useRef, useState } from 'react';
4
- import { Internals, Loop, useBufferState, useCurrentFrame, useVideoConfig, } from 'remotion';
3
+ import { Internals, useBufferState, useCurrentFrame, Video } from 'remotion';
5
4
  import { MediaPlayer } from './media-player';
6
- const { useUnsafeVideoConfig, Timeline, SharedAudioContext, useMediaMutedState, useMediaVolumeState, useFrameForVolumeProp, evaluateVolume, warnAboutTooHighVolume, usePreload, } = Internals;
7
- const calculateLoopDuration = ({ endAt, mediaDuration, playbackRate, startFrom, }) => {
8
- let duration = mediaDuration;
9
- if (typeof endAt !== 'undefined') {
10
- duration = endAt;
11
- }
12
- if (typeof startFrom !== 'undefined') {
13
- duration -= startFrom;
14
- }
15
- const actualDuration = duration / playbackRate;
16
- return Math.floor(actualDuration);
17
- };
18
- const NewVideoForPreview = ({ src, style, playbackRate, logLevel, className, muted, volume, loopVolumeCurveBehavior, onVideoFrame, }) => {
5
+ const { useUnsafeVideoConfig, Timeline, SharedAudioContext, useMediaMutedState, useMediaVolumeState, useFrameForVolumeProp, evaluateVolume, warnAboutTooHighVolume, usePreload, useMediaInTimeline, SequenceContext, } = Internals;
6
+ const NewVideoForPreview = ({ src, style, playbackRate, logLevel, className, muted, volume, loopVolumeCurveBehavior, onVideoFrame, showInTimeline, loop, name, trimAfter, trimBefore, stack, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, audioStreamIndex, }) => {
19
7
  const canvasRef = useRef(null);
20
8
  const videoConfig = useUnsafeVideoConfig();
21
9
  const frame = useCurrentFrame();
22
10
  const mediaPlayerRef = useRef(null);
23
11
  const [mediaPlayerReady, setMediaPlayerReady] = useState(false);
12
+ const [shouldFallbackToNativeVideo, setShouldFallbackToNativeVideo] = useState(false);
24
13
  const [playing] = Timeline.usePlayingState();
25
14
  const timelineContext = useContext(Timeline.TimelineContext);
26
15
  const globalPlaybackRate = timelineContext.playbackRate;
27
16
  const sharedAudioContext = useContext(SharedAudioContext);
28
17
  const buffer = useBufferState();
29
- const delayHandleRef = useRef(null);
30
18
  const [mediaMuted] = useMediaMutedState();
31
19
  const [mediaVolume] = useMediaVolumeState();
32
- const volumePropFrame = useFrameForVolumeProp(loopVolumeCurveBehavior ?? 'repeat');
20
+ const volumePropFrame = useFrameForVolumeProp(loopVolumeCurveBehavior);
33
21
  const userPreferredVolume = evaluateVolume({
34
22
  frame: volumePropFrame,
35
23
  volume,
36
24
  mediaVolume,
37
25
  });
38
26
  warnAboutTooHighVolume(userPreferredVolume);
27
+ const [timelineId] = useState(() => String(Math.random()));
28
+ const parentSequence = useContext(SequenceContext);
29
+ useMediaInTimeline({
30
+ volume,
31
+ mediaVolume,
32
+ mediaType: 'video',
33
+ src,
34
+ playbackRate,
35
+ displayName: name ?? null,
36
+ id: timelineId,
37
+ stack,
38
+ showInTimeline,
39
+ premountDisplay: parentSequence?.premountDisplay ?? null,
40
+ postmountDisplay: parentSequence?.postmountDisplay ?? null,
41
+ });
39
42
  if (!videoConfig) {
40
43
  throw new Error('No video config found');
41
44
  }
42
45
  if (!src) {
43
46
  throw new TypeError('No `src` was passed to <NewVideoForPreview>.');
44
47
  }
45
- const actualFps = videoConfig.fps / playbackRate;
46
- const currentTime = frame / actualFps;
47
- const [initialTimestamp] = useState(currentTime);
48
+ const currentTime = frame / videoConfig.fps;
49
+ const currentTimeRef = useRef(currentTime);
50
+ currentTimeRef.current = currentTime;
48
51
  const preloadedSrc = usePreload(src);
49
52
  useEffect(() => {
50
53
  if (!canvasRef.current)
@@ -59,34 +62,84 @@ const NewVideoForPreview = ({ src, style, playbackRate, logLevel, className, mut
59
62
  src: preloadedSrc,
60
63
  logLevel,
61
64
  sharedAudioContext: sharedAudioContext.audioContext,
65
+ loop,
66
+ trimAfterSeconds: trimAfter ? trimAfter / videoConfig.fps : undefined,
67
+ trimBeforeSeconds: trimBefore
68
+ ? trimBefore / videoConfig.fps
69
+ : undefined,
70
+ playbackRate,
71
+ audioStreamIndex,
62
72
  });
63
73
  mediaPlayerRef.current = player;
64
74
  player
65
- .initialize(initialTimestamp)
66
- .then(() => {
67
- setMediaPlayerReady(true);
68
- Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[NewVideoForPreview] MediaPlayer initialized successfully`);
75
+ .initialize(currentTimeRef.current)
76
+ .then((result) => {
77
+ if (result.type === 'unknown-container-format') {
78
+ if (disallowFallbackToOffthreadVideo) {
79
+ throw new Error(`Unknown container format ${preloadedSrc}, and 'disallowFallbackToOffthreadVideo' was set.`);
80
+ }
81
+ Internals.Log.warn({ logLevel, tag: '@remotion/media' }, `Unknown container format for ${preloadedSrc} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <OffthreadVideo>`);
82
+ setShouldFallbackToNativeVideo(true);
83
+ return;
84
+ }
85
+ if (result.type === 'network-error') {
86
+ if (disallowFallbackToOffthreadVideo) {
87
+ throw new Error(`Network error fetching ${preloadedSrc}, and 'disallowFallbackToOffthreadVideo' was set.`);
88
+ }
89
+ Internals.Log.warn({ logLevel, tag: '@remotion/media' }, `Network error fetching ${preloadedSrc}, falling back to <OffthreadVideo>`);
90
+ setShouldFallbackToNativeVideo(true);
91
+ return;
92
+ }
93
+ if (result.type === 'cannot-decode') {
94
+ if (disallowFallbackToOffthreadVideo) {
95
+ throw new Error(`Cannot decode ${preloadedSrc}, and 'disallowFallbackToOffthreadVideo' was set.`);
96
+ }
97
+ Internals.Log.warn({ logLevel, tag: '@remotion/media' }, `Cannot decode ${preloadedSrc}, falling back to <OffthreadVideo>`);
98
+ setShouldFallbackToNativeVideo(true);
99
+ return;
100
+ }
101
+ if (result.type === 'no-tracks') {
102
+ if (disallowFallbackToOffthreadVideo) {
103
+ throw new Error(`No video or audio tracks found for ${preloadedSrc}, and 'disallowFallbackToOffthreadVideo' was set.`);
104
+ }
105
+ Internals.Log.warn({ logLevel, tag: '@remotion/media' }, `No video or audio tracks found for ${preloadedSrc}, falling back to <OffthreadVideo>`);
106
+ setShouldFallbackToNativeVideo(true);
107
+ return;
108
+ }
109
+ if (result.type === 'success') {
110
+ setMediaPlayerReady(true);
111
+ }
69
112
  })
70
113
  .catch((error) => {
71
114
  Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] Failed to initialize MediaPlayer', error);
115
+ setShouldFallbackToNativeVideo(true);
72
116
  });
73
117
  }
74
118
  catch (error) {
75
119
  Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] MediaPlayer initialization failed', error);
120
+ setShouldFallbackToNativeVideo(true);
76
121
  }
77
122
  return () => {
78
- if (delayHandleRef.current) {
79
- delayHandleRef.current.unblock();
80
- delayHandleRef.current = null;
81
- }
82
123
  if (mediaPlayerRef.current) {
83
124
  Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[NewVideoForPreview] Disposing MediaPlayer`);
84
125
  mediaPlayerRef.current.dispose();
85
126
  mediaPlayerRef.current = null;
86
127
  }
87
128
  setMediaPlayerReady(false);
129
+ setShouldFallbackToNativeVideo(false);
88
130
  };
89
- }, [preloadedSrc, logLevel, sharedAudioContext, initialTimestamp]);
131
+ }, [
132
+ preloadedSrc,
133
+ logLevel,
134
+ sharedAudioContext,
135
+ loop,
136
+ trimAfter,
137
+ trimBefore,
138
+ videoConfig.fps,
139
+ playbackRate,
140
+ disallowFallbackToOffthreadVideo,
141
+ audioStreamIndex,
142
+ ]);
90
143
  const classNameValue = useMemo(() => {
91
144
  return [Internals.OBJECTFIT_CONTAIN_CLASS_NAME, className]
92
145
  .filter(Internals.truthy)
@@ -116,17 +169,25 @@ const NewVideoForPreview = ({ src, style, playbackRate, logLevel, className, mut
116
169
  const mediaPlayer = mediaPlayerRef.current;
117
170
  if (!mediaPlayer || !mediaPlayerReady)
118
171
  return;
119
- mediaPlayer.onBufferingChange((newBufferingState) => {
120
- if (newBufferingState && !delayHandleRef.current) {
121
- delayHandleRef.current = buffer.delayPlayback();
172
+ let currentBlock = null;
173
+ const unsubscribe = mediaPlayer.onBufferingChange((newBufferingState) => {
174
+ if (newBufferingState && !currentBlock) {
175
+ currentBlock = buffer.delayPlayback();
122
176
  Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] MediaPlayer buffering - blocking Remotion playback');
123
177
  }
124
- else if (!newBufferingState && delayHandleRef.current) {
125
- delayHandleRef.current.unblock();
126
- delayHandleRef.current = null;
178
+ else if (!newBufferingState && currentBlock) {
179
+ currentBlock.unblock();
180
+ currentBlock = null;
127
181
  Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] MediaPlayer unbuffering - unblocking Remotion playback');
128
182
  }
129
183
  });
184
+ return () => {
185
+ unsubscribe();
186
+ if (currentBlock) {
187
+ currentBlock.unblock();
188
+ currentBlock = null;
189
+ }
190
+ };
130
191
  }, [mediaPlayerReady, buffer, logLevel]);
131
192
  const effectiveMuted = muted || mediaMuted || userPreferredVolume <= 0;
132
193
  useEffect(() => {
@@ -148,71 +209,33 @@ const NewVideoForPreview = ({ src, style, playbackRate, logLevel, className, mut
148
209
  if (!mediaPlayer || !mediaPlayerReady) {
149
210
  return;
150
211
  }
151
- mediaPlayer.setPlaybackRate(effectivePlaybackRate).catch((error) => {
152
- Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] Failed to set playback rate', error);
153
- });
212
+ mediaPlayer.setPlaybackRate(effectivePlaybackRate);
154
213
  }, [effectivePlaybackRate, mediaPlayerReady, logLevel]);
155
214
  useEffect(() => {
156
215
  const mediaPlayer = mediaPlayerRef.current;
157
216
  if (!mediaPlayer || !mediaPlayerReady) {
158
217
  return;
159
218
  }
160
- if (onVideoFrame) {
161
- mediaPlayer.onVideoFrame(onVideoFrame);
162
- }
163
- }, [onVideoFrame, mediaPlayerReady]);
164
- return (_jsx("canvas", { ref: canvasRef, width: videoConfig.width, height: videoConfig.height, style: style, className: classNameValue }));
165
- };
166
- const VideoForPreviewWithDuration = ({ className, durationInSeconds, logLevel, loopVolumeCurveBehavior, muted, onVideoFrame, playbackRate, src, style, volume, loop, name, trimAfter, trimBefore, }) => {
167
- const { fps } = useVideoConfig();
168
- if (loop) {
169
- if (!Number.isFinite(durationInSeconds) || durationInSeconds === null) {
170
- return (_jsx(VideoForPreviewWithDuration, { loop: false, className: className, durationInSeconds: durationInSeconds, logLevel: logLevel, loopVolumeCurveBehavior: loopVolumeCurveBehavior, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: src, style: style, volume: volume, name: name, trimAfter: trimAfter, trimBefore: trimBefore }));
171
- }
172
- const mediaDuration = durationInSeconds * fps;
173
- return (_jsx(Loop, { durationInFrames: calculateLoopDuration({
174
- endAt: trimAfter,
175
- mediaDuration,
176
- playbackRate: playbackRate ?? 1,
177
- startFrom: trimBefore,
178
- }), layout: "none", name: name, children: _jsx(VideoForPreviewWithDuration, { loop: false, className: className, durationInSeconds: durationInSeconds, logLevel: logLevel, loopVolumeCurveBehavior: loopVolumeCurveBehavior, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: src, style: style, volume: volume, name: name, trimAfter: trimAfter, trimBefore: trimBefore }) }));
179
- }
180
- return (_jsx(NewVideoForPreview, { src: src, style: style, playbackRate: playbackRate, logLevel: logLevel, muted: muted, volume: volume, loopVolumeCurveBehavior: loopVolumeCurveBehavior, onVideoFrame: onVideoFrame, className: className }));
181
- };
182
- export const VideoForPreview = ({ className, loop, src, logLevel, muted, name, volume, loopVolumeCurveBehavior, onVideoFrame, playbackRate, style, }) => {
183
- const preloadedSrc = usePreload(src);
184
- const [durationInSeconds, setDurationInSeconds] = useState(null);
219
+ mediaPlayer.setLoop(loop);
220
+ }, [loop, mediaPlayerReady]);
185
221
  useEffect(() => {
186
- if (!loop) {
222
+ const mediaPlayer = mediaPlayerRef.current;
223
+ if (!mediaPlayer || !mediaPlayerReady || !onVideoFrame) {
187
224
  return;
188
225
  }
189
- let cancelled = false;
190
- const computeDuration = async () => {
191
- const urlSource = new UrlSource(preloadedSrc);
192
- const input = new Input({
193
- source: urlSource,
194
- formats: ALL_FORMATS,
195
- });
196
- try {
197
- const duration = await input.computeDuration();
198
- if (!cancelled) {
199
- setDurationInSeconds(duration);
200
- }
201
- }
202
- catch (error) {
203
- Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] Failed to compute duration', error);
204
- }
205
- finally {
206
- input.dispose();
207
- }
208
- };
209
- computeDuration();
226
+ const unsubscribe = mediaPlayer.onVideoFrame(onVideoFrame);
210
227
  return () => {
211
- cancelled = true;
228
+ unsubscribe();
212
229
  };
213
- }, [loop, preloadedSrc, logLevel]);
214
- if (loop && durationInSeconds === null) {
215
- return null;
230
+ }, [onVideoFrame, mediaPlayerReady]);
231
+ if (shouldFallbackToNativeVideo && !disallowFallbackToOffthreadVideo) {
232
+ // <Video> will fallback to <VideoForPreview> anyway
233
+ // not using <OffthreadVideo> because it does not support looping
234
+ return (_jsx(Video, { src: src, style: style, className: className, muted: muted, volume: volume, trimAfter: trimAfter, trimBefore: trimBefore, playbackRate: playbackRate, loopVolumeCurveBehavior: loopVolumeCurveBehavior, name: name, loop: loop, showInTimeline: showInTimeline, stack: stack ?? undefined, ...fallbackOffthreadVideoProps }));
216
235
  }
217
- return (_jsx(VideoForPreviewWithDuration, { durationInSeconds: durationInSeconds, className: className, logLevel: logLevel, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: src, style: style, volume: volume, name: name, trimAfter: undefined, trimBefore: undefined, loop: loop, loopVolumeCurveBehavior: loopVolumeCurveBehavior }));
236
+ return (_jsx("canvas", { ref: canvasRef, width: videoConfig.width, height: videoConfig.height, style: style, className: classNameValue }));
237
+ };
238
+ export const VideoForPreview = ({ className, loop, src, logLevel, muted, name, volume, loopVolumeCurveBehavior, onVideoFrame, playbackRate, style, showInTimeline, trimAfter, trimBefore, stack, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, audioStreamIndex, }) => {
239
+ const preloadedSrc = usePreload(src);
240
+ return (_jsx(NewVideoForPreview, { className: className, logLevel: logLevel, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: preloadedSrc, style: style, volume: volume, name: name, trimAfter: trimAfter, trimBefore: trimBefore, loop: loop, loopVolumeCurveBehavior: loopVolumeCurveBehavior, showInTimeline: showInTimeline, stack: stack, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps, audioStreamIndex: audioStreamIndex }));
218
241
  };
@@ -19,6 +19,9 @@ type InnerVideoProps = {
19
19
  readonly audioStreamIndex: number;
20
20
  readonly disallowFallbackToOffthreadVideo: boolean;
21
21
  readonly stack: string | undefined;
22
+ readonly toneFrequency: number;
23
+ readonly trimBeforeValue: number | undefined;
24
+ readonly trimAfterValue: number | undefined;
22
25
  };
23
26
  export declare const VideoForRendering: React.FC<InnerVideoProps>;
24
27
  export {};
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useContext, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
- import { cancelRender, Internals, useCurrentFrame, useDelayRender, useRemotionEnvironment, useVideoConfig, } from 'remotion';
3
+ import { cancelRender, Internals, Loop, random, useCurrentFrame, useDelayRender, useRemotionEnvironment, useVideoConfig, } from 'remotion';
4
+ import { calculateLoopDuration } from '../../../core/src/calculate-loop';
4
5
  import { applyVolume } from '../convert-audiodata/apply-volume';
6
+ import { TARGET_SAMPLE_RATE } from '../convert-audiodata/resample-audiodata';
5
7
  import { frameForVolumeProp } from '../looped-frame';
6
8
  import { extractFrameViaBroadcastChannel } from '../video-extraction/extract-frame-via-broadcast-channel';
7
- export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted, loopVolumeCurveBehavior, delayRenderRetries, delayRenderTimeoutInMilliseconds, onVideoFrame, logLevel, loop, style, className, fallbackOffthreadVideoProps, audioStreamIndex, name, disallowFallbackToOffthreadVideo, stack, }) => {
9
+ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted, loopVolumeCurveBehavior, delayRenderRetries, delayRenderTimeoutInMilliseconds, onVideoFrame, logLevel, loop, style, className, fallbackOffthreadVideoProps, audioStreamIndex, name, disallowFallbackToOffthreadVideo, stack, toneFrequency, trimAfterValue, trimBeforeValue, }) => {
8
10
  if (!src) {
9
11
  throw new TypeError('No `src` was passed to <Video>.');
10
12
  }
@@ -13,7 +15,15 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
13
15
  const { fps } = useVideoConfig();
14
16
  const { registerRenderAsset, unregisterRenderAsset } = useContext(Internals.RenderAssetManager);
15
17
  const startsAt = Internals.useMediaStartsAt();
16
- const [id] = useState(() => `${Math.random()}`.replace('0.', ''));
18
+ const sequenceContext = useContext(Internals.SequenceContext);
19
+ // Generate a string that's as unique as possible for this asset
20
+ // but at the same time the same on all threads
21
+ const id = useMemo(() => `media-video-${random(src)}-${sequenceContext?.cumulatedFrom}-${sequenceContext?.relativeFrom}-${sequenceContext?.durationInFrames}`, [
22
+ src,
23
+ sequenceContext?.cumulatedFrom,
24
+ sequenceContext?.relativeFrom,
25
+ sequenceContext?.durationInFrames,
26
+ ]);
17
27
  const environment = useRemotionEnvironment();
18
28
  const { delayRender, continueRender } = useDelayRender();
19
29
  const canvasRef = useRef(null);
@@ -25,10 +35,9 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
25
35
  if (replaceWithOffthreadVideo) {
26
36
  return;
27
37
  }
28
- const actualFps = playbackRate ? fps / playbackRate : fps;
29
- const timestamp = frame / actualFps;
30
- const durationInSeconds = 1 / actualFps;
31
- const newHandle = delayRender(`Extracting frame number ${frame}`, {
38
+ const timestamp = frame / fps;
39
+ const durationInSeconds = 1 / fps;
40
+ const newHandle = delayRender(`Extracting frame at time ${timestamp}`, {
32
41
  retries: delayRenderRetries ?? undefined,
33
42
  timeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? undefined,
34
43
  });
@@ -45,43 +54,48 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
45
54
  src,
46
55
  timeInSeconds: timestamp,
47
56
  durationInSeconds,
48
- playbackRate: playbackRate ?? 1,
49
- logLevel: logLevel ?? 'info',
57
+ playbackRate,
58
+ logLevel,
50
59
  includeAudio: shouldRenderAudio,
51
60
  includeVideo: window.remotion_videoEnabled,
52
61
  isClientSideRendering: environment.isClientSideRendering,
53
- loop: loop ?? false,
54
- audioStreamIndex: audioStreamIndex ?? 0,
62
+ loop,
63
+ audioStreamIndex,
64
+ trimAfter: trimAfterValue,
65
+ trimBefore: trimBeforeValue,
66
+ fps,
55
67
  })
56
68
  .then((result) => {
57
- if (result === 'unknown-container-format') {
69
+ if (result.type === 'unknown-container-format') {
58
70
  if (disallowFallbackToOffthreadVideo) {
59
71
  cancelRender(new Error(`Unknown container format ${src}, and 'disallowFallbackToOffthreadVideo' was set. Failing the render.`));
60
72
  }
61
73
  if (window.remotion_isMainTab) {
62
74
  Internals.Log.info({ logLevel, tag: '@remotion/media' }, `Unknown container format for ${src} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <OffthreadVideo>`);
63
75
  }
64
- setReplaceWithOffthreadVideo(true);
76
+ setReplaceWithOffthreadVideo({ durationInSeconds: null });
65
77
  return;
66
78
  }
67
- if (result === 'cannot-decode') {
79
+ if (result.type === 'cannot-decode') {
68
80
  if (disallowFallbackToOffthreadVideo) {
69
81
  cancelRender(new Error(`Cannot decode ${src}, and 'disallowFallbackToOffthreadVideo' was set. Failing the render.`));
70
82
  }
71
83
  if (window.remotion_isMainTab) {
72
84
  Internals.Log.info({ logLevel, tag: '@remotion/media' }, `Cannot decode ${src}, falling back to <OffthreadVideo>`);
73
85
  }
74
- setReplaceWithOffthreadVideo(true);
86
+ setReplaceWithOffthreadVideo({
87
+ durationInSeconds: result.durationInSeconds,
88
+ });
75
89
  return;
76
90
  }
77
- if (result === 'network-error') {
91
+ if (result.type === 'network-error') {
78
92
  if (disallowFallbackToOffthreadVideo) {
79
93
  cancelRender(new Error(`Cannot decode ${src}, and 'disallowFallbackToOffthreadVideo' was set. Failing the render.`));
80
94
  }
81
95
  if (window.remotion_isMainTab) {
82
96
  Internals.Log.info({ logLevel, tag: '@remotion/media' }, `Network error fetching ${src}, falling back to <OffthreadVideo>`);
83
97
  }
84
- setReplaceWithOffthreadVideo(true);
98
+ setReplaceWithOffthreadVideo({ durationInSeconds: null });
85
99
  return;
86
100
  }
87
101
  const { frame: imageBitmap, audio, durationInSeconds: assetDurationInSeconds, } = result;
@@ -104,11 +118,17 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
104
118
  imageBitmap.close();
105
119
  }
106
120
  else if (window.remotion_videoEnabled) {
107
- cancelRender(new Error('No video frame found'));
121
+ // In the case of https://discord.com/channels/809501355504959528/809501355504959531/1424400511070765086
122
+ // A video that only starts at time 0.033sec
123
+ // we shall not crash here but clear the canvas
124
+ const context = canvasRef.current?.getContext('2d');
125
+ if (context) {
126
+ context.clearRect(0, 0, context.canvas.width, context.canvas.height);
127
+ }
108
128
  }
109
129
  const volumePropsFrame = frameForVolumeProp({
110
- behavior: loopVolumeCurveBehavior ?? 'repeat',
111
- loop: loop ?? false,
130
+ behavior: loopVolumeCurveBehavior,
131
+ loop,
112
132
  assetDurationInSeconds: assetDurationInSeconds ?? 0,
113
133
  fps,
114
134
  frame,
@@ -126,11 +146,10 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
126
146
  type: 'inline-audio',
127
147
  id,
128
148
  audio: Array.from(audio.data),
129
- sampleRate: audio.sampleRate,
130
- numberOfChannels: audio.numberOfChannels,
131
149
  frame: absoluteFrame,
132
150
  timestamp: audio.timestamp,
133
- duration: (audio.numberOfFrames / audio.sampleRate) * 1000000,
151
+ duration: (audio.numberOfFrames / TARGET_SAMPLE_RATE) * 1000000,
152
+ toneFrequency,
134
153
  });
135
154
  }
136
155
  continueRender(newHandle);
@@ -166,6 +185,9 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
166
185
  replaceWithOffthreadVideo,
167
186
  audioStreamIndex,
168
187
  disallowFallbackToOffthreadVideo,
188
+ toneFrequency,
189
+ trimAfterValue,
190
+ trimBeforeValue,
169
191
  ]);
170
192
  const classNameValue = useMemo(() => {
171
193
  return [Internals.OBJECTFIT_CONTAIN_CLASS_NAME, className]
@@ -173,10 +195,21 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
173
195
  .join(' ');
174
196
  }, [className]);
175
197
  if (replaceWithOffthreadVideo) {
176
- // TODO: Loop and other props
177
- return (_jsx(Internals.InnerOffthreadVideo, { src: src, playbackRate: playbackRate ?? 1, muted: muted ?? false, acceptableTimeShiftInSeconds: fallbackOffthreadVideoProps?.acceptableTimeShiftInSeconds, loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? 'repeat', delayRenderRetries: delayRenderRetries ?? undefined, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? undefined, style: style, allowAmplificationDuringRender: true, transparent: fallbackOffthreadVideoProps?.transparent ?? false, toneMapped: fallbackOffthreadVideoProps?.toneMapped ?? true, audioStreamIndex: audioStreamIndex ?? 0, name: name, className: className, onVideoFrame: onVideoFrame, volume: volumeProp, id: id, onError: fallbackOffthreadVideoProps?.onError, toneFrequency: fallbackOffthreadVideoProps?.toneFrequency ?? 1,
198
+ const fallback = (_jsx(Internals.InnerOffthreadVideo, { src: src, playbackRate: playbackRate ?? 1, muted: muted ?? false, acceptableTimeShiftInSeconds: fallbackOffthreadVideoProps?.acceptableTimeShiftInSeconds, loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? 'repeat', delayRenderRetries: delayRenderRetries ?? undefined, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? undefined, style: style, allowAmplificationDuringRender: true, transparent: fallbackOffthreadVideoProps?.transparent ?? false, toneMapped: fallbackOffthreadVideoProps?.toneMapped ?? true, audioStreamIndex: audioStreamIndex ?? 0, name: name, className: className, onVideoFrame: onVideoFrame, volume: volumeProp, id: id, onError: fallbackOffthreadVideoProps?.onError, toneFrequency: fallbackOffthreadVideoProps?.toneFrequency ?? 1,
178
199
  // these shouldn't matter during rendering / should not appear at all
179
200
  showInTimeline: false, crossOrigin: undefined, onAutoPlayError: () => undefined, pauseWhenBuffering: false, trimAfter: undefined, trimBefore: undefined, useWebAudioApi: false, startFrom: undefined, endAt: undefined, stack: stack, _remotionInternalNativeLoopPassed: false }));
201
+ if (loop) {
202
+ if (!replaceWithOffthreadVideo.durationInSeconds) {
203
+ cancelRender(new Error(`Cannot render video ${src}: @remotion/media was unable to render, and fell back to <OffthreadVideo>. Also, "loop" was set, but <OffthreadVideo> does not support looping and @remotion/media could also not determine the duration of the video.`));
204
+ }
205
+ return (_jsx(Loop, { layout: "none", durationInFrames: calculateLoopDuration({
206
+ trimAfter: trimAfterValue,
207
+ mediaDurationInFrames: replaceWithOffthreadVideo.durationInSeconds * fps,
208
+ playbackRate,
209
+ trimBefore: trimBeforeValue,
210
+ }), children: fallback }));
211
+ }
212
+ return fallback;
180
213
  }
181
214
  return _jsx("canvas", { ref: canvasRef, style: style, className: classNameValue });
182
215
  };
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Internals, Sequence, useRemotionEnvironment } from 'remotion';
2
+ import { Internals, useRemotionEnvironment } from 'remotion';
3
3
  import { VideoForPreview } from './video-for-preview';
4
4
  import { VideoForRendering } from './video-for-rendering';
5
5
  const { validateMediaTrimProps, resolveTrimProps, validateMediaProps } = Internals;
6
- const InnerVideo = ({ src, audioStreamIndex, className, delayRenderRetries, delayRenderTimeoutInMilliseconds, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, logLevel, loop, loopVolumeCurveBehavior, muted, name, onVideoFrame, playbackRate, style, trimAfter, trimBefore, volume, showInTimeline, stack, }) => {
6
+ const InnerVideo = ({ src, audioStreamIndex, className, delayRenderRetries, delayRenderTimeoutInMilliseconds, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, logLevel, loop, loopVolumeCurveBehavior, muted, name, onVideoFrame, playbackRate, style, trimAfter, trimBefore, volume, stack, toneFrequency, showInTimeline, }) => {
7
7
  const environment = useRemotionEnvironment();
8
8
  if (typeof src !== 'string') {
9
9
  throw new TypeError(`The \`<Video>\` tag requires a string for \`src\`, but got ${JSON.stringify(src)} instead.`);
@@ -20,17 +20,13 @@ const InnerVideo = ({ src, audioStreamIndex, className, delayRenderRetries, dela
20
20
  trimBefore,
21
21
  trimAfter,
22
22
  });
23
- if (typeof trimBeforeValue !== 'undefined' ||
24
- typeof trimAfterValue !== 'undefined') {
25
- return (_jsx(Sequence, { layout: "none", from: 0 - (trimBeforeValue ?? 0), showInTimeline: false, durationInFrames: trimAfterValue, name: name, children: _jsx(InnerVideo, { audioStreamIndex: audioStreamIndex, className: className, delayRenderRetries: delayRenderRetries, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo, name: name, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps, logLevel: logLevel, loop: loop, loopVolumeCurveBehavior: loopVolumeCurveBehavior, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: src, stack: stack, style: style, volume: volume, trimAfter: undefined, trimBefore: undefined, showInTimeline: showInTimeline }) }));
26
- }
27
23
  validateMediaProps({ playbackRate, volume }, 'Video');
28
24
  if (environment.isRendering) {
29
- return (_jsx(VideoForRendering, { audioStreamIndex: audioStreamIndex ?? 0, className: className, delayRenderRetries: delayRenderRetries ?? null, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false, name: name, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps, logLevel: logLevel, loop: loop, loopVolumeCurveBehavior: loopVolumeCurveBehavior, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: src, stack: stack, style: style, volume: volume }));
25
+ return (_jsx(VideoForRendering, { audioStreamIndex: audioStreamIndex ?? 0, className: className, delayRenderRetries: delayRenderRetries ?? null, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false, name: name, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps, logLevel: logLevel, loop: loop, loopVolumeCurveBehavior: loopVolumeCurveBehavior, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: src, stack: stack, style: style, volume: volume, toneFrequency: toneFrequency, trimAfterValue: trimAfterValue, trimBeforeValue: trimBeforeValue }));
30
26
  }
31
- return (_jsx(VideoForPreview, { className: className, name: name, logLevel: logLevel, loop: loop, loopVolumeCurveBehavior: loopVolumeCurveBehavior, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: src, style: style, volume: volume }));
27
+ return (_jsx(VideoForPreview, { audioStreamIndex: audioStreamIndex ?? 0, className: className, name: name, logLevel: logLevel, loop: loop, loopVolumeCurveBehavior: loopVolumeCurveBehavior, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: src, style: style, volume: volume, showInTimeline: showInTimeline, trimAfter: trimAfterValue, trimBefore: trimBeforeValue, stack: stack ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps }));
32
28
  };
33
- export const Video = ({ src, audioStreamIndex, className, delayRenderRetries, delayRenderTimeoutInMilliseconds, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, logLevel, loop, loopVolumeCurveBehavior, muted, name, onVideoFrame, playbackRate, showInTimeline, style, trimAfter, trimBefore, volume, stack, }) => {
34
- return (_jsx(InnerVideo, { audioStreamIndex: audioStreamIndex ?? 0, className: className, delayRenderRetries: delayRenderRetries ?? null, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {}, logLevel: logLevel ?? 'info', loop: loop ?? false, loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? 'repeat', muted: muted ?? false, name: name, onVideoFrame: onVideoFrame, playbackRate: playbackRate ?? 1, showInTimeline: showInTimeline ?? true, src: src, style: style ?? {}, trimAfter: trimAfter, trimBefore: trimBefore, volume: volume ?? 1, stack: stack }));
29
+ export const Video = ({ src, audioStreamIndex, className, delayRenderRetries, delayRenderTimeoutInMilliseconds, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, logLevel, loop, loopVolumeCurveBehavior, muted, name, onVideoFrame, playbackRate, showInTimeline, style, trimAfter, trimBefore, volume, stack, toneFrequency, }) => {
30
+ return (_jsx(InnerVideo, { audioStreamIndex: audioStreamIndex ?? 0, className: className, delayRenderRetries: delayRenderRetries ?? null, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {}, logLevel: logLevel ?? window.remotion_logLevel, loop: loop ?? false, loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? 'repeat', muted: muted ?? false, name: name, onVideoFrame: onVideoFrame, playbackRate: playbackRate ?? 1, showInTimeline: showInTimeline ?? true, src: src, style: style ?? {}, trimAfter: trimAfter, trimBefore: trimBefore, volume: volume ?? 1, toneFrequency: toneFrequency ?? 1, stack: stack }));
35
31
  };
36
32
  Internals.addSequenceStackTraces(Video);
@@ -1,6 +1,19 @@
1
1
  import { type LogLevel } from 'remotion';
2
2
  import type { PcmS16AudioData } from '../convert-audiodata/convert-audiodata';
3
- export declare const extractFrameViaBroadcastChannel: ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, isClientSideRendering, loop, audioStreamIndex, }: {
3
+ export type ExtractFrameViaBroadcastChannelResult = {
4
+ type: 'success';
5
+ frame: ImageBitmap | VideoFrame | null;
6
+ audio: PcmS16AudioData | null;
7
+ durationInSeconds: number | null;
8
+ } | {
9
+ type: 'cannot-decode';
10
+ durationInSeconds: number | null;
11
+ } | {
12
+ type: 'network-error';
13
+ } | {
14
+ type: 'unknown-container-format';
15
+ };
16
+ export declare const extractFrameViaBroadcastChannel: ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, isClientSideRendering, loop, audioStreamIndex, trimAfter, trimBefore, fps, }: {
4
17
  src: string;
5
18
  timeInSeconds: number;
6
19
  durationInSeconds: number;
@@ -11,8 +24,7 @@ export declare const extractFrameViaBroadcastChannel: ({ src, timeInSeconds, log
11
24
  isClientSideRendering: boolean;
12
25
  loop: boolean;
13
26
  audioStreamIndex: number;
14
- }) => Promise<{
15
- frame: ImageBitmap | VideoFrame | null;
16
- audio: PcmS16AudioData | null;
17
- durationInSeconds: number | null;
18
- } | "cannot-decode" | "network-error" | "unknown-container-format">;
27
+ trimAfter: number | undefined;
28
+ trimBefore: number | undefined;
29
+ fps: number;
30
+ }) => Promise<ExtractFrameViaBroadcastChannelResult>;
@@ -15,16 +15,20 @@ if (window.remotion_broadcastChannel && window.remotion_isMainTab) {
15
15
  includeVideo: data.includeVideo,
16
16
  loop: data.loop,
17
17
  audioStreamIndex: data.audioStreamIndex,
18
+ trimAfter: data.trimAfter,
19
+ trimBefore: data.trimBefore,
20
+ fps: data.fps,
18
21
  });
19
- if (result === 'cannot-decode') {
22
+ if (result.type === 'cannot-decode') {
20
23
  const cannotDecodeResponse = {
21
24
  type: 'response-cannot-decode',
22
25
  id: data.id,
26
+ durationInSeconds: result.durationInSeconds,
23
27
  };
24
28
  window.remotion_broadcastChannel.postMessage(cannotDecodeResponse);
25
29
  return;
26
30
  }
27
- if (result === 'network-error') {
31
+ if (result.type === 'network-error') {
28
32
  const networkErrorResponse = {
29
33
  type: 'response-network-error',
30
34
  id: data.id,
@@ -32,7 +36,7 @@ if (window.remotion_broadcastChannel && window.remotion_isMainTab) {
32
36
  window.remotion_broadcastChannel.postMessage(networkErrorResponse);
33
37
  return;
34
38
  }
35
- if (result === 'unknown-container-format') {
39
+ if (result.type === 'unknown-container-format') {
36
40
  const unknownContainerFormatResponse = {
37
41
  type: 'response-unknown-container-format',
38
42
  id: data.id,
@@ -72,7 +76,7 @@ if (window.remotion_broadcastChannel && window.remotion_isMainTab) {
72
76
  }
73
77
  });
74
78
  }
75
- export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, isClientSideRendering, loop, audioStreamIndex, }) => {
79
+ export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, isClientSideRendering, loop, audioStreamIndex, trimAfter, trimBefore, fps, }) => {
76
80
  if (isClientSideRendering || window.remotion_isMainTab) {
77
81
  return extractFrameAndAudio({
78
82
  logLevel,
@@ -84,6 +88,9 @@ export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel,
84
88
  includeVideo,
85
89
  loop,
86
90
  audioStreamIndex,
91
+ trimAfter,
92
+ trimBefore,
93
+ fps,
87
94
  });
88
95
  }
89
96
  const requestId = crypto.randomUUID();
@@ -98,6 +105,7 @@ export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel,
98
105
  }
99
106
  if (data.type === 'response-success') {
100
107
  resolve({
108
+ type: 'success',
101
109
  frame: data.frame ? data.frame : null,
102
110
  audio: data.audio ? data.audio : null,
103
111
  durationInSeconds: data.durationInSeconds
@@ -113,17 +121,20 @@ export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel,
113
121
  return;
114
122
  }
115
123
  if (data.type === 'response-cannot-decode') {
116
- resolve('cannot-decode');
124
+ resolve({
125
+ type: 'cannot-decode',
126
+ durationInSeconds: data.durationInSeconds,
127
+ });
117
128
  window.remotion_broadcastChannel.removeEventListener('message', onMessage);
118
129
  return;
119
130
  }
120
131
  if (data.type === 'response-network-error') {
121
- resolve('network-error');
132
+ resolve({ type: 'network-error' });
122
133
  window.remotion_broadcastChannel.removeEventListener('message', onMessage);
123
134
  return;
124
135
  }
125
136
  if (data.type === 'response-unknown-container-format') {
126
- resolve('unknown-container-format');
137
+ resolve({ type: 'unknown-container-format' });
127
138
  window.remotion_broadcastChannel.removeEventListener('message', onMessage);
128
139
  return;
129
140
  }
@@ -143,6 +154,9 @@ export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel,
143
154
  includeVideo,
144
155
  loop,
145
156
  audioStreamIndex,
157
+ trimAfter,
158
+ trimBefore,
159
+ fps,
146
160
  };
147
161
  window.remotion_broadcastChannel.postMessage(request);
148
162
  let timeoutId;