@remotion/media 4.0.364 → 4.0.366

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,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
- import { Html5Video, Internals, useBufferState, useCurrentFrame } from 'remotion';
3
+ import { Html5Video, Internals, useBufferState, useCurrentFrame, useVideoConfig, } from 'remotion';
4
+ import { getTimeInSeconds } from '../get-time-in-seconds';
4
5
  import { MediaPlayer } from '../media-player';
5
6
  import { useLoopDisplay } from '../show-in-timeline';
6
7
  import { useMediaInTimeline } from '../use-media-in-timeline';
7
8
  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, debugOverlay, }) => {
9
+ const VideoForPreviewAssertedShowing = ({ src: unpreloadedSrc, style, playbackRate, logLevel, className, muted, volume, loopVolumeCurveBehavior, onVideoFrame, showInTimeline, loop, name, trimAfter, trimBefore, stack, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, audioStreamIndex, debugOverlay, }) => {
9
10
  const src = usePreload(unpreloadedSrc);
10
11
  const canvasRef = useRef(null);
11
12
  const videoConfig = useUnsafeVideoConfig();
@@ -30,6 +31,8 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
30
31
  });
31
32
  warnAboutTooHighVolume(userPreferredVolume);
32
33
  const parentSequence = useContext(SequenceContext);
34
+ const isPremounting = Boolean(parentSequence?.premounting);
35
+ const isPostmounting = Boolean(parentSequence?.postmounting);
33
36
  const loopDisplay = useLoopDisplay({
34
37
  loop,
35
38
  mediaDurationInSeconds,
@@ -60,6 +63,11 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
60
63
  const currentTimeRef = useRef(currentTime);
61
64
  currentTimeRef.current = currentTime;
62
65
  const preloadedSrc = usePreload(src);
66
+ const buffering = useContext(Internals.BufferingContextReact);
67
+ if (!buffering) {
68
+ throw new Error('useMediaPlayback must be used inside a <BufferingContext>');
69
+ }
70
+ const isPlayerBuffering = Internals.useIsPlayerBuffering(buffering);
63
71
  useEffect(() => {
64
72
  if (!canvasRef.current)
65
73
  return;
@@ -81,6 +89,9 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
81
89
  audioStreamIndex,
82
90
  debugOverlay,
83
91
  bufferState: buffer,
92
+ isPremounting,
93
+ isPostmounting,
94
+ globalPlaybackRate,
84
95
  });
85
96
  mediaPlayerRef.current = player;
86
97
  player
@@ -157,6 +168,9 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
157
168
  audioStreamIndex,
158
169
  debugOverlay,
159
170
  buffer,
171
+ isPremounting,
172
+ isPostmounting,
173
+ globalPlaybackRate,
160
174
  ]);
161
175
  const classNameValue = useMemo(() => {
162
176
  return [Internals.OBJECTFIT_CONTAIN_CLASS_NAME, className]
@@ -167,46 +181,22 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
167
181
  const mediaPlayer = mediaPlayerRef.current;
168
182
  if (!mediaPlayer)
169
183
  return;
170
- if (playing) {
171
- mediaPlayer.play().catch((error) => {
172
- Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] Failed to play', error);
173
- });
184
+ if (playing && !isPlayerBuffering) {
185
+ mediaPlayer.play(currentTimeRef.current);
174
186
  }
175
187
  else {
176
188
  mediaPlayer.pause();
177
189
  }
178
- }, [playing, logLevel, mediaPlayerReady]);
190
+ }, [isPlayerBuffering, playing, logLevel, mediaPlayerReady]);
179
191
  useLayoutEffect(() => {
180
192
  const mediaPlayer = mediaPlayerRef.current;
181
193
  if (!mediaPlayer || !mediaPlayerReady)
182
194
  return;
183
- mediaPlayer.seekTo(currentTime);
195
+ mediaPlayer.seekTo(currentTime).catch(() => {
196
+ // Might be disposed
197
+ });
184
198
  Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[VideoForPreview] Updating target time to ${currentTime.toFixed(3)}s`);
185
199
  }, [currentTime, logLevel, mediaPlayerReady]);
186
- useEffect(() => {
187
- const mediaPlayer = mediaPlayerRef.current;
188
- if (!mediaPlayer || !mediaPlayerReady)
189
- return;
190
- let currentBlock = null;
191
- const unsubscribe = mediaPlayer.onBufferingChange((newBufferingState) => {
192
- if (newBufferingState && !currentBlock) {
193
- currentBlock = buffer.delayPlayback();
194
- Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] MediaPlayer buffering - blocking Remotion playback');
195
- }
196
- else if (!newBufferingState && currentBlock) {
197
- currentBlock.unblock();
198
- currentBlock = null;
199
- Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] MediaPlayer unbuffering - unblocking Remotion playback');
200
- }
201
- });
202
- return () => {
203
- unsubscribe();
204
- if (currentBlock) {
205
- currentBlock.unblock();
206
- currentBlock = null;
207
- }
208
- };
209
- }, [mediaPlayerReady, buffer, logLevel]);
210
200
  const effectiveMuted = isSequenceHidden || muted || mediaMuted || userPreferredVolume <= 0;
211
201
  useEffect(() => {
212
202
  const mediaPlayer = mediaPlayerRef.current;
@@ -228,14 +218,20 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
228
218
  }
229
219
  mediaPlayer.setDebugOverlay(debugOverlay);
230
220
  }, [debugOverlay, mediaPlayerReady]);
231
- const effectivePlaybackRate = useMemo(() => playbackRate * globalPlaybackRate, [playbackRate, globalPlaybackRate]);
232
221
  useEffect(() => {
233
222
  const mediaPlayer = mediaPlayerRef.current;
234
223
  if (!mediaPlayer || !mediaPlayerReady) {
235
224
  return;
236
225
  }
237
- mediaPlayer.setPlaybackRate(effectivePlaybackRate);
238
- }, [effectivePlaybackRate, mediaPlayerReady]);
226
+ mediaPlayer.setPlaybackRate(playbackRate);
227
+ }, [playbackRate, mediaPlayerReady]);
228
+ useEffect(() => {
229
+ const mediaPlayer = mediaPlayerRef.current;
230
+ if (!mediaPlayer || !mediaPlayerReady) {
231
+ return;
232
+ }
233
+ mediaPlayer.setGlobalPlaybackRate(globalPlaybackRate);
234
+ }, [globalPlaybackRate, mediaPlayerReady]);
239
235
  useEffect(() => {
240
236
  const mediaPlayer = mediaPlayerRef.current;
241
237
  if (!mediaPlayer || !mediaPlayerReady) {
@@ -243,6 +239,20 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
243
239
  }
244
240
  mediaPlayer.setLoop(loop);
245
241
  }, [loop, mediaPlayerReady]);
242
+ useEffect(() => {
243
+ const mediaPlayer = mediaPlayerRef.current;
244
+ if (!mediaPlayer || !mediaPlayerReady) {
245
+ return;
246
+ }
247
+ mediaPlayer.setIsPremounting(isPremounting);
248
+ }, [isPremounting, mediaPlayerReady]);
249
+ useEffect(() => {
250
+ const mediaPlayer = mediaPlayerRef.current;
251
+ if (!mediaPlayer || !mediaPlayerReady) {
252
+ return;
253
+ }
254
+ mediaPlayer.setIsPostmounting(isPostmounting);
255
+ }, [isPostmounting, mediaPlayerReady]);
246
256
  useEffect(() => {
247
257
  const mediaPlayer = mediaPlayerRef.current;
248
258
  if (!mediaPlayer || !mediaPlayerReady) {
@@ -252,14 +262,25 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
252
262
  }, [videoConfig.fps, mediaPlayerReady]);
253
263
  useEffect(() => {
254
264
  const mediaPlayer = mediaPlayerRef.current;
255
- if (!mediaPlayer || !mediaPlayerReady || !onVideoFrame) {
265
+ if (!mediaPlayer || !mediaPlayerReady) {
256
266
  return;
257
267
  }
258
- const unsubscribe = mediaPlayer.onVideoFrame(onVideoFrame);
259
- return () => {
260
- unsubscribe();
261
- };
268
+ mediaPlayer.setVideoFrameCallback(onVideoFrame ?? null);
262
269
  }, [onVideoFrame, mediaPlayerReady]);
270
+ useEffect(() => {
271
+ const mediaPlayer = mediaPlayerRef.current;
272
+ if (!mediaPlayer || !mediaPlayerReady) {
273
+ return;
274
+ }
275
+ mediaPlayer.setTrimBefore(trimBefore);
276
+ }, [trimBefore, mediaPlayerReady]);
277
+ useEffect(() => {
278
+ const mediaPlayer = mediaPlayerRef.current;
279
+ if (!mediaPlayer || !mediaPlayerReady) {
280
+ return;
281
+ }
282
+ mediaPlayer.setTrimAfter(trimAfter);
283
+ }, [trimAfter, mediaPlayerReady]);
263
284
  const actualStyle = useMemo(() => {
264
285
  return {
265
286
  ...style,
@@ -273,3 +294,33 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
273
294
  }
274
295
  return (_jsx("canvas", { ref: canvasRef, width: videoConfig.width, height: videoConfig.height, style: actualStyle, className: classNameValue }));
275
296
  };
297
+ export const VideoForPreview = (props) => {
298
+ const frame = useCurrentFrame();
299
+ const videoConfig = useVideoConfig();
300
+ const currentTime = frame / videoConfig.fps;
301
+ const showShow = useMemo(() => {
302
+ return (getTimeInSeconds({
303
+ unloopedTimeInSeconds: currentTime,
304
+ playbackRate: props.playbackRate,
305
+ loop: props.loop,
306
+ trimBefore: props.trimBefore,
307
+ trimAfter: props.trimAfter,
308
+ mediaDurationInSeconds: Infinity,
309
+ fps: videoConfig.fps,
310
+ ifNoMediaDuration: 'infinity',
311
+ src: props.src,
312
+ }) !== null);
313
+ }, [
314
+ currentTime,
315
+ props.loop,
316
+ props.playbackRate,
317
+ props.src,
318
+ props.trimAfter,
319
+ props.trimBefore,
320
+ videoConfig.fps,
321
+ ]);
322
+ if (!showShow) {
323
+ return null;
324
+ }
325
+ return _jsx(VideoForPreviewAssertedShowing, { ...props });
326
+ };
@@ -0,0 +1,37 @@
1
+ import type { InputVideoTrack, WrappedCanvas } from 'mediabunny';
2
+ import type { LogLevel } from 'remotion';
3
+ import type { Nonce } from './nonce-manager';
4
+ export declare const videoIteratorManager: ({ delayPlaybackHandleIfNotPremounting, canvas, context, drawDebugOverlay, logLevel, getOnVideoFrameCallback, videoTrack, }: {
5
+ videoTrack: InputVideoTrack;
6
+ delayPlaybackHandleIfNotPremounting: () => {
7
+ unblock: () => void;
8
+ };
9
+ context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;
10
+ canvas: OffscreenCanvas | HTMLCanvasElement;
11
+ getOnVideoFrameCallback: () => null | ((frame: CanvasImageSource) => void);
12
+ logLevel: LogLevel;
13
+ drawDebugOverlay: () => void;
14
+ }) => {
15
+ startVideoIterator: (timeToSeek: number, nonce: Nonce) => Promise<void>;
16
+ getVideoIteratorsCreated: () => number;
17
+ seek: ({ newTime, nonce }: {
18
+ newTime: number;
19
+ nonce: Nonce;
20
+ }) => Promise<void>;
21
+ destroy: () => void;
22
+ getVideoFrameIterator: () => {
23
+ destroy: () => void;
24
+ getNext: () => Promise<IteratorResult<WrappedCanvas, void>>;
25
+ isDestroyed: () => boolean;
26
+ tryToSatisfySeek: (time: number) => Promise<{
27
+ type: "not-satisfied";
28
+ reason: string;
29
+ } | {
30
+ type: "satisfied";
31
+ frame: WrappedCanvas;
32
+ }>;
33
+ } | null;
34
+ drawFrame: (frame: WrappedCanvas) => void;
35
+ getFramesRendered: () => number;
36
+ };
37
+ export type VideoIteratorManager = ReturnType<typeof videoIteratorManager>;
@@ -0,0 +1,83 @@
1
+ import { CanvasSink } from 'mediabunny';
2
+ import { Internals } from 'remotion';
3
+ import { createVideoIterator, } from './video/video-preview-iterator';
4
+ export const videoIteratorManager = ({ delayPlaybackHandleIfNotPremounting, canvas, context, drawDebugOverlay, logLevel, getOnVideoFrameCallback, videoTrack, }) => {
5
+ let videoIteratorsCreated = 0;
6
+ let videoFrameIterator = null;
7
+ let framesRendered = 0;
8
+ canvas.width = videoTrack.displayWidth;
9
+ canvas.height = videoTrack.displayHeight;
10
+ const canvasSink = new CanvasSink(videoTrack, {
11
+ poolSize: 2,
12
+ fit: 'contain',
13
+ alpha: true,
14
+ });
15
+ const drawFrame = (frame) => {
16
+ context.clearRect(0, 0, canvas.width, canvas.height);
17
+ context.drawImage(frame.canvas, 0, 0);
18
+ framesRendered++;
19
+ drawDebugOverlay();
20
+ const callback = getOnVideoFrameCallback();
21
+ if (callback) {
22
+ callback(canvas);
23
+ }
24
+ Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[MediaPlayer] Drew frame ${frame.timestamp.toFixed(3)}s`);
25
+ };
26
+ const startVideoIterator = async (timeToSeek, nonce) => {
27
+ videoFrameIterator?.destroy();
28
+ const iterator = createVideoIterator(timeToSeek, canvasSink);
29
+ videoIteratorsCreated++;
30
+ videoFrameIterator = iterator;
31
+ const delayHandle = delayPlaybackHandleIfNotPremounting();
32
+ const frameResult = await iterator.getNext();
33
+ delayHandle.unblock();
34
+ if (iterator.isDestroyed()) {
35
+ return;
36
+ }
37
+ if (nonce.isStale()) {
38
+ return;
39
+ }
40
+ if (videoFrameIterator.isDestroyed()) {
41
+ return;
42
+ }
43
+ if (!frameResult.value) {
44
+ // media ended
45
+ return;
46
+ }
47
+ drawFrame(frameResult.value);
48
+ };
49
+ const seek = async ({ newTime, nonce }) => {
50
+ if (!videoFrameIterator) {
51
+ return;
52
+ }
53
+ // Should return immediately, so it's okay to not use Promise.all here
54
+ const videoSatisfyResult = await videoFrameIterator.tryToSatisfySeek(newTime);
55
+ // Doing this before the staleness check, because
56
+ // frame might be better than what we currently have
57
+ // TODO: check if this is actually true
58
+ if (videoSatisfyResult.type === 'satisfied') {
59
+ drawFrame(videoSatisfyResult.frame);
60
+ return;
61
+ }
62
+ if (nonce.isStale()) {
63
+ return;
64
+ }
65
+ // Intentionally not awaited, letting audio start as well
66
+ startVideoIterator(newTime, nonce).catch(() => {
67
+ // Ignore errors, might be stale or disposed
68
+ });
69
+ };
70
+ return {
71
+ startVideoIterator,
72
+ getVideoIteratorsCreated: () => videoIteratorsCreated,
73
+ seek,
74
+ destroy: () => {
75
+ videoFrameIterator?.destroy();
76
+ context.clearRect(0, 0, canvas.width, canvas.height);
77
+ videoFrameIterator = null;
78
+ },
79
+ getVideoFrameIterator: () => videoFrameIterator,
80
+ drawFrame,
81
+ getFramesRendered: () => framesRendered,
82
+ };
83
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotion/media",
3
- "version": "4.0.364",
3
+ "version": "4.0.366",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/esm/index.mjs",
@@ -21,8 +21,8 @@
21
21
  "make": "tsc -d && bun --env-file=../.env.bundle bundle.ts"
22
22
  },
23
23
  "dependencies": {
24
- "mediabunny": "1.24.1",
25
- "remotion": "4.0.364",
24
+ "mediabunny": "1.24.2",
25
+ "remotion": "4.0.366",
26
26
  "webdriverio": "9.19.2"
27
27
  },
28
28
  "peerDependencies": {
@@ -30,7 +30,7 @@
30
30
  "react-dom": ">=16.8.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@remotion/eslint-config-internal": "4.0.364",
33
+ "@remotion/eslint-config-internal": "4.0.366",
34
34
  "@vitest/browser": "^3.2.4",
35
35
  "eslint": "9.19.0",
36
36
  "react": "19.0.0",