@remotion/media 4.0.363 → 4.0.365

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useContext, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { Html5Video, Internals, useBufferState, useCurrentFrame } from 'remotion';
4
+ import { MediaPlayer } from '../media-player';
4
5
  import { useLoopDisplay } from '../show-in-timeline';
5
6
  import { useMediaInTimeline } from '../use-media-in-timeline';
6
- import { MediaPlayer } from './media-player';
7
7
  const { useUnsafeVideoConfig, Timeline, SharedAudioContext, useMediaMutedState, useMediaVolumeState, useFrameForVolumeProp, evaluateVolume, warnAboutTooHighVolume, usePreload, SequenceContext, SequenceVisibilityToggleContext, } = Internals;
8
- export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logLevel, className, muted, volume, loopVolumeCurveBehavior, onVideoFrame, showInTimeline, loop, name, trimAfter, trimBefore, stack, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, audioStreamIndex, }) => {
8
+ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logLevel, className, muted, volume, loopVolumeCurveBehavior, onVideoFrame, showInTimeline, loop, name, trimAfter, trimBefore, stack, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, audioStreamIndex, debugOverlay, }) => {
9
9
  const src = usePreload(unpreloadedSrc);
10
10
  const canvasRef = useRef(null);
11
11
  const videoConfig = useUnsafeVideoConfig();
@@ -56,13 +56,15 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
56
56
  if (!videoConfig) {
57
57
  throw new Error('No video config found');
58
58
  }
59
- if (!src) {
60
- throw new TypeError('No `src` was passed to <NewVideoForPreview>.');
61
- }
62
59
  const currentTime = frame / videoConfig.fps;
63
60
  const currentTimeRef = useRef(currentTime);
64
61
  currentTimeRef.current = currentTime;
65
62
  const preloadedSrc = usePreload(src);
63
+ const buffering = useContext(Internals.BufferingContextReact);
64
+ if (!buffering) {
65
+ throw new Error('useMediaPlayback must be used inside a <BufferingContext>');
66
+ }
67
+ const isPlayerBuffering = Internals.useIsPlayerBuffering(buffering);
66
68
  useEffect(() => {
67
69
  if (!canvasRef.current)
68
70
  return;
@@ -82,11 +84,16 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
82
84
  fps: videoConfig.fps,
83
85
  playbackRate,
84
86
  audioStreamIndex,
87
+ debugOverlay,
88
+ bufferState: buffer,
85
89
  });
86
90
  mediaPlayerRef.current = player;
87
91
  player
88
92
  .initialize(currentTimeRef.current)
89
93
  .then((result) => {
94
+ if (result.type === 'disposed') {
95
+ return;
96
+ }
90
97
  if (result.type === 'unknown-container-format') {
91
98
  if (disallowFallbackToOffthreadVideo) {
92
99
  throw new Error(`Unknown container format ${preloadedSrc}, and 'disallowFallbackToOffthreadVideo' was set.`);
@@ -125,17 +132,17 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
125
132
  }
126
133
  })
127
134
  .catch((error) => {
128
- Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] Failed to initialize MediaPlayer', error);
135
+ Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] Failed to initialize MediaPlayer', error);
129
136
  setShouldFallbackToNativeVideo(true);
130
137
  });
131
138
  }
132
139
  catch (error) {
133
- Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] MediaPlayer initialization failed', error);
140
+ Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] MediaPlayer initialization failed', error);
134
141
  setShouldFallbackToNativeVideo(true);
135
142
  }
136
143
  return () => {
137
144
  if (mediaPlayerRef.current) {
138
- Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[NewVideoForPreview] Disposing MediaPlayer`);
145
+ Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[VideoForPreview] Disposing MediaPlayer`);
139
146
  mediaPlayerRef.current.dispose();
140
147
  mediaPlayerRef.current = null;
141
148
  }
@@ -153,6 +160,8 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
153
160
  playbackRate,
154
161
  disallowFallbackToOffthreadVideo,
155
162
  audioStreamIndex,
163
+ debugOverlay,
164
+ buffer,
156
165
  ]);
157
166
  const classNameValue = useMemo(() => {
158
167
  return [Internals.OBJECTFIT_CONTAIN_CLASS_NAME, className]
@@ -163,46 +172,20 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
163
172
  const mediaPlayer = mediaPlayerRef.current;
164
173
  if (!mediaPlayer)
165
174
  return;
166
- if (playing) {
167
- mediaPlayer.play().catch((error) => {
168
- Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] Failed to play', error);
169
- });
175
+ if (playing && !isPlayerBuffering) {
176
+ mediaPlayer.play(currentTimeRef.current);
170
177
  }
171
178
  else {
172
179
  mediaPlayer.pause();
173
180
  }
174
- }, [playing, logLevel, mediaPlayerReady]);
175
- useEffect(() => {
181
+ }, [isPlayerBuffering, playing, logLevel, mediaPlayerReady]);
182
+ useLayoutEffect(() => {
176
183
  const mediaPlayer = mediaPlayerRef.current;
177
184
  if (!mediaPlayer || !mediaPlayerReady)
178
185
  return;
179
186
  mediaPlayer.seekTo(currentTime);
180
- Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[NewVideoForPreview] Updating target time to ${currentTime.toFixed(3)}s`);
187
+ Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[VideoForPreview] Updating target time to ${currentTime.toFixed(3)}s`);
181
188
  }, [currentTime, logLevel, mediaPlayerReady]);
182
- useEffect(() => {
183
- const mediaPlayer = mediaPlayerRef.current;
184
- if (!mediaPlayer || !mediaPlayerReady)
185
- return;
186
- let currentBlock = null;
187
- const unsubscribe = mediaPlayer.onBufferingChange((newBufferingState) => {
188
- if (newBufferingState && !currentBlock) {
189
- currentBlock = buffer.delayPlayback();
190
- Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] MediaPlayer buffering - blocking Remotion playback');
191
- }
192
- else if (!newBufferingState && currentBlock) {
193
- currentBlock.unblock();
194
- currentBlock = null;
195
- Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[NewVideoForPreview] MediaPlayer unbuffering - unblocking Remotion playback');
196
- }
197
- });
198
- return () => {
199
- unsubscribe();
200
- if (currentBlock) {
201
- currentBlock.unblock();
202
- currentBlock = null;
203
- }
204
- };
205
- }, [mediaPlayerReady, buffer, logLevel]);
206
189
  const effectiveMuted = isSequenceHidden || muted || mediaMuted || userPreferredVolume <= 0;
207
190
  useEffect(() => {
208
191
  const mediaPlayer = mediaPlayerRef.current;
@@ -217,6 +200,13 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
217
200
  }
218
201
  mediaPlayer.setVolume(userPreferredVolume);
219
202
  }, [userPreferredVolume, mediaPlayerReady]);
203
+ useEffect(() => {
204
+ const mediaPlayer = mediaPlayerRef.current;
205
+ if (!mediaPlayer || !mediaPlayerReady) {
206
+ return;
207
+ }
208
+ mediaPlayer.setDebugOverlay(debugOverlay);
209
+ }, [debugOverlay, mediaPlayerReady]);
220
210
  const effectivePlaybackRate = useMemo(() => playbackRate * globalPlaybackRate, [playbackRate, globalPlaybackRate]);
221
211
  useEffect(() => {
222
212
  const mediaPlayer = mediaPlayerRef.current;
@@ -0,0 +1,14 @@
1
+ import type { CanvasSink, WrappedCanvas } from 'mediabunny';
2
+ export declare const createVideoIterator: (timeToSeek: number, videoSink: CanvasSink) => {
3
+ destroy: () => void;
4
+ getNext: () => Promise<IteratorResult<WrappedCanvas, void>>;
5
+ isDestroyed: () => boolean;
6
+ tryToSatisfySeek: (time: number) => Promise<{
7
+ type: "not-satisfied";
8
+ reason: string;
9
+ } | {
10
+ type: "satisfied";
11
+ frame: WrappedCanvas;
12
+ }>;
13
+ };
14
+ export type VideoIterator = ReturnType<typeof createVideoIterator>;
@@ -0,0 +1,122 @@
1
+ import { roundTo4Digits } from '../helpers/round-to-4-digits';
2
+ export const createVideoIterator = (timeToSeek, videoSink) => {
3
+ let destroyed = false;
4
+ const iterator = videoSink.canvases(timeToSeek);
5
+ let lastReturnedFrame = null;
6
+ let iteratorEnded = false;
7
+ const getNextOrNullIfNotAvailable = async () => {
8
+ const next = iterator.next();
9
+ const result = await Promise.race([
10
+ next,
11
+ new Promise((resolve) => {
12
+ Promise.resolve().then(() => resolve());
13
+ }),
14
+ ]);
15
+ if (!result) {
16
+ return {
17
+ type: 'need-to-wait-for-it',
18
+ waitPromise: async () => {
19
+ const res = await next;
20
+ if (res.value) {
21
+ lastReturnedFrame = res.value;
22
+ }
23
+ else {
24
+ iteratorEnded = true;
25
+ }
26
+ return res.value;
27
+ },
28
+ };
29
+ }
30
+ if (result.value) {
31
+ lastReturnedFrame = result.value;
32
+ }
33
+ else {
34
+ iteratorEnded = true;
35
+ }
36
+ return {
37
+ type: 'got-frame-or-end',
38
+ frame: result.value ?? null,
39
+ };
40
+ };
41
+ const destroy = () => {
42
+ destroyed = true;
43
+ lastReturnedFrame = null;
44
+ iterator.return().catch(() => undefined);
45
+ };
46
+ const tryToSatisfySeek = async (time) => {
47
+ if (lastReturnedFrame) {
48
+ const frameTimestamp = roundTo4Digits(lastReturnedFrame.timestamp);
49
+ if (roundTo4Digits(time) < frameTimestamp) {
50
+ return {
51
+ type: 'not-satisfied',
52
+ reason: `iterator is too far, most recently returned ${frameTimestamp}`,
53
+ };
54
+ }
55
+ const frameEndTimestamp = roundTo4Digits(lastReturnedFrame.timestamp + lastReturnedFrame.duration);
56
+ const timestamp = roundTo4Digits(time);
57
+ if (frameTimestamp <= timestamp && frameEndTimestamp > timestamp) {
58
+ return {
59
+ type: 'satisfied',
60
+ frame: lastReturnedFrame,
61
+ };
62
+ }
63
+ }
64
+ if (iteratorEnded) {
65
+ if (lastReturnedFrame) {
66
+ return {
67
+ type: 'satisfied',
68
+ frame: lastReturnedFrame,
69
+ };
70
+ }
71
+ return {
72
+ type: 'not-satisfied',
73
+ reason: 'iterator ended',
74
+ };
75
+ }
76
+ while (true) {
77
+ const frame = await getNextOrNullIfNotAvailable();
78
+ if (frame.type === 'need-to-wait-for-it') {
79
+ return {
80
+ type: 'not-satisfied',
81
+ reason: 'iterator did not have frame ready',
82
+ };
83
+ }
84
+ if (frame.type === 'got-frame-or-end') {
85
+ if (frame.frame === null) {
86
+ iteratorEnded = true;
87
+ if (lastReturnedFrame) {
88
+ return {
89
+ type: 'satisfied',
90
+ frame: lastReturnedFrame,
91
+ };
92
+ }
93
+ return {
94
+ type: 'not-satisfied',
95
+ reason: 'iterator ended and did not have frame ready',
96
+ };
97
+ }
98
+ const frameTimestamp = roundTo4Digits(frame.frame.timestamp);
99
+ const frameEndTimestamp = roundTo4Digits(frame.frame.timestamp + frame.frame.duration);
100
+ const timestamp = roundTo4Digits(time);
101
+ if (frameTimestamp <= timestamp && frameEndTimestamp > timestamp) {
102
+ return {
103
+ type: 'satisfied',
104
+ frame: frame.frame,
105
+ };
106
+ }
107
+ continue;
108
+ }
109
+ throw new Error('Unreachable');
110
+ }
111
+ };
112
+ return {
113
+ destroy,
114
+ getNext: () => {
115
+ return iterator.next();
116
+ },
117
+ isDestroyed: () => {
118
+ return destroyed;
119
+ },
120
+ tryToSatisfySeek,
121
+ };
122
+ };
@@ -3,7 +3,7 @@ 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, stack, toneFrequency, showInTimeline, }) => {
6
+ const InnerVideo = ({ src, audioStreamIndex, className, delayRenderRetries, delayRenderTimeoutInMilliseconds, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, logLevel, loop, loopVolumeCurveBehavior, muted, name, onVideoFrame, playbackRate, style, trimAfter, trimBefore, volume, stack, toneFrequency, showInTimeline, debugOverlay, }) => {
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.`);
@@ -24,10 +24,10 @@ const InnerVideo = ({ src, audioStreamIndex, className, delayRenderRetries, dela
24
24
  if (environment.isRendering) {
25
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 }));
26
26
  }
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 }));
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, debugOverlay: debugOverlay ?? false }));
28
28
  };
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, }) => {
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, debugOverlay, }) => {
30
30
  return (_jsx(InnerVideo, { audioStreamIndex: audioStreamIndex ?? 0, className: className, delayRenderRetries: delayRenderRetries ?? null, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {}, logLevel: logLevel ??
31
- (typeof window !== 'undefined' ? window.remotion_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, toneFrequency: toneFrequency ?? 1, stack: stack }));
31
+ (typeof window !== 'undefined' ? window.remotion_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, toneFrequency: toneFrequency ?? 1, stack: stack, debugOverlay: debugOverlay ?? false }));
32
32
  };
33
33
  Internals.addSequenceStackTraces(Video);
@@ -1,10 +1,7 @@
1
1
  import { Internals } from 'remotion';
2
2
  import { SAFE_BACK_WINDOW_IN_SECONDS } from '../caches';
3
+ import { roundTo4Digits } from '../helpers/round-to-4-digits';
3
4
  import { renderTimestampRange } from '../render-timestamp-range';
4
- // Round to only 4 digits, because WebM has a timescale of 1_000, e.g. framer.webm
5
- const roundTo4Digits = (timestamp) => {
6
- return Math.round(timestamp * 1000) / 1000;
7
- };
8
5
  export const makeKeyframeBank = ({ startTimestampInSeconds, endTimestampInSeconds, sampleIterator, logLevel: parentLogLevel, src, }) => {
9
6
  Internals.Log.verbose({ logLevel: parentLogLevel, tag: '@remotion/media' }, `Creating keyframe bank from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
10
7
  const frames = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotion/media",
3
- "version": "4.0.363",
3
+ "version": "4.0.365",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/esm/index.mjs",
@@ -21,8 +21,8 @@
21
21
  "make": "tsc -d && bun --env-file=../.env.bundle bundle.ts"
22
22
  },
23
23
  "dependencies": {
24
- "mediabunny": "1.23.0",
25
- "remotion": "4.0.363",
24
+ "mediabunny": "1.24.2",
25
+ "remotion": "4.0.365",
26
26
  "webdriverio": "9.19.2"
27
27
  },
28
28
  "peerDependencies": {
@@ -30,7 +30,7 @@
30
30
  "react-dom": ">=16.8.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@remotion/eslint-config-internal": "4.0.363",
33
+ "@remotion/eslint-config-internal": "4.0.365",
34
34
  "@vitest/browser": "^3.2.4",
35
35
  "eslint": "9.19.0",
36
36
  "react": "19.0.0",