@remotion/media 4.0.445 → 4.0.447

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.
@@ -9,6 +9,14 @@ export type FallbackHtml5AudioProps = {
9
9
  };
10
10
  export type AudioProps = {
11
11
  src: string;
12
+ /**
13
+ * When set, `<Audio>` applies timing via an inner `<Sequence layout="none">` that is hidden from the timeline (`showInTimeline={false}`) so the clip still appears once as media.
14
+ */
15
+ from?: number;
16
+ /**
17
+ * When set with `from`, bounds the clip in frames. Defaults to `Infinity` like `<Sequence>`.
18
+ */
19
+ durationInFrames?: number;
12
20
  trimBefore?: number;
13
21
  trimAfter?: number;
14
22
  volume?: VolumeProp;
@@ -0,0 +1,14 @@
1
+ export type ObjectFitValue = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
2
+ /**
3
+ * Draws a source image onto a canvas context with the specified object-fit behavior.
4
+ * This implements object-fit at the canvas drawing level, which is more reliable
5
+ * than CSS object-fit on canvas elements.
6
+ */
7
+ export declare const drawWithObjectFit: (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, source: CanvasImageSource, options: {
8
+ sourceWidth: number;
9
+ sourceHeight: number;
10
+ destWidth: number;
11
+ destHeight: number;
12
+ fit: ObjectFitValue;
13
+ }) => void;
14
+ export declare const parseObjectFit: (value: string | undefined) => ObjectFitValue | null;
@@ -37,7 +37,7 @@ var __callDispose = (stack, error, hasError) => {
37
37
  };
38
38
 
39
39
  // src/audio/audio.tsx
40
- import { Internals as Internals19, useRemotionEnvironment as useRemotionEnvironment2 } from "remotion";
40
+ import { Internals as Internals19, Sequence, useRemotionEnvironment as useRemotionEnvironment2 } from "remotion";
41
41
 
42
42
  // src/audio/audio-for-preview.tsx
43
43
  import { useContext as useContext3, useEffect as useEffect2, useMemo as useMemo2, useRef, useState as useState2 } from "react";
@@ -1093,8 +1093,10 @@ var videoIteratorManager = ({
1093
1093
  let framesRendered = 0;
1094
1094
  let currentDelayHandle = null;
1095
1095
  if (canvas) {
1096
- canvas.width = videoTrack.displayWidth;
1097
- canvas.height = videoTrack.displayHeight;
1096
+ if (canvas.width !== videoTrack.displayWidth || canvas.height !== videoTrack.displayHeight) {
1097
+ canvas.width = videoTrack.displayWidth;
1098
+ canvas.height = videoTrack.displayHeight;
1099
+ }
1098
1100
  }
1099
1101
  const canvasSink = new CanvasSink(videoTrack, {
1100
1102
  poolSize: 2,
@@ -4469,29 +4471,40 @@ var audioSchema = {
4469
4471
  loop: { type: "boolean", default: false, description: "Loop" }
4470
4472
  };
4471
4473
  var AudioInner = (props) => {
4472
- const { name, stack, showInTimeline, controls, ...otherProps } = props;
4474
+ const {
4475
+ name,
4476
+ stack,
4477
+ showInTimeline,
4478
+ controls,
4479
+ from,
4480
+ durationInFrames,
4481
+ ...otherProps
4482
+ } = props;
4473
4483
  const environment = useRemotionEnvironment2();
4474
4484
  if (typeof props.src !== "string") {
4475
4485
  throw new TypeError(`The \`<Audio>\` tag requires a string for \`src\`, but got ${JSON.stringify(props.src)} instead.`);
4476
4486
  }
4477
4487
  validateMediaProps({ playbackRate: props.playbackRate, volume: props.volume }, "Audio");
4478
- if (environment.isRendering) {
4479
- return /* @__PURE__ */ jsx3(AudioForRendering, {
4488
+ return /* @__PURE__ */ jsx3(Sequence, {
4489
+ layout: "none",
4490
+ from: from ?? 0,
4491
+ durationInFrames: durationInFrames ?? Infinity,
4492
+ showInTimeline: false,
4493
+ children: environment.isRendering ? /* @__PURE__ */ jsx3(AudioForRendering, {
4480
4494
  ...otherProps
4481
- });
4482
- }
4483
- return /* @__PURE__ */ jsx3(AudioForPreview, {
4484
- name,
4485
- ...otherProps,
4486
- stack: stack ?? null,
4487
- controls
4495
+ }) : /* @__PURE__ */ jsx3(AudioForPreview, {
4496
+ name,
4497
+ ...otherProps,
4498
+ stack: stack ?? null,
4499
+ controls
4500
+ })
4488
4501
  });
4489
4502
  };
4490
4503
  var Audio = Internals19.wrapInSchema(AudioInner, audioSchema);
4491
4504
  Internals19.addSequenceStackTraces(Audio);
4492
4505
 
4493
4506
  // src/video/video.tsx
4494
- import { Internals as Internals23, useRemotionEnvironment as useRemotionEnvironment4 } from "remotion";
4507
+ import { Internals as Internals23, Sequence as Sequence2, useRemotionEnvironment as useRemotionEnvironment4 } from "remotion";
4495
4508
 
4496
4509
  // src/video/video-for-preview.tsx
4497
4510
  import {
@@ -4510,6 +4523,29 @@ import {
4510
4523
  useVideoConfig as useVideoConfig3
4511
4524
  } from "remotion";
4512
4525
 
4526
+ // src/video/video-frame-cache.ts
4527
+ var cache = new Map;
4528
+ var cacheVideoFrame = (src, sourceCanvas) => {
4529
+ const { width, height } = sourceCanvas;
4530
+ if (width === 0 || height === 0) {
4531
+ return;
4532
+ }
4533
+ let cached = cache.get(src);
4534
+ if (!cached || cached.width !== width || cached.height !== height) {
4535
+ cached = new OffscreenCanvas(width, height);
4536
+ cache.set(src, cached);
4537
+ }
4538
+ const ctx = cached.getContext("2d");
4539
+ if (!ctx) {
4540
+ return;
4541
+ }
4542
+ ctx.clearRect(0, 0, width, height);
4543
+ ctx.drawImage(sourceCanvas, 0, 0);
4544
+ };
4545
+ var getCachedVideoFrame = (src) => {
4546
+ return cache.get(src) ?? null;
4547
+ };
4548
+
4513
4549
  // src/video/warn-object-fit-css.ts
4514
4550
  import { Internals as Internals20 } from "remotion";
4515
4551
  var OBJECT_FIT_CLASS_PATTERN = /\bobject-(contain|cover|fill|none|scale-down)\b/;
@@ -4570,7 +4606,8 @@ var VideoForPreviewAssertedShowing = ({
4570
4606
  onError,
4571
4607
  credentials,
4572
4608
  controls,
4573
- objectFit: objectFitProp
4609
+ objectFit: objectFitProp,
4610
+ _experimentalInitiallyDrawCachedFrame
4574
4611
  }) => {
4575
4612
  const src = usePreload2(unpreloadedSrc);
4576
4613
  const canvasRef = useRef2(null);
@@ -4647,6 +4684,44 @@ var VideoForPreviewAssertedShowing = ({
4647
4684
  const initialMuted = useRef2(effectiveMuted);
4648
4685
  const initialDurationInFrames = useRef2(videoConfig.durationInFrames);
4649
4686
  const initialSequenceOffset = useRef2(sequenceOffset);
4687
+ const hasDrawnRealFrameRef = useRef2(false);
4688
+ const isPremountingRef = useRef2(isPremounting);
4689
+ isPremountingRef.current = isPremounting;
4690
+ useLayoutEffect3(() => {
4691
+ if (!_experimentalInitiallyDrawCachedFrame) {
4692
+ return;
4693
+ }
4694
+ const canvas = canvasRef.current;
4695
+ if (!canvas) {
4696
+ return;
4697
+ }
4698
+ const cached = getCachedVideoFrame(src);
4699
+ if (!cached) {
4700
+ return;
4701
+ }
4702
+ canvas.width = cached.width;
4703
+ canvas.height = cached.height;
4704
+ const ctx = canvas.getContext("2d", {
4705
+ alpha: true,
4706
+ desynchronized: true
4707
+ });
4708
+ if (!ctx) {
4709
+ return;
4710
+ }
4711
+ ctx.drawImage(cached, 0, 0);
4712
+ }, [_experimentalInitiallyDrawCachedFrame, src]);
4713
+ useLayoutEffect3(() => {
4714
+ if (!_experimentalInitiallyDrawCachedFrame) {
4715
+ return;
4716
+ }
4717
+ const canvas = canvasRef.current;
4718
+ return () => {
4719
+ if (!canvas || !hasDrawnRealFrameRef.current || isPremountingRef.current) {
4720
+ return;
4721
+ }
4722
+ cacheVideoFrame(src, canvas);
4723
+ };
4724
+ }, [_experimentalInitiallyDrawCachedFrame, src]);
4650
4725
  useEffect3(() => {
4651
4726
  if (!sharedAudioContext)
4652
4727
  return;
@@ -4715,6 +4790,7 @@ var VideoForPreviewAssertedShowing = ({
4715
4790
  if (result.type === "success") {
4716
4791
  setMediaPlayerReady(true);
4717
4792
  setMediaDurationInSeconds(result.durationInSeconds);
4793
+ hasDrawnRealFrameRef.current = true;
4718
4794
  }
4719
4795
  }).catch((error) => {
4720
4796
  const [action, errorToUse] = callOnErrorAndResolve({
@@ -4752,6 +4828,7 @@ var VideoForPreviewAssertedShowing = ({
4752
4828
  }
4753
4829
  setMediaPlayerReady(false);
4754
4830
  setShouldFallbackToNativeVideo(false);
4831
+ hasDrawnRealFrameRef.current = false;
4755
4832
  };
4756
4833
  }, [
4757
4834
  audioStreamIndex,
@@ -5272,7 +5349,8 @@ var InnerVideo = ({
5272
5349
  onError,
5273
5350
  credentials,
5274
5351
  controls,
5275
- objectFit
5352
+ objectFit,
5353
+ _experimentalInitiallyDrawCachedFrame
5276
5354
  }) => {
5277
5355
  const environment = useRemotionEnvironment4();
5278
5356
  if (typeof src !== "string") {
@@ -5344,7 +5422,8 @@ var InnerVideo = ({
5344
5422
  onError,
5345
5423
  credentials,
5346
5424
  controls,
5347
- objectFit
5425
+ objectFit,
5426
+ _experimentalInitiallyDrawCachedFrame
5348
5427
  });
5349
5428
  };
5350
5429
  var VideoInner = ({
@@ -5375,38 +5454,48 @@ var VideoInner = ({
5375
5454
  onError,
5376
5455
  credentials,
5377
5456
  controls,
5378
- objectFit
5457
+ objectFit,
5458
+ _experimentalInitiallyDrawCachedFrame,
5459
+ from,
5460
+ durationInFrames
5379
5461
  }) => {
5380
5462
  const fallbackLogLevel = Internals23.useLogLevel();
5381
- return /* @__PURE__ */ jsx6(InnerVideo, {
5382
- audioStreamIndex: audioStreamIndex ?? 0,
5383
- className,
5384
- delayRenderRetries: delayRenderRetries ?? null,
5385
- delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null,
5386
- disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false,
5387
- fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {},
5388
- logLevel: logLevel ?? fallbackLogLevel,
5389
- loop: loop ?? false,
5390
- loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? "repeat",
5391
- muted: muted ?? false,
5392
- name,
5393
- onVideoFrame,
5394
- playbackRate: playbackRate ?? 1,
5395
- showInTimeline: showInTimeline ?? true,
5396
- src,
5397
- style: style ?? {},
5398
- trimAfter,
5399
- trimBefore,
5400
- volume: volume ?? 1,
5401
- toneFrequency: toneFrequency ?? 1,
5402
- stack,
5403
- debugOverlay: debugOverlay ?? false,
5404
- debugAudioScheduling: debugAudioScheduling ?? false,
5405
- headless: headless ?? false,
5406
- onError,
5407
- credentials,
5408
- controls,
5409
- objectFit: objectFit ?? "contain"
5463
+ return /* @__PURE__ */ jsx6(Sequence2, {
5464
+ layout: "none",
5465
+ from: from ?? 0,
5466
+ durationInFrames: durationInFrames ?? Infinity,
5467
+ showInTimeline: false,
5468
+ children: /* @__PURE__ */ jsx6(InnerVideo, {
5469
+ audioStreamIndex: audioStreamIndex ?? 0,
5470
+ className,
5471
+ delayRenderRetries: delayRenderRetries ?? null,
5472
+ delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null,
5473
+ disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false,
5474
+ fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {},
5475
+ logLevel: logLevel ?? fallbackLogLevel,
5476
+ loop: loop ?? false,
5477
+ loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? "repeat",
5478
+ muted: muted ?? false,
5479
+ name,
5480
+ onVideoFrame,
5481
+ playbackRate: playbackRate ?? 1,
5482
+ showInTimeline: showInTimeline ?? true,
5483
+ src,
5484
+ style: style ?? {},
5485
+ trimAfter,
5486
+ trimBefore,
5487
+ volume: volume ?? 1,
5488
+ toneFrequency: toneFrequency ?? 1,
5489
+ stack,
5490
+ debugOverlay: debugOverlay ?? false,
5491
+ debugAudioScheduling: debugAudioScheduling ?? false,
5492
+ headless: headless ?? false,
5493
+ onError,
5494
+ credentials,
5495
+ controls,
5496
+ objectFit: objectFit ?? "contain",
5497
+ _experimentalInitiallyDrawCachedFrame: _experimentalInitiallyDrawCachedFrame ?? false
5498
+ })
5410
5499
  });
5411
5500
  };
5412
5501
  var Video = Internals23.wrapInSchema(VideoInner, videoSchema);
package/dist/index.d.ts CHANGED
@@ -39,7 +39,11 @@ export declare const experimental_Video: import("react").ComponentType<{
39
39
  onError: import("./on-error").MediaOnError | undefined;
40
40
  credentials: RequestCredentials | undefined;
41
41
  objectFit: import(".").VideoObjectFit;
42
- }>>;
42
+ _experimentalInitiallyDrawCachedFrame: boolean;
43
+ }> & {
44
+ from?: number | undefined;
45
+ durationInFrames?: number | undefined;
46
+ }>;
43
47
  export { AudioForPreview } from './audio/audio-for-preview';
44
48
  export { AudioProps, FallbackHtml5AudioProps } from './audio/props';
45
49
  export { MediaErrorAction } from './on-error';
@@ -51,7 +51,17 @@ type OptionalVideoProps = {
51
51
  onError: MediaOnError | undefined;
52
52
  credentials: RequestCredentials | undefined;
53
53
  objectFit: VideoObjectFit;
54
+ _experimentalInitiallyDrawCachedFrame: boolean;
54
55
  };
55
56
  export type InnerVideoProps = MandatoryVideoProps & OuterVideoProps & OptionalVideoProps;
56
- export type VideoProps = MandatoryVideoProps & Partial<OuterVideoProps> & Partial<OptionalVideoProps>;
57
+ export type VideoProps = MandatoryVideoProps & Partial<OuterVideoProps> & Partial<OptionalVideoProps> & {
58
+ /**
59
+ * When set, `<Video>` applies timing via an inner `<Sequence layout="none">` that is hidden from the timeline (`showInTimeline={false}`) so the clip still appears once as media.
60
+ */
61
+ from?: number;
62
+ /**
63
+ * Bounds the clip in frames together with `from`. Defaults to `Infinity` like `<Sequence>`.
64
+ */
65
+ durationInFrames?: number;
66
+ };
57
67
  export {};
@@ -27,6 +27,7 @@ type VideoForPreviewProps = {
27
27
  readonly onError: MediaOnError | undefined;
28
28
  readonly credentials: RequestCredentials | undefined;
29
29
  readonly objectFit: VideoObjectFit;
30
+ readonly _experimentalInitiallyDrawCachedFrame: boolean;
30
31
  };
31
32
  export declare const VideoForPreview: React.FC<VideoForPreviewProps & {
32
33
  readonly controls: SequenceControls | undefined;
@@ -0,0 +1,2 @@
1
+ export declare const cacheVideoFrame: (src: string, sourceCanvas: HTMLCanvasElement | OffscreenCanvas) => void;
2
+ export declare const getCachedVideoFrame: (src: string) => OffscreenCanvas | null;
@@ -31,4 +31,8 @@ export declare const Video: React.ComponentType<{
31
31
  onError: import("../on-error").MediaOnError | undefined;
32
32
  credentials: RequestCredentials | undefined;
33
33
  objectFit: import("./props").VideoObjectFit;
34
- }>>;
34
+ _experimentalInitiallyDrawCachedFrame: boolean;
35
+ }> & {
36
+ from?: number | undefined;
37
+ durationInFrames?: number | undefined;
38
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotion/media",
3
- "version": "4.0.445",
3
+ "version": "4.0.447",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/esm/index.mjs",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "mediabunny": "1.39.2",
26
- "remotion": "4.0.445",
26
+ "remotion": "4.0.447",
27
27
  "zod": "4.3.6"
28
28
  },
29
29
  "peerDependencies": {
@@ -31,7 +31,7 @@
31
31
  "react-dom": ">=16.8.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@remotion/eslint-config-internal": "4.0.445",
34
+ "@remotion/eslint-config-internal": "4.0.447",
35
35
  "@vitest/browser-webdriverio": "4.0.9",
36
36
  "eslint": "9.19.0",
37
37
  "react": "19.2.3",
@@ -1,5 +0,0 @@
1
- export declare const calculatePlaybackTime: ({ audioSyncAnchor, currentTime, playbackRate, }: {
2
- audioSyncAnchor: number;
3
- currentTime: number;
4
- playbackRate: number;
5
- }) => number;