@remotion/media 4.0.355 → 4.0.356

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 (37) hide show
  1. package/dist/audio/audio-for-rendering.js +37 -3
  2. package/dist/audio/audio.js +1 -1
  3. package/dist/audio/props.d.ts +15 -0
  4. package/dist/audio-extraction/audio-iterator.d.ts +3 -2
  5. package/dist/audio-extraction/audio-iterator.js +13 -2
  6. package/dist/audio-extraction/audio-manager.d.ts +6 -5
  7. package/dist/audio-extraction/audio-manager.js +5 -3
  8. package/dist/audio-extraction/extract-audio.d.ts +3 -2
  9. package/dist/audio-extraction/extract-audio.js +11 -4
  10. package/dist/caches.d.ts +6 -5
  11. package/dist/convert-audiodata/apply-tonefrequency.d.ts +2 -0
  12. package/dist/convert-audiodata/apply-tonefrequency.js +44 -0
  13. package/dist/convert-audiodata/wsola.d.ts +13 -0
  14. package/dist/convert-audiodata/wsola.js +197 -0
  15. package/dist/esm/index.mjs +1297 -140
  16. package/dist/extract-frame-and-audio.d.ts +3 -2
  17. package/dist/extract-frame-and-audio.js +60 -26
  18. package/dist/get-sink-weak.d.ts +2 -7
  19. package/dist/index.d.ts +12 -3
  20. package/dist/index.js +11 -2
  21. package/dist/video/media-player.d.ts +70 -0
  22. package/dist/video/media-player.js +419 -0
  23. package/dist/video/props.d.ts +36 -18
  24. package/dist/video/timeout-utils.d.ts +2 -0
  25. package/dist/video/timeout-utils.js +18 -0
  26. package/dist/video/video-for-preview.d.ts +17 -0
  27. package/dist/video/video-for-preview.js +218 -0
  28. package/dist/video/video-for-rendering.d.ts +23 -2
  29. package/dist/video/video-for-rendering.js +47 -4
  30. package/dist/video/video.js +13 -14
  31. package/dist/video-extraction/extract-frame-via-broadcast-channel.d.ts +3 -2
  32. package/dist/video-extraction/extract-frame-via-broadcast-channel.js +53 -4
  33. package/dist/video-extraction/extract-frame.d.ts +2 -1
  34. package/dist/video-extraction/extract-frame.js +9 -3
  35. package/dist/video-extraction/get-frames-since-keyframe.d.ts +12 -7
  36. package/dist/video-extraction/get-frames-since-keyframe.js +70 -17
  37. package/package.json +3 -3
@@ -1,24 +1,42 @@
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
+ showInTimeline: boolean;
24
39
  };
40
+ export type InnerVideoProps = MandatoryVideoProps & OuterVideoProps & OptionalVideoProps;
41
+ export type VideoProps = MandatoryVideoProps & Partial<OuterVideoProps> & Partial<OptionalVideoProps>;
42
+ export {};
@@ -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,17 @@
1
+ import React from 'react';
2
+ import type { LogLevel, LoopVolumeCurveBehavior, OnVideoFrame, VolumeProp } from 'remotion';
3
+ type InnerVideoProps = {
4
+ readonly className: string | undefined;
5
+ readonly loop: boolean;
6
+ readonly src: string;
7
+ readonly logLevel: LogLevel;
8
+ readonly muted: boolean;
9
+ readonly name: string | undefined;
10
+ readonly volume: VolumeProp;
11
+ readonly loopVolumeCurveBehavior: LoopVolumeCurveBehavior;
12
+ readonly onVideoFrame: OnVideoFrame | undefined;
13
+ readonly playbackRate: number;
14
+ readonly style: React.CSSProperties;
15
+ };
16
+ export declare const VideoForPreview: React.FC<InnerVideoProps>;
17
+ export {};
@@ -0,0 +1,218 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ALL_FORMATS, Input, UrlSource } from 'mediabunny';
3
+ import { useContext, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Internals, Loop, useBufferState, useCurrentFrame, useVideoConfig, } from 'remotion';
5
+ 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, }) => {
19
+ const canvasRef = useRef(null);
20
+ const videoConfig = useUnsafeVideoConfig();
21
+ const frame = useCurrentFrame();
22
+ const mediaPlayerRef = useRef(null);
23
+ const [mediaPlayerReady, setMediaPlayerReady] = useState(false);
24
+ const [playing] = Timeline.usePlayingState();
25
+ const timelineContext = useContext(Timeline.TimelineContext);
26
+ const globalPlaybackRate = timelineContext.playbackRate;
27
+ const sharedAudioContext = useContext(SharedAudioContext);
28
+ const buffer = useBufferState();
29
+ const delayHandleRef = useRef(null);
30
+ const [mediaMuted] = useMediaMutedState();
31
+ const [mediaVolume] = useMediaVolumeState();
32
+ const volumePropFrame = useFrameForVolumeProp(loopVolumeCurveBehavior ?? 'repeat');
33
+ const userPreferredVolume = evaluateVolume({
34
+ frame: volumePropFrame,
35
+ volume,
36
+ mediaVolume,
37
+ });
38
+ warnAboutTooHighVolume(userPreferredVolume);
39
+ if (!videoConfig) {
40
+ throw new Error('No video config found');
41
+ }
42
+ if (!src) {
43
+ throw new TypeError('No `src` was passed to <NewVideoForPreview>.');
44
+ }
45
+ const actualFps = videoConfig.fps / playbackRate;
46
+ const currentTime = frame / actualFps;
47
+ const [initialTimestamp] = useState(currentTime);
48
+ const preloadedSrc = usePreload(src);
49
+ useEffect(() => {
50
+ if (!canvasRef.current)
51
+ return;
52
+ if (!sharedAudioContext)
53
+ return;
54
+ if (!sharedAudioContext.audioContext)
55
+ return;
56
+ try {
57
+ const player = new MediaPlayer({
58
+ canvas: canvasRef.current,
59
+ src: preloadedSrc,
60
+ logLevel,
61
+ sharedAudioContext: sharedAudioContext.audioContext,
62
+ });
63
+ mediaPlayerRef.current = player;
64
+ player
65
+ .initialize(initialTimestamp)
66
+ .then(() => {
67
+ setMediaPlayerReady(true);
68
+ Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[NewVideoForPreview] MediaPlayer initialized successfully`);
69
+ })
70
+ .catch((error) => {
71
+ Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] Failed to initialize MediaPlayer', error);
72
+ });
73
+ }
74
+ catch (error) {
75
+ Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] MediaPlayer initialization failed', error);
76
+ }
77
+ return () => {
78
+ if (delayHandleRef.current) {
79
+ delayHandleRef.current.unblock();
80
+ delayHandleRef.current = null;
81
+ }
82
+ if (mediaPlayerRef.current) {
83
+ Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[NewVideoForPreview] Disposing MediaPlayer`);
84
+ mediaPlayerRef.current.dispose();
85
+ mediaPlayerRef.current = null;
86
+ }
87
+ setMediaPlayerReady(false);
88
+ };
89
+ }, [preloadedSrc, logLevel, sharedAudioContext, initialTimestamp]);
90
+ const classNameValue = useMemo(() => {
91
+ return [Internals.OBJECTFIT_CONTAIN_CLASS_NAME, className]
92
+ .filter(Internals.truthy)
93
+ .join(' ');
94
+ }, [className]);
95
+ useEffect(() => {
96
+ const mediaPlayer = mediaPlayerRef.current;
97
+ if (!mediaPlayer)
98
+ return;
99
+ if (playing) {
100
+ mediaPlayer.play().catch((error) => {
101
+ Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] Failed to play', error);
102
+ });
103
+ }
104
+ else {
105
+ mediaPlayer.pause();
106
+ }
107
+ }, [playing, logLevel, mediaPlayerReady]);
108
+ useEffect(() => {
109
+ const mediaPlayer = mediaPlayerRef.current;
110
+ if (!mediaPlayer || !mediaPlayerReady)
111
+ return;
112
+ mediaPlayer.seekTo(currentTime);
113
+ Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[NewVideoForPreview] Updating target time to ${currentTime.toFixed(3)}s`);
114
+ }, [currentTime, logLevel, mediaPlayerReady]);
115
+ useEffect(() => {
116
+ const mediaPlayer = mediaPlayerRef.current;
117
+ if (!mediaPlayer || !mediaPlayerReady)
118
+ return;
119
+ mediaPlayer.onBufferingChange((newBufferingState) => {
120
+ if (newBufferingState && !delayHandleRef.current) {
121
+ delayHandleRef.current = buffer.delayPlayback();
122
+ Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] MediaPlayer buffering - blocking Remotion playback');
123
+ }
124
+ else if (!newBufferingState && delayHandleRef.current) {
125
+ delayHandleRef.current.unblock();
126
+ delayHandleRef.current = null;
127
+ Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] MediaPlayer unbuffering - unblocking Remotion playback');
128
+ }
129
+ });
130
+ }, [mediaPlayerReady, buffer, logLevel]);
131
+ const effectiveMuted = muted || mediaMuted || userPreferredVolume <= 0;
132
+ useEffect(() => {
133
+ const mediaPlayer = mediaPlayerRef.current;
134
+ if (!mediaPlayer || !mediaPlayerReady)
135
+ return;
136
+ mediaPlayer.setMuted(effectiveMuted);
137
+ }, [effectiveMuted, mediaPlayerReady]);
138
+ useEffect(() => {
139
+ const mediaPlayer = mediaPlayerRef.current;
140
+ if (!mediaPlayer || !mediaPlayerReady) {
141
+ return;
142
+ }
143
+ mediaPlayer.setVolume(userPreferredVolume);
144
+ }, [userPreferredVolume, mediaPlayerReady, logLevel]);
145
+ const effectivePlaybackRate = useMemo(() => playbackRate * globalPlaybackRate, [playbackRate, globalPlaybackRate]);
146
+ useEffect(() => {
147
+ const mediaPlayer = mediaPlayerRef.current;
148
+ if (!mediaPlayer || !mediaPlayerReady) {
149
+ return;
150
+ }
151
+ mediaPlayer.setPlaybackRate(effectivePlaybackRate).catch((error) => {
152
+ Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] Failed to set playback rate', error);
153
+ });
154
+ }, [effectivePlaybackRate, mediaPlayerReady, logLevel]);
155
+ useEffect(() => {
156
+ const mediaPlayer = mediaPlayerRef.current;
157
+ if (!mediaPlayer || !mediaPlayerReady) {
158
+ return;
159
+ }
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);
185
+ useEffect(() => {
186
+ if (!loop) {
187
+ return;
188
+ }
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();
210
+ return () => {
211
+ cancelled = true;
212
+ };
213
+ }, [loop, preloadedSrc, logLevel]);
214
+ if (loop && durationInSeconds === null) {
215
+ return null;
216
+ }
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 }));
218
+ };
@@ -1,3 +1,24 @@
1
1
  import React from 'react';
2
- import type { VideoProps } from './props';
3
- export declare const VideoForRendering: React.FC<VideoProps>;
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 delayRenderRetries: number | null;
17
+ readonly delayRenderTimeoutInMilliseconds: number | null;
18
+ readonly fallbackOffthreadVideoProps: FallbackOffthreadVideoProps;
19
+ readonly audioStreamIndex: number;
20
+ readonly disallowFallbackToOffthreadVideo: boolean;
21
+ readonly stack: string | undefined;
22
+ };
23
+ export declare const VideoForRendering: React.FC<InnerVideoProps>;
24
+ export {};
@@ -4,9 +4,7 @@ import { cancelRender, Internals, useCurrentFrame, useDelayRender, useRemotionEn
4
4
  import { applyVolume } from '../convert-audiodata/apply-volume';
5
5
  import { frameForVolumeProp } from '../looped-frame';
6
6
  import { extractFrameViaBroadcastChannel } from '../video-extraction/extract-frame-via-broadcast-channel';
7
- export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted, loopVolumeCurveBehavior, delayRenderRetries, delayRenderTimeoutInMilliseconds,
8
- // call when a frame of the video, i.e. frame drawn on canvas
9
- onVideoFrame, logLevel = window.remotion_logLevel, loop, style, className, }) => {
7
+ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted, loopVolumeCurveBehavior, delayRenderRetries, delayRenderTimeoutInMilliseconds, onVideoFrame, logLevel, loop, style, className, fallbackOffthreadVideoProps, audioStreamIndex, name, disallowFallbackToOffthreadVideo, stack, }) => {
10
8
  if (!src) {
11
9
  throw new TypeError('No `src` was passed to <Video>.');
12
10
  }
@@ -19,10 +17,14 @@ onVideoFrame, logLevel = window.remotion_logLevel, loop, style, className, }) =>
19
17
  const environment = useRemotionEnvironment();
20
18
  const { delayRender, continueRender } = useDelayRender();
21
19
  const canvasRef = useRef(null);
20
+ const [replaceWithOffthreadVideo, setReplaceWithOffthreadVideo] = useState(false);
22
21
  useLayoutEffect(() => {
23
22
  if (!canvasRef.current) {
24
23
  return;
25
24
  }
25
+ if (replaceWithOffthreadVideo) {
26
+ return;
27
+ }
26
28
  const actualFps = playbackRate ? fps / playbackRate : fps;
27
29
  const timestamp = frame / actualFps;
28
30
  const durationInSeconds = 1 / actualFps;
@@ -49,8 +51,40 @@ onVideoFrame, logLevel = window.remotion_logLevel, loop, style, className, }) =>
49
51
  includeVideo: window.remotion_videoEnabled,
50
52
  isClientSideRendering: environment.isClientSideRendering,
51
53
  loop: loop ?? false,
54
+ audioStreamIndex: audioStreamIndex ?? 0,
52
55
  })
53
- .then(({ frame: imageBitmap, audio, durationInSeconds: assetDurationInSeconds, }) => {
56
+ .then((result) => {
57
+ if (result === 'unknown-container-format') {
58
+ if (disallowFallbackToOffthreadVideo) {
59
+ cancelRender(new Error(`Unknown container format ${src}, and 'disallowFallbackToOffthreadVideo' was set. Failing the render.`));
60
+ }
61
+ if (window.remotion_isMainTab) {
62
+ 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
+ }
64
+ setReplaceWithOffthreadVideo(true);
65
+ return;
66
+ }
67
+ if (result === 'cannot-decode') {
68
+ if (disallowFallbackToOffthreadVideo) {
69
+ cancelRender(new Error(`Cannot decode ${src}, and 'disallowFallbackToOffthreadVideo' was set. Failing the render.`));
70
+ }
71
+ if (window.remotion_isMainTab) {
72
+ Internals.Log.info({ logLevel, tag: '@remotion/media' }, `Cannot decode ${src}, falling back to <OffthreadVideo>`);
73
+ }
74
+ setReplaceWithOffthreadVideo(true);
75
+ return;
76
+ }
77
+ if (result === 'network-error') {
78
+ if (disallowFallbackToOffthreadVideo) {
79
+ cancelRender(new Error(`Cannot decode ${src}, and 'disallowFallbackToOffthreadVideo' was set. Failing the render.`));
80
+ }
81
+ if (window.remotion_isMainTab) {
82
+ Internals.Log.info({ logLevel, tag: '@remotion/media' }, `Network error fetching ${src}, falling back to <OffthreadVideo>`);
83
+ }
84
+ setReplaceWithOffthreadVideo(true);
85
+ return;
86
+ }
87
+ const { frame: imageBitmap, audio, durationInSeconds: assetDurationInSeconds, } = result;
54
88
  if (imageBitmap) {
55
89
  onVideoFrame?.(imageBitmap);
56
90
  const context = canvasRef.current?.getContext('2d');
@@ -129,11 +163,20 @@ onVideoFrame, logLevel = window.remotion_logLevel, loop, style, className, }) =>
129
163
  startsAt,
130
164
  unregisterRenderAsset,
131
165
  volumeProp,
166
+ replaceWithOffthreadVideo,
167
+ audioStreamIndex,
168
+ disallowFallbackToOffthreadVideo,
132
169
  ]);
133
170
  const classNameValue = useMemo(() => {
134
171
  return [Internals.OBJECTFIT_CONTAIN_CLASS_NAME, className]
135
172
  .filter(Internals.truthy)
136
173
  .join(' ');
137
174
  }, [className]);
175
+ 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,
178
+ // these shouldn't matter during rendering / should not appear at all
179
+ showInTimeline: false, crossOrigin: undefined, onAutoPlayError: () => undefined, pauseWhenBuffering: false, trimAfter: undefined, trimBefore: undefined, useWebAudioApi: false, startFrom: undefined, endAt: undefined, stack: stack, _remotionInternalNativeLoopPassed: false }));
180
+ }
138
181
  return _jsx("canvas", { ref: canvasRef, style: style, className: classNameValue });
139
182
  };
@@ -1,16 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useCallback } from 'react';
3
2
  import { Internals, Sequence, useRemotionEnvironment } from 'remotion';
3
+ import { VideoForPreview } from './video-for-preview';
4
4
  import { VideoForRendering } from './video-for-rendering';
5
- const { validateMediaTrimProps, resolveTrimProps, validateMediaProps, VideoForPreview, } = Internals;
6
- export const Video = (props) => {
7
- // Should only destruct `trimBefore` and `trimAfter` from props,
8
- // rest gets drilled down
9
- const { trimBefore, trimAfter, name, pauseWhenBuffering, stack, showInTimeline, ...otherProps } = props;
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, }) => {
10
7
  const environment = useRemotionEnvironment();
11
- const onDuration = useCallback(() => undefined, []);
12
- if (typeof props.src !== 'string') {
13
- throw new TypeError(`The \`<Video>\` tag requires a string for \`src\`, but got ${JSON.stringify(props.src)} instead.`);
8
+ if (typeof src !== 'string') {
9
+ throw new TypeError(`The \`<Video>\` tag requires a string for \`src\`, but got ${JSON.stringify(src)} instead.`);
14
10
  }
15
11
  validateMediaTrimProps({
16
12
  startFrom: undefined,
@@ -26,12 +22,15 @@ export const Video = (props) => {
26
22
  });
27
23
  if (typeof trimBeforeValue !== 'undefined' ||
28
24
  typeof trimAfterValue !== 'undefined') {
29
- return (_jsx(Sequence, { layout: "none", from: 0 - (trimBeforeValue ?? 0), showInTimeline: false, durationInFrames: trimAfterValue, name: name, children: _jsx(Video, { pauseWhenBuffering: pauseWhenBuffering ?? false, ...otherProps }) }));
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 }) }));
30
26
  }
31
- validateMediaProps(props, 'Video');
27
+ validateMediaProps({ playbackRate, volume }, 'Video');
32
28
  if (environment.isRendering) {
33
- return _jsx(VideoForRendering, { ...otherProps });
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 }));
34
30
  }
35
- const { onVideoFrame, delayRenderRetries, delayRenderTimeoutInMilliseconds, ...propsForPreview } = otherProps;
36
- return (_jsx(VideoForPreview, { _remotionInternalStack: stack ?? null, _remotionInternalNativeLoopPassed: false, onDuration: onDuration, onlyWarnForMediaSeekingError: true, pauseWhenBuffering: pauseWhenBuffering ?? false, showInTimeline: showInTimeline ?? true, onVideoFrame: onVideoFrame ?? null, ...propsForPreview }));
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 }));
37
32
  };
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 }));
35
+ };
36
+ Internals.addSequenceStackTraces(Video);
@@ -1,6 +1,6 @@
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, }: {
3
+ export declare const extractFrameViaBroadcastChannel: ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, isClientSideRendering, loop, audioStreamIndex, }: {
4
4
  src: string;
5
5
  timeInSeconds: number;
6
6
  durationInSeconds: number;
@@ -10,8 +10,9 @@ export declare const extractFrameViaBroadcastChannel: ({ src, timeInSeconds, log
10
10
  includeVideo: boolean;
11
11
  isClientSideRendering: boolean;
12
12
  loop: boolean;
13
+ audioStreamIndex: number;
13
14
  }) => Promise<{
14
15
  frame: ImageBitmap | VideoFrame | null;
15
16
  audio: PcmS16AudioData | null;
16
17
  durationInSeconds: number | null;
17
- }>;
18
+ } | "cannot-decode" | "network-error" | "unknown-container-format">;
@@ -5,7 +5,7 @@ if (window.remotion_broadcastChannel && window.remotion_isMainTab) {
5
5
  const data = event.data;
6
6
  if (data.type === 'request') {
7
7
  try {
8
- const { frame, audio, durationInSeconds } = await extractFrameAndAudio({
8
+ const result = await extractFrameAndAudio({
9
9
  src: data.src,
10
10
  timeInSeconds: data.timeInSeconds,
11
11
  logLevel: data.logLevel,
@@ -14,7 +14,33 @@ if (window.remotion_broadcastChannel && window.remotion_isMainTab) {
14
14
  includeAudio: data.includeAudio,
15
15
  includeVideo: data.includeVideo,
16
16
  loop: data.loop,
17
+ audioStreamIndex: data.audioStreamIndex,
17
18
  });
19
+ if (result === 'cannot-decode') {
20
+ const cannotDecodeResponse = {
21
+ type: 'response-cannot-decode',
22
+ id: data.id,
23
+ };
24
+ window.remotion_broadcastChannel.postMessage(cannotDecodeResponse);
25
+ return;
26
+ }
27
+ if (result === 'network-error') {
28
+ const networkErrorResponse = {
29
+ type: 'response-network-error',
30
+ id: data.id,
31
+ };
32
+ window.remotion_broadcastChannel.postMessage(networkErrorResponse);
33
+ return;
34
+ }
35
+ if (result === 'unknown-container-format') {
36
+ const unknownContainerFormatResponse = {
37
+ type: 'response-unknown-container-format',
38
+ id: data.id,
39
+ };
40
+ window.remotion_broadcastChannel.postMessage(unknownContainerFormatResponse);
41
+ return;
42
+ }
43
+ const { frame, audio, durationInSeconds } = result;
18
44
  const videoFrame = frame;
19
45
  const imageBitmap = videoFrame
20
46
  ? await createImageBitmap(videoFrame)
@@ -46,7 +72,7 @@ if (window.remotion_broadcastChannel && window.remotion_isMainTab) {
46
72
  }
47
73
  });
48
74
  }
49
- export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, isClientSideRendering, loop, }) => {
75
+ export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, isClientSideRendering, loop, audioStreamIndex, }) => {
50
76
  if (isClientSideRendering || window.remotion_isMainTab) {
51
77
  return extractFrameAndAudio({
52
78
  logLevel,
@@ -57,6 +83,7 @@ export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel,
57
83
  includeAudio,
58
84
  includeVideo,
59
85
  loop,
86
+ audioStreamIndex,
60
87
  });
61
88
  }
62
89
  const requestId = crypto.randomUUID();
@@ -66,7 +93,10 @@ export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel,
66
93
  if (!data) {
67
94
  return;
68
95
  }
69
- if (data.type === 'response-success' && data.id === requestId) {
96
+ if (data.id !== requestId) {
97
+ return;
98
+ }
99
+ if (data.type === 'response-success') {
70
100
  resolve({
71
101
  frame: data.frame ? data.frame : null,
72
102
  audio: data.audio ? data.audio : null,
@@ -75,11 +105,29 @@ export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel,
75
105
  : null,
76
106
  });
77
107
  window.remotion_broadcastChannel.removeEventListener('message', onMessage);
108
+ return;
78
109
  }
79
- else if (data.type === 'response-error' && data.id === requestId) {
110
+ if (data.type === 'response-error') {
80
111
  reject(data.errorStack);
81
112
  window.remotion_broadcastChannel.removeEventListener('message', onMessage);
113
+ return;
114
+ }
115
+ if (data.type === 'response-cannot-decode') {
116
+ resolve('cannot-decode');
117
+ window.remotion_broadcastChannel.removeEventListener('message', onMessage);
118
+ return;
119
+ }
120
+ if (data.type === 'response-network-error') {
121
+ resolve('network-error');
122
+ window.remotion_broadcastChannel.removeEventListener('message', onMessage);
123
+ return;
124
+ }
125
+ if (data.type === 'response-unknown-container-format') {
126
+ resolve('unknown-container-format');
127
+ window.remotion_broadcastChannel.removeEventListener('message', onMessage);
128
+ return;
82
129
  }
130
+ throw new Error(`Invalid message: ${JSON.stringify(data)}`);
83
131
  };
84
132
  window.remotion_broadcastChannel.addEventListener('message', onMessage);
85
133
  });
@@ -94,6 +142,7 @@ export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel,
94
142
  includeAudio,
95
143
  includeVideo,
96
144
  loop,
145
+ audioStreamIndex,
97
146
  };
98
147
  window.remotion_broadcastChannel.postMessage(request);
99
148
  let timeoutId;
@@ -1,7 +1,8 @@
1
+ import type { VideoSample } from 'mediabunny';
1
2
  import { type LogLevel } from 'remotion';
2
3
  export declare const extractFrame: ({ src, timeInSeconds: unloopedTimeinSeconds, logLevel, loop, }: {
3
4
  src: string;
4
5
  timeInSeconds: number;
5
6
  logLevel: LogLevel;
6
7
  loop: boolean;
7
- }) => Promise<import("mediabunny").VideoSample | null>;
8
+ }) => Promise<VideoSample | "cannot-decode" | "unknown-container-format" | null>;
@@ -2,12 +2,18 @@ import { keyframeManager } from '../caches';
2
2
  import { getSinkWeak } from '../get-sink-weak';
3
3
  export const extractFrame = async ({ src, timeInSeconds: unloopedTimeinSeconds, logLevel, loop, }) => {
4
4
  const sink = await getSinkWeak(src, logLevel);
5
- const { video, getDuration } = sink;
6
- if (video === null) {
5
+ const video = await sink.getVideo();
6
+ if (video === 'no-video-track') {
7
7
  throw new Error(`No video track found for ${src}`);
8
8
  }
9
+ if (video === 'cannot-decode') {
10
+ return 'cannot-decode';
11
+ }
12
+ if (video === 'unknown-container-format') {
13
+ return 'unknown-container-format';
14
+ }
9
15
  const timeInSeconds = loop
10
- ? unloopedTimeinSeconds % (await getDuration())
16
+ ? unloopedTimeinSeconds % (await sink.getDuration())
11
17
  : unloopedTimeinSeconds;
12
18
  const keyframeBank = await keyframeManager.requestKeyframeBank({
13
19
  packetSink: video.packetSink,