@remotion/web-renderer 4.0.446 → 4.0.448

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.
package/dist/audio.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { TRenderAsset } from 'remotion';
2
- export declare const onlyInlineAudio: ({ assets, fps, timestamp, }: {
2
+ export declare const onlyInlineAudio: ({ assets, fps, timestamp, sampleRate, }: {
3
3
  assets: TRenderAsset[];
4
4
  fps: number;
5
5
  timestamp: number;
6
+ sampleRate: number;
6
7
  }) => AudioData | null;
@@ -1,12 +1,13 @@
1
1
  import { type ComponentType } from 'react';
2
2
  import type { Codec, DelayRenderScope, LogLevel, TRenderAsset } from 'remotion';
3
3
  import type { $ZodObject } from 'zod/v4/core';
4
+ import type { HtmlInCanvasContext } from './html-in-canvas';
4
5
  import type { TimeUpdaterRef } from './update-time';
5
6
  export type ErrorHolder = {
6
7
  error: Error | null;
7
8
  };
8
9
  export declare function checkForError(errorHolder: ErrorHolder): void;
9
- export declare function createScaffold<Props extends Record<string, unknown>>({ width, height, delayRenderTimeoutInMilliseconds, logLevel, resolvedProps, id, mediaCacheSizeInBytes, durationInFrames, fps, initialFrame, schema, Component, audioEnabled, videoEnabled, defaultCodec, defaultOutName }: {
10
+ export declare function createScaffold<Props extends Record<string, unknown>>({ width, height, delayRenderTimeoutInMilliseconds, logLevel, resolvedProps, id, mediaCacheSizeInBytes, durationInFrames, fps, initialFrame, schema, Component, audioEnabled, videoEnabled, defaultCodec, defaultOutName, allowHtmlInCanvas }: {
10
11
  width: number;
11
12
  height: number;
12
13
  delayRenderTimeoutInMilliseconds: number;
@@ -23,6 +24,7 @@ export declare function createScaffold<Props extends Record<string, unknown>>({
23
24
  videoEnabled: boolean;
24
25
  defaultCodec: Codec | null;
25
26
  defaultOutName: string | null;
27
+ allowHtmlInCanvas: boolean;
26
28
  }): {
27
29
  delayRenderScope: DelayRenderScope;
28
30
  div: HTMLDivElement;
@@ -31,5 +33,6 @@ export declare function createScaffold<Props extends Record<string, unknown>>({
31
33
  collectAssets: () => TRenderAsset[];
32
34
  } | null>;
33
35
  errorHolder: ErrorHolder;
36
+ htmlInCanvasContext: HtmlInCanvasContext | null;
34
37
  [Symbol.dispose]: () => void;
35
38
  };
@@ -16,6 +16,7 @@ export declare const calculateTransforms: ({ element, rootElement, }: {
16
16
  precompositing: {
17
17
  needs3DTransformViaWebGL: boolean;
18
18
  needsMaskImage: LinearGradientInfo | null;
19
+ needsFilter: string | null;
19
20
  needsPrecompositing: boolean;
20
21
  };
21
22
  };
@@ -0,0 +1 @@
1
+ export declare const getPrecomposeRectForFilter: (element: HTMLElement | SVGElement) => DOMRect;
@@ -584,7 +584,7 @@ var getEncodableAudioCodecs = async (container, options) => {
584
584
  };
585
585
  // src/render-media-on-web.tsx
586
586
  import { BufferTarget, StreamTarget } from "mediabunny";
587
- import { Internals as Internals8 } from "remotion";
587
+ import { Internals as Internals9 } from "remotion";
588
588
  import { VERSION } from "remotion/version";
589
589
 
590
590
  // src/add-sample.ts
@@ -670,7 +670,6 @@ var handleArtifacts = () => {
670
670
 
671
671
  // src/audio.ts
672
672
  var TARGET_NUMBER_OF_CHANNELS = 2;
673
- var TARGET_SAMPLE_RATE = 48000;
674
673
  function mixAudio(waves, length) {
675
674
  if (waves.length === 1 && waves[0].length === length) {
676
675
  return waves[0];
@@ -691,13 +690,14 @@ function mixAudio(waves, length) {
691
690
  var onlyInlineAudio = ({
692
691
  assets,
693
692
  fps,
694
- timestamp
693
+ timestamp,
694
+ sampleRate
695
695
  }) => {
696
696
  const inlineAudio = assets.filter((asset) => asset.type === "inline-audio");
697
697
  if (inlineAudio.length === 0) {
698
698
  return null;
699
699
  }
700
- const expectedLength = Math.round(TARGET_NUMBER_OF_CHANNELS * TARGET_SAMPLE_RATE / fps);
700
+ const expectedLength = Math.round(TARGET_NUMBER_OF_CHANNELS * sampleRate / fps);
701
701
  for (const asset of inlineAudio) {
702
702
  if (asset.toneFrequency !== 1) {
703
703
  throw new Error("Setting the toneFrequency is not supported yet in web rendering.");
@@ -709,7 +709,7 @@ var onlyInlineAudio = ({
709
709
  format: "s16",
710
710
  numberOfChannels: TARGET_NUMBER_OF_CHANNELS,
711
711
  numberOfFrames: expectedLength / TARGET_NUMBER_OF_CHANNELS,
712
- sampleRate: TARGET_SAMPLE_RATE,
712
+ sampleRate,
713
713
  timestamp
714
714
  });
715
715
  };
@@ -823,6 +823,82 @@ import { flushSync as flushSync2 } from "react-dom";
823
823
  import ReactDOM from "react-dom/client";
824
824
  import { Internals as Internals3 } from "remotion";
825
825
 
826
+ // src/html-in-canvas.ts
827
+ var supportsNativeHtmlInCanvas = () => {
828
+ if (typeof document === "undefined") {
829
+ return false;
830
+ }
831
+ const ctx = document.createElement("canvas").getContext("2d");
832
+ return typeof ctx?.drawElementImage === "function";
833
+ };
834
+ var setupHtmlInCanvas = ({
835
+ wrapper,
836
+ div,
837
+ width,
838
+ height
839
+ }) => {
840
+ if (!supportsNativeHtmlInCanvas()) {
841
+ return null;
842
+ }
843
+ const layoutCanvas = document.createElement("canvas");
844
+ layoutCanvas.layoutSubtree = true;
845
+ layoutCanvas.width = width;
846
+ layoutCanvas.height = height;
847
+ layoutCanvas.style.position = "absolute";
848
+ layoutCanvas.style.top = "0";
849
+ layoutCanvas.style.left = "0";
850
+ layoutCanvas.style.width = `${width}px`;
851
+ layoutCanvas.style.height = `${height}px`;
852
+ layoutCanvas.style.visibility = "visible";
853
+ const maybeCtx = layoutCanvas.getContext("2d");
854
+ if (!maybeCtx || typeof maybeCtx.drawElementImage !== "function") {
855
+ return null;
856
+ }
857
+ if (typeof layoutCanvas.requestPaint !== "function") {
858
+ return null;
859
+ }
860
+ wrapper.removeChild(div);
861
+ layoutCanvas.appendChild(div);
862
+ wrapper.appendChild(layoutCanvas);
863
+ return { layoutCanvas, ctx: maybeCtx };
864
+ };
865
+ var waitForPaint = (layoutCanvas) => {
866
+ return new Promise((resolve) => {
867
+ layoutCanvas.addEventListener("paint", () => resolve(), { once: true });
868
+ layoutCanvas.requestPaint();
869
+ });
870
+ };
871
+ var drawWithHtmlInCanvas = async ({
872
+ htmlInCanvasContext,
873
+ element,
874
+ scaledWidth,
875
+ scaledHeight
876
+ }) => {
877
+ const { ctx, layoutCanvas } = htmlInCanvasContext;
878
+ await waitForPaint(layoutCanvas);
879
+ layoutCanvas.width = scaledWidth;
880
+ layoutCanvas.height = scaledHeight;
881
+ ctx.reset();
882
+ ctx.drawElementImage(element, 0, 0, scaledWidth, scaledHeight);
883
+ const offscreen = new OffscreenCanvas(scaledWidth, scaledHeight);
884
+ const offCtx = offscreen.getContext("2d");
885
+ if (!offCtx) {
886
+ throw new Error("Could not get offscreen context");
887
+ }
888
+ offCtx.drawImage(layoutCanvas, 0, 0);
889
+ return offCtx;
890
+ };
891
+ var teardownHtmlInCanvas = ({
892
+ htmlInCanvasContext,
893
+ wrapper,
894
+ div
895
+ }) => {
896
+ const { layoutCanvas } = htmlInCanvasContext;
897
+ layoutCanvas.removeChild(div);
898
+ wrapper.removeChild(layoutCanvas);
899
+ wrapper.appendChild(div);
900
+ };
901
+
826
902
  // src/update-time.tsx
827
903
  import { useImperativeHandle, useState } from "react";
828
904
  import { flushSync } from "react-dom";
@@ -943,7 +1019,8 @@ function createScaffold({
943
1019
  audioEnabled,
944
1020
  videoEnabled,
945
1021
  defaultCodec,
946
- defaultOutName
1022
+ defaultOutName,
1023
+ allowHtmlInCanvas
947
1024
  }) {
948
1025
  if (!ReactDOM.createRoot) {
949
1026
  throw new Error("@remotion/web-renderer requires React 18 or higher");
@@ -969,6 +1046,7 @@ function createScaffold({
969
1046
  const cleanupCSS = Internals3.CSSUtils.injectCSS(Internals3.CSSUtils.makeDefaultPreviewCSS(`.${scaffoldClassName}`, "white"));
970
1047
  wrapper.appendChild(div);
971
1048
  document.body.appendChild(wrapper);
1049
+ const htmlInCanvasContext = allowHtmlInCanvas ? setupHtmlInCanvas({ wrapper, div, width, height }) : null;
972
1050
  const errorHolder = { error: null };
973
1051
  const root = ReactDOM.createRoot(div, {
974
1052
  onUncaughtError: (err, errorInfo) => {
@@ -1029,7 +1107,8 @@ function createScaffold({
1029
1107
  defaultOutName: defaultOutName ?? null,
1030
1108
  defaultVideoImageFormat: null,
1031
1109
  defaultPixelFormat: null,
1032
- defaultProResProfile: null
1110
+ defaultProResProfile: null,
1111
+ defaultSampleRate: null
1033
1112
  },
1034
1113
  folders: []
1035
1114
  },
@@ -1059,8 +1138,12 @@ function createScaffold({
1059
1138
  delayRenderScope,
1060
1139
  div,
1061
1140
  errorHolder,
1141
+ htmlInCanvasContext,
1062
1142
  [Symbol.dispose]: () => {
1063
1143
  root.unmount();
1144
+ if (htmlInCanvasContext) {
1145
+ teardownHtmlInCanvas({ htmlInCanvasContext, wrapper, div });
1146
+ }
1064
1147
  div.remove();
1065
1148
  wrapper.remove();
1066
1149
  cleanupCSS();
@@ -1274,6 +1357,9 @@ var sendUsageEvent = async ({
1274
1357
  });
1275
1358
  };
1276
1359
 
1360
+ // src/take-screenshot.ts
1361
+ import { Internals as Internals8 } from "remotion";
1362
+
1277
1363
  // src/tree-walker-cleanup-after-children.ts
1278
1364
  var createTreeWalkerCleanupAfterChildren = (treeWalker) => {
1279
1365
  const cleanupAfterChildren = [];
@@ -1972,6 +2058,7 @@ var calculateTransforms = ({
1972
2058
  let opacity = 1;
1973
2059
  let elementComputedStyle = null;
1974
2060
  let maskImageInfo = null;
2061
+ let filterValue = null;
1975
2062
  while (parent) {
1976
2063
  const computedStyle = getComputedStyle(parent);
1977
2064
  if (parent === element) {
@@ -1979,6 +2066,16 @@ var calculateTransforms = ({
1979
2066
  opacity = parseFloat(computedStyle.opacity);
1980
2067
  const maskImageValue = getMaskImageValue(computedStyle);
1981
2068
  maskImageInfo = maskImageValue ? parseMaskImage(maskImageValue) : null;
2069
+ const computedFilter = computedStyle.filter;
2070
+ if (computedFilter && computedFilter !== "none") {
2071
+ filterValue = computedFilter;
2072
+ const originalFilter = parent.style.filter;
2073
+ parent.style.filter = "none";
2074
+ const parentRefFilter = parent;
2075
+ toReset.push(() => {
2076
+ parentRefFilter.style.filter = originalFilter;
2077
+ });
2078
+ }
1982
2079
  const originalMaskImage = parent.style.maskImage;
1983
2080
  const originalWebkitMaskImage = parent.style.webkitMaskImage;
1984
2081
  parent.style.maskImage = "none";
@@ -2042,6 +2139,7 @@ var calculateTransforms = ({
2042
2139
  }
2043
2140
  const needs3DTransformViaWebGL = !totalMatrix.is2D;
2044
2141
  const needsMaskImage = maskImageInfo !== null;
2142
+ const needsFilter = filterValue !== null;
2045
2143
  return {
2046
2144
  dimensions,
2047
2145
  totalMatrix,
@@ -2057,7 +2155,8 @@ var calculateTransforms = ({
2057
2155
  precompositing: {
2058
2156
  needs3DTransformViaWebGL,
2059
2157
  needsMaskImage: maskImageInfo,
2060
- needsPrecompositing: Boolean(needs3DTransformViaWebGL || needsMaskImage)
2158
+ needsFilter: filterValue,
2159
+ needsPrecompositing: Boolean(needs3DTransformViaWebGL || needsMaskImage || needsFilter)
2061
2160
  }
2062
2161
  };
2063
2162
  };
@@ -3424,6 +3523,11 @@ var handle3dTransform = ({
3424
3523
  return transformed;
3425
3524
  };
3426
3525
 
3526
+ // src/drawing/handle-filter.ts
3527
+ var getPrecomposeRectForFilter = (element) => {
3528
+ return getBiggestBoundingClientRect(element);
3529
+ };
3530
+
3427
3531
  // src/drawing/handle-mask.ts
3428
3532
  var getPrecomposeRectForMask = (element) => {
3429
3533
  const boundingRect = getBiggestBoundingClientRect(element);
@@ -3511,6 +3615,12 @@ var processNode = async ({
3511
3615
  if (precompositing.needsMaskImage) {
3512
3616
  precomposeRect = roundToExpandRect(getPrecomposeRectForMask(element));
3513
3617
  }
3618
+ if (precompositing.needsFilter) {
3619
+ precomposeRect = roundToExpandRect(getWiderRectAndExpand({
3620
+ firstRect: precomposeRect,
3621
+ secondRect: getPrecomposeRectForFilter(element)
3622
+ }));
3623
+ }
3514
3624
  if (precompositing.needs3DTransformViaWebGL) {
3515
3625
  const tentativePrecomposeRect = getPrecomposeRectFor3DTransform({
3516
3626
  element,
@@ -3573,8 +3683,13 @@ var processNode = async ({
3573
3683
  }
3574
3684
  }
3575
3685
  const previousTransform = context.getTransform();
3686
+ const previousFilter = context.filter;
3576
3687
  context.setTransform(new DOMMatrix);
3688
+ if (precompositing.needsFilter) {
3689
+ context.filter = precompositing.needsFilter;
3690
+ }
3577
3691
  context.drawImage(drawable, 0, drawable.height - rectAfterTransforms.height, rectAfterTransforms.width, rectAfterTransforms.height, rectAfterTransforms.left - parentRect.x, rectAfterTransforms.top - parentRect.y, rectAfterTransforms.width, rectAfterTransforms.height);
3692
+ context.filter = previousFilter;
3578
3693
  context.setTransform(previousTransform);
3579
3694
  Internals6.Log.trace({
3580
3695
  logLevel,
@@ -3918,10 +4033,32 @@ var createLayer = async ({
3918
4033
  logLevel,
3919
4034
  internalState,
3920
4035
  onlyBackgroundClipText,
3921
- cutout
4036
+ cutout,
4037
+ htmlInCanvasContext,
4038
+ onHtmlInCanvasLayerOutcome
3922
4039
  }) => {
3923
4040
  const scaledWidth = Math.ceil(cutout.width * scale);
3924
4041
  const scaledHeight = Math.ceil(cutout.height * scale);
4042
+ if (!onlyBackgroundClipText && element instanceof HTMLElement && htmlInCanvasContext && onHtmlInCanvasLayerOutcome) {
4043
+ try {
4044
+ const offCtx = await drawWithHtmlInCanvas({
4045
+ htmlInCanvasContext,
4046
+ element,
4047
+ scaledWidth,
4048
+ scaledHeight
4049
+ });
4050
+ onHtmlInCanvasLayerOutcome({ native: true });
4051
+ return offCtx;
4052
+ } catch (err) {
4053
+ const detail = err instanceof Error ? err.message : JSON.stringify(err);
4054
+ onHtmlInCanvasLayerOutcome({
4055
+ native: false,
4056
+ reason: `drawElementImage failed (${detail}); falling back to the built-in DOM composer.`,
4057
+ shouldWarn: true
4058
+ });
4059
+ Internals8.Log.verbose({ logLevel, tag: "@remotion/web-renderer" }, "HTML-in-canvas capture failed, falling back to software compose", err);
4060
+ }
4061
+ }
3925
4062
  const canvas = new OffscreenCanvas(scaledWidth, scaledHeight);
3926
4063
  const context = canvas.getContext("2d");
3927
4064
  if (!context) {
@@ -4166,11 +4303,25 @@ var internalRenderMediaOnWeb = async ({
4166
4303
  licenseKey,
4167
4304
  muted,
4168
4305
  scale,
4169
- isProduction
4306
+ isProduction,
4307
+ allowHtmlInCanvas,
4308
+ sampleRate
4170
4309
  }) => {
4171
4310
  let __stack2 = [];
4172
4311
  try {
4173
4312
  validateScale(scale);
4313
+ let htmlInCanvasLayerOutcomeReported = false;
4314
+ const onHtmlInCanvasLayerOutcome = (outcome) => {
4315
+ if (htmlInCanvasLayerOutcomeReported) {
4316
+ return;
4317
+ }
4318
+ htmlInCanvasLayerOutcomeReported = true;
4319
+ if (outcome.native) {
4320
+ Internals9.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, "Using Chromium experimental HTML-in-canvas (drawElementImage) for video frames. See https://github.com/WICG/html-in-canvas");
4321
+ } else if (outcome.shouldWarn) {
4322
+ Internals9.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, `Not using HTML-in-canvas: ${outcome.reason}`);
4323
+ }
4324
+ };
4174
4325
  const outputTarget = userDesiredOutputTarget === null ? await canUseWebFsWriter() ? "web-fs" : "arraybuffer" : userDesiredOutputTarget;
4175
4326
  if (outputTarget === "web-fs") {
4176
4327
  await cleanupStaleOpfsFiles();
@@ -4201,11 +4352,11 @@ var internalRenderMediaOnWeb = async ({
4201
4352
  if (issue.severity === "error") {
4202
4353
  return Promise.reject(new Error(issue.message));
4203
4354
  }
4204
- Internals8.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, issue.message);
4355
+ Internals9.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, issue.message);
4205
4356
  }
4206
4357
  finalAudioCodec = audioResult.codec;
4207
4358
  }
4208
- const resolved = await Internals8.resolveVideoConfig({
4359
+ const resolved = await Internals9.resolveVideoConfig({
4209
4360
  calculateMetadata: composition.calculateMetadata ?? null,
4210
4361
  signal: signal ?? new AbortController().signal,
4211
4362
  defaultProps: composition.defaultProps ?? {},
@@ -4236,9 +4387,38 @@ var internalRenderMediaOnWeb = async ({
4236
4387
  videoEnabled,
4237
4388
  initialFrame: 0,
4238
4389
  defaultCodec: resolved.defaultCodec,
4239
- defaultOutName: resolved.defaultOutName
4390
+ defaultOutName: resolved.defaultOutName,
4391
+ allowHtmlInCanvas
4240
4392
  }), 0);
4241
- const { delayRenderScope, div, timeUpdater, collectAssets, errorHolder } = scaffold;
4393
+ const {
4394
+ delayRenderScope,
4395
+ div,
4396
+ timeUpdater,
4397
+ collectAssets,
4398
+ errorHolder,
4399
+ htmlInCanvasContext
4400
+ } = scaffold;
4401
+ if (allowHtmlInCanvas && !htmlInCanvasContext) {
4402
+ if (!supportsNativeHtmlInCanvas()) {
4403
+ onHtmlInCanvasLayerOutcome({
4404
+ native: false,
4405
+ reason: "This browser does not expose CanvasRenderingContext2D.prototype.drawElementImage. In Chromium, enable chrome://flags/#canvas-draw-element and use a version that ships the API.",
4406
+ shouldWarn: false
4407
+ });
4408
+ } else {
4409
+ onHtmlInCanvasLayerOutcome({
4410
+ native: false,
4411
+ reason: "drawElementImage is available but canvas.requestPaint() is missing. Use a Chromium version that ships requestPaint.",
4412
+ shouldWarn: true
4413
+ });
4414
+ }
4415
+ } else if (!allowHtmlInCanvas) {
4416
+ onHtmlInCanvasLayerOutcome({
4417
+ native: false,
4418
+ reason: "allowHtmlInCanvas is false; using the built-in DOM composer.",
4419
+ shouldWarn: false
4420
+ });
4421
+ }
4242
4422
  const internalState = __using(__stack2, makeInternalState(), 0);
4243
4423
  const keepalive = __using(__stack2, createBackgroundKeepalive({
4244
4424
  fps: resolved.fps,
@@ -4351,7 +4531,9 @@ var internalRenderMediaOnWeb = async ({
4351
4531
  logLevel,
4352
4532
  internalState,
4353
4533
  onlyBackgroundClipText: false,
4354
- cutout: new DOMRect(0, 0, resolved.width, resolved.height)
4534
+ cutout: new DOMRect(0, 0, resolved.width, resolved.height),
4535
+ htmlInCanvasContext,
4536
+ onHtmlInCanvasLayerOutcome: htmlInCanvasContext ? onHtmlInCanvasLayerOutcome : undefined
4355
4537
  });
4356
4538
  internalState.addCreateFrameTime(performance.now() - createFrameStart);
4357
4539
  layerCanvas = layer.canvas;
@@ -4402,7 +4584,7 @@ var internalRenderMediaOnWeb = async ({
4402
4584
  if (signal?.aborted) {
4403
4585
  throw new Error("renderMediaOnWeb() was cancelled");
4404
4586
  }
4405
- const audio = muted ? null : onlyInlineAudio({ assets, fps: resolved.fps, timestamp });
4587
+ const audio = muted ? null : onlyInlineAudio({ assets, fps: resolved.fps, timestamp, sampleRate });
4406
4588
  internalState.addAudioMixingTime(performance.now() - audioCombineStart);
4407
4589
  const addSampleStart = performance.now();
4408
4590
  const encodingPromises = [];
@@ -4427,7 +4609,7 @@ var internalRenderMediaOnWeb = async ({
4427
4609
  videoSampleSource?.videoSampleSource.close();
4428
4610
  audioSampleSource?.audioSampleSource.close();
4429
4611
  await outputWithCleanup.output.finalize();
4430
- Internals8.Log.verbose({ logLevel, tag: "web-renderer" }, `Render timings: waitForReady=${internalState.getWaitForReadyTime().toFixed(2)}ms, createFrame=${internalState.getCreateFrameTime().toFixed(2)}ms, addSample=${internalState.getAddSampleTime().toFixed(2)}ms, audioMixing=${internalState.getAudioMixingTime().toFixed(2)}ms`);
4612
+ Internals9.Log.verbose({ logLevel, tag: "web-renderer" }, `Render timings: waitForReady=${internalState.getWaitForReadyTime().toFixed(2)}ms, createFrame=${internalState.getCreateFrameTime().toFixed(2)}ms, addSample=${internalState.getAddSampleTime().toFixed(2)}ms, audioMixing=${internalState.getAudioMixingTime().toFixed(2)}ms`);
4431
4613
  if (webFsTarget) {
4432
4614
  sendUsageEvent({
4433
4615
  licenseKey: licenseKey ?? null,
@@ -4478,7 +4660,7 @@ var internalRenderMediaOnWeb = async ({
4478
4660
  isStill: false,
4479
4661
  isProduction: isProduction ?? true
4480
4662
  }).catch((err2) => {
4481
- Internals8.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
4663
+ Internals9.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
4482
4664
  });
4483
4665
  }
4484
4666
  throw err;
@@ -4515,33 +4697,80 @@ var renderMediaOnWeb = (options) => {
4515
4697
  licenseKey: options.licenseKey ?? null,
4516
4698
  muted: options.muted ?? false,
4517
4699
  scale: options.scale ?? 1,
4518
- isProduction: options.isProduction ?? true
4700
+ isProduction: options.isProduction ?? true,
4701
+ allowHtmlInCanvas: options.allowHtmlInCanvas ?? false,
4702
+ sampleRate: options.sampleRate ?? 48000
4519
4703
  }));
4520
4704
  return onlyOneRenderAtATimeQueue.ref;
4521
4705
  };
4522
4706
  // src/render-still-on-web.tsx
4523
4707
  import {
4524
- Internals as Internals9
4708
+ Internals as Internals10
4525
4709
  } from "remotion";
4710
+
4711
+ // src/render-still-screenshot-task.ts
4712
+ var mimeTypeForFormat = (format) => {
4713
+ if (format === "jpeg") {
4714
+ return "image/jpeg";
4715
+ }
4716
+ if (format === "webp") {
4717
+ return "image/webp";
4718
+ }
4719
+ return "image/png";
4720
+ };
4721
+ var encodeCanvasToBlob = async (canvas, options) => {
4722
+ const format = options?.format ?? "png";
4723
+ const type = mimeTypeForFormat(format);
4724
+ if (format === "png") {
4725
+ return canvas.convertToBlob({ type });
4726
+ }
4727
+ return canvas.convertToBlob({
4728
+ type,
4729
+ quality: options?.quality
4730
+ });
4731
+ };
4732
+ var createRenderStillOnWebResult = ({
4733
+ canvas,
4734
+ internalState
4735
+ }) => {
4736
+ return {
4737
+ internalState,
4738
+ canvas: () => Promise.resolve(canvas),
4739
+ blob: (options) => encodeCanvasToBlob(canvas, options),
4740
+ url: async (options) => {
4741
+ const blob = await encodeCanvasToBlob(canvas, options);
4742
+ return URL.createObjectURL(blob);
4743
+ }
4744
+ };
4745
+ };
4746
+
4747
+ // src/render-still-on-web.tsx
4526
4748
  async function internalRenderStillOnWeb({
4527
4749
  frame,
4528
4750
  delayRenderTimeoutInMilliseconds,
4529
4751
  logLevel,
4530
4752
  inputProps,
4531
4753
  schema,
4532
- imageFormat,
4533
4754
  mediaCacheSizeInBytes,
4534
4755
  composition,
4535
4756
  signal,
4536
4757
  onArtifact,
4537
4758
  licenseKey,
4538
4759
  scale,
4539
- isProduction
4760
+ isProduction,
4761
+ allowHtmlInCanvas
4540
4762
  }) {
4541
4763
  let __stack = [];
4542
4764
  try {
4543
4765
  validateScale(scale);
4544
- const resolved = await Internals9.resolveVideoConfig({
4766
+ const onHtmlInCanvasLayerOutcome = (outcome) => {
4767
+ if (outcome.native) {
4768
+ Internals10.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, "Using Chromium experimental HTML-in-canvas (drawElementImage) for this frame. Pixels may differ from the built-in DOM composer. Set allowHtmlInCanvas: false to force software rasterization. See https://github.com/WICG/html-in-canvas");
4769
+ } else if (outcome.shouldWarn) {
4770
+ Internals10.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, `Not using HTML-in-canvas: ${outcome.reason}`);
4771
+ }
4772
+ };
4773
+ const resolved = await Internals10.resolveVideoConfig({
4545
4774
  calculateMetadata: composition.calculateMetadata ?? null,
4546
4775
  signal: signal ?? new AbortController().signal,
4547
4776
  defaultProps: composition.defaultProps ?? {},
@@ -4572,9 +4801,37 @@ async function internalRenderStillOnWeb({
4572
4801
  schema: schema ?? null,
4573
4802
  initialFrame: frame,
4574
4803
  defaultCodec: resolved.defaultCodec,
4575
- defaultOutName: resolved.defaultOutName
4804
+ defaultOutName: resolved.defaultOutName,
4805
+ allowHtmlInCanvas
4576
4806
  }), 0);
4577
- const { delayRenderScope, div, collectAssets, errorHolder } = scaffold;
4807
+ const {
4808
+ delayRenderScope,
4809
+ div,
4810
+ collectAssets,
4811
+ errorHolder,
4812
+ htmlInCanvasContext
4813
+ } = scaffold;
4814
+ if (allowHtmlInCanvas && !htmlInCanvasContext) {
4815
+ if (!supportsNativeHtmlInCanvas()) {
4816
+ onHtmlInCanvasLayerOutcome({
4817
+ native: false,
4818
+ reason: "This browser does not expose CanvasRenderingContext2D.prototype.drawElementImage. In Chromium, enable chrome://flags/#canvas-draw-element and use a version that ships the API.",
4819
+ shouldWarn: false
4820
+ });
4821
+ } else {
4822
+ onHtmlInCanvasLayerOutcome({
4823
+ native: false,
4824
+ reason: "drawElementImage is available but canvas.requestPaint() is missing. Use a Chromium version that ships requestPaint.",
4825
+ shouldWarn: true
4826
+ });
4827
+ }
4828
+ } else if (!allowHtmlInCanvas) {
4829
+ onHtmlInCanvasLayerOutcome({
4830
+ native: false,
4831
+ reason: "allowHtmlInCanvas is false; using the built-in DOM composer.",
4832
+ shouldWarn: false
4833
+ });
4834
+ }
4578
4835
  const artifactsHandler = handleArtifacts();
4579
4836
  try {
4580
4837
  if (signal?.aborted) {
@@ -4598,14 +4855,19 @@ async function internalRenderStillOnWeb({
4598
4855
  logLevel,
4599
4856
  internalState,
4600
4857
  onlyBackgroundClipText: false,
4601
- cutout: new DOMRect(0, 0, resolved.width, resolved.height)
4602
- });
4603
- const imageData = await capturedFrame.canvas.convertToBlob({
4604
- type: `image/${imageFormat}`
4858
+ cutout: new DOMRect(0, 0, resolved.width, resolved.height),
4859
+ htmlInCanvasContext,
4860
+ onHtmlInCanvasLayerOutcome: htmlInCanvasContext ? onHtmlInCanvasLayerOutcome : undefined
4605
4861
  });
4862
+ const { canvas } = capturedFrame;
4606
4863
  const assets = collectAssets.current.collectAssets();
4607
4864
  if (onArtifact) {
4608
- await artifactsHandler.handle({ imageData, frame, assets, onArtifact });
4865
+ await artifactsHandler.handle({
4866
+ imageData: canvas,
4867
+ frame,
4868
+ assets,
4869
+ onArtifact
4870
+ });
4609
4871
  }
4610
4872
  sendUsageEvent({
4611
4873
  licenseKey: licenseKey ?? null,
@@ -4614,7 +4876,7 @@ async function internalRenderStillOnWeb({
4614
4876
  isStill: true,
4615
4877
  isProduction
4616
4878
  });
4617
- return { blob: imageData, internalState };
4879
+ return createRenderStillOnWebResult({ canvas, internalState });
4618
4880
  } catch (err) {
4619
4881
  if (!signal?.aborted) {
4620
4882
  sendUsageEvent({
@@ -4624,7 +4886,7 @@ async function internalRenderStillOnWeb({
4624
4886
  isStill: true,
4625
4887
  isProduction
4626
4888
  }).catch((err2) => {
4627
- Internals9.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
4889
+ Internals10.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
4628
4890
  });
4629
4891
  }
4630
4892
  throw err;
@@ -4646,7 +4908,8 @@ var renderStillOnWeb = (options) => {
4646
4908
  onArtifact: options.onArtifact ?? null,
4647
4909
  licenseKey: options.licenseKey ?? null,
4648
4910
  scale: options.scale ?? 1,
4649
- isProduction: options.isProduction ?? true
4911
+ isProduction: options.isProduction ?? true,
4912
+ allowHtmlInCanvas: options.allowHtmlInCanvas ?? false
4650
4913
  }));
4651
4914
  return onlyOneRenderAtATimeQueue.ref;
4652
4915
  };
@@ -0,0 +1,43 @@
1
+ type Canvas2DWithDrawElement = CanvasRenderingContext2D & {
2
+ drawElementImage: (element: Element, dx: number, dy: number, dwidth: number, dheight: number) => DOMMatrix;
3
+ };
4
+ type HTMLCanvasWithLayoutSubtree = HTMLCanvasElement & {
5
+ layoutSubtree?: boolean;
6
+ requestPaint?: () => void;
7
+ };
8
+ export declare const supportsNativeHtmlInCanvas: () => boolean;
9
+ export type HtmlInCanvasContext = {
10
+ layoutCanvas: HTMLCanvasWithLayoutSubtree;
11
+ ctx: Canvas2DWithDrawElement;
12
+ };
13
+ /**
14
+ * Sets up a persistent layoutsubtree canvas that wraps the scaffold div.
15
+ * The div becomes a direct child of the canvas, which is required for drawElementImage.
16
+ * Must be called once before rendering begins; the canvas stays in the DOM for the
17
+ * lifetime of the render.
18
+ */
19
+ export declare const setupHtmlInCanvas: ({ wrapper, div, width, height, }: {
20
+ wrapper: HTMLDivElement;
21
+ div: HTMLDivElement;
22
+ width: number;
23
+ height: number;
24
+ }) => HtmlInCanvasContext | null;
25
+ /**
26
+ * Triggers a fresh paint record via requestPaint(), waits for the paint event,
27
+ * then captures the element into an OffscreenCanvas using drawElementImage.
28
+ *
29
+ * The caller is responsible for ensuring the frame content is ready (via
30
+ * waitForReady) before calling this function.
31
+ */
32
+ export declare const drawWithHtmlInCanvas: ({ htmlInCanvasContext, element, scaledWidth, scaledHeight, }: {
33
+ htmlInCanvasContext: HtmlInCanvasContext;
34
+ element: HTMLElement;
35
+ scaledWidth: number;
36
+ scaledHeight: number;
37
+ }) => Promise<OffscreenCanvasRenderingContext2D>;
38
+ export declare const teardownHtmlInCanvas: ({ htmlInCanvasContext, wrapper, div, }: {
39
+ htmlInCanvasContext: HtmlInCanvasContext;
40
+ wrapper: HTMLDivElement;
41
+ div: HTMLDivElement;
42
+ }) => void;
43
+ export {};
package/dist/index.d.ts CHANGED
@@ -10,5 +10,6 @@ export type { WebRendererOutputTarget } from './output-target';
10
10
  export { renderMediaOnWeb } from './render-media-on-web';
11
11
  export type { RenderMediaOnWebOptions, RenderMediaOnWebProgress, RenderMediaOnWebProgressCallback, RenderMediaOnWebResult, WebRendererHardwareAcceleration, } from './render-media-on-web';
12
12
  export { renderStillOnWeb } from './render-still-on-web';
13
- export type { RenderStillOnWebImageFormat, RenderStillOnWebOptions, } from './render-still-on-web';
13
+ export type { RenderStillOnWebEncodeOptions, RenderStillOnWebImageFormat, RenderStillOnWebOptions, RenderStillOnWebResult, } from './render-still-on-web';
14
+ export type { HtmlInCanvasLayerOutcome } from './take-screenshot';
14
15
  export type { OnFrameCallback } from './validate-video-frame';
@@ -65,6 +65,8 @@ type OptionalRenderMediaOnWebOptions<Schema extends $ZodObject> = {
65
65
  isProduction: boolean;
66
66
  muted: boolean;
67
67
  scale: number;
68
+ allowHtmlInCanvas: boolean;
69
+ sampleRate: number;
68
70
  };
69
71
  export type RenderMediaOnWebOptions<Schema extends $ZodObject, Props extends Record<string, unknown>> = MandatoryRenderMediaOnWebOptions<Schema, Props> & Partial<OptionalRenderMediaOnWebOptions<Schema>> & InputPropsIfHasProps<Schema, Props>;
70
72
  export declare const renderMediaOnWeb: <Schema extends $ZodObject<Readonly<Readonly<{
@@ -3,10 +3,10 @@ import type { $ZodObject } from 'zod/v4/core';
3
3
  import type { WebRendererOnArtifact } from './artifact';
4
4
  import type { CompositionCalculateMetadataOrExplicit } from './props-if-has-props';
5
5
  import type { InputPropsIfHasProps } from './render-media-on-web';
6
- export type RenderStillOnWebImageFormat = 'png' | 'jpeg' | 'webp';
6
+ import { type RenderStillOnWebResult } from './render-still-screenshot-task';
7
+ export type { RenderStillOnWebEncodeOptions, RenderStillOnWebImageFormat, RenderStillOnWebResult, } from './render-still-screenshot-task';
7
8
  type MandatoryRenderStillOnWebOptions<Schema extends $ZodObject, Props extends Record<string, unknown>> = {
8
9
  frame: number;
9
- imageFormat: RenderStillOnWebImageFormat;
10
10
  } & {
11
11
  composition: CompositionCalculateMetadataOrExplicit<Schema, Props>;
12
12
  };
@@ -20,29 +20,9 @@ type OptionalRenderStillOnWebOptions<Schema extends $ZodObject> = {
20
20
  licenseKey: string | null;
21
21
  scale: number;
22
22
  isProduction: boolean;
23
+ allowHtmlInCanvas: boolean;
23
24
  };
24
25
  export type RenderStillOnWebOptions<Schema extends $ZodObject, Props extends Record<string, unknown>> = MandatoryRenderStillOnWebOptions<Schema, Props> & Partial<OptionalRenderStillOnWebOptions<Schema>> & InputPropsIfHasProps<Schema, Props>;
25
26
  export declare const renderStillOnWeb: <Schema extends $ZodObject<Readonly<Readonly<{
26
27
  [k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
27
- }>>, import("zod/v4/core").$ZodObjectConfig>, Props extends Record<string, unknown>>(options: RenderStillOnWebOptions<Schema, Props>) => Promise<{
28
- blob: Blob;
29
- internalState: {
30
- getDrawn3dPixels: () => number;
31
- getPrecomposedTiles: () => number;
32
- addPrecompose: ({ canvasWidth, canvasHeight, }: {
33
- canvasWidth: number;
34
- canvasHeight: number;
35
- }) => void;
36
- helperCanvasState: import("./internal-state").HelperCanvasState;
37
- [Symbol.dispose]: () => void;
38
- getWaitForReadyTime: () => number;
39
- addWaitForReadyTime: (time: number) => void;
40
- getAddSampleTime: () => number;
41
- addAddSampleTime: (time: number) => void;
42
- getCreateFrameTime: () => number;
43
- addCreateFrameTime: (time: number) => void;
44
- getAudioMixingTime: () => number;
45
- addAudioMixingTime: (time: number) => void;
46
- };
47
- }>;
48
- export {};
28
+ }>>, import("zod/v4/core").$ZodObjectConfig>, Props extends Record<string, unknown>>(options: RenderStillOnWebOptions<Schema, Props>) => Promise<RenderStillOnWebResult>;
@@ -0,0 +1,45 @@
1
+ import type { InternalState } from './internal-state';
2
+ export type RenderStillOnWebImageFormat = 'png' | 'jpeg' | 'webp';
3
+ export type RenderStillOnWebEncodeOptions = {
4
+ format?: RenderStillOnWebImageFormat;
5
+ /**
6
+ * Encoder quality for `jpeg` and `webp`, between `0` and `1`.
7
+ * Ignored for `png`.
8
+ */
9
+ quality?: number;
10
+ };
11
+ /**
12
+ * Outcome of `renderStillOnWeb()`. The frame is already rendered; use
13
+ * `canvas()`, `blob()`, or `url()` to read pixels or encode (same pattern as
14
+ * Renoun’s `screenshot()` task).
15
+ */
16
+ export type RenderStillOnWebResult = {
17
+ internalState: InternalState;
18
+ canvas(): Promise<OffscreenCanvas>;
19
+ blob(options?: RenderStillOnWebEncodeOptions): Promise<Blob>;
20
+ /**
21
+ * Creates an object URL from an encoded blob. Call `URL.revokeObjectURL()` when done.
22
+ */
23
+ url(options?: RenderStillOnWebEncodeOptions): Promise<string>;
24
+ };
25
+ export declare const createRenderStillOnWebResult: ({ canvas, internalState, }: {
26
+ canvas: OffscreenCanvas;
27
+ internalState: {
28
+ getDrawn3dPixels: () => number;
29
+ getPrecomposedTiles: () => number;
30
+ addPrecompose: ({ canvasWidth, canvasHeight, }: {
31
+ canvasWidth: number;
32
+ canvasHeight: number;
33
+ }) => void;
34
+ helperCanvasState: import("./internal-state").HelperCanvasState;
35
+ [Symbol.dispose]: () => void;
36
+ getWaitForReadyTime: () => number;
37
+ addWaitForReadyTime: (time: number) => void;
38
+ getAddSampleTime: () => number;
39
+ addAddSampleTime: (time: number) => void;
40
+ getCreateFrameTime: () => number;
41
+ addCreateFrameTime: (time: number) => void;
42
+ getAudioMixingTime: () => number;
43
+ addAudioMixingTime: (time: number) => void;
44
+ };
45
+ }) => RenderStillOnWebResult;
@@ -1,4 +1,12 @@
1
- export declare const createLayer: ({ element, scale, logLevel, internalState, onlyBackgroundClipText, cutout, }: {
1
+ import type { HtmlInCanvasContext } from './html-in-canvas';
2
+ export type HtmlInCanvasLayerOutcome = {
3
+ native: true;
4
+ } | {
5
+ native: false;
6
+ reason: string;
7
+ shouldWarn: boolean;
8
+ };
9
+ export declare const createLayer: ({ element, scale, logLevel, internalState, onlyBackgroundClipText, cutout, htmlInCanvasContext, onHtmlInCanvasLayerOutcome, }: {
2
10
  element: HTMLElement | SVGElement;
3
11
  scale: number;
4
12
  logLevel: "error" | "info" | "trace" | "verbose" | "warn";
@@ -22,4 +30,6 @@ export declare const createLayer: ({ element, scale, logLevel, internalState, on
22
30
  };
23
31
  onlyBackgroundClipText: boolean;
24
32
  cutout: DOMRect;
33
+ htmlInCanvasContext?: HtmlInCanvasContext | null | undefined;
34
+ onHtmlInCanvasLayerOutcome?: ((outcome: HtmlInCanvasLayerOutcome) => void) | undefined;
25
35
  }) => Promise<OffscreenCanvasRenderingContext2D>;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "url": "https://github.com/remotion-dev/remotion/tree/main/packages/web-renderer"
4
4
  },
5
5
  "name": "@remotion/web-renderer",
6
- "version": "4.0.446",
6
+ "version": "4.0.448",
7
7
  "main": "dist/index.js",
8
8
  "type": "module",
9
9
  "scripts": {
@@ -22,16 +22,16 @@
22
22
  "@mediabunny/mp3-encoder": "1.39.2",
23
23
  "@mediabunny/aac-encoder": "1.39.2",
24
24
  "@mediabunny/flac-encoder": "1.39.2",
25
- "@remotion/licensing": "4.0.446",
26
- "remotion": "4.0.446",
25
+ "@remotion/licensing": "4.0.448",
26
+ "remotion": "4.0.448",
27
27
  "mediabunny": "1.39.2"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@react-three/fiber": "9.2.0",
31
- "@remotion/eslint-config-internal": "4.0.446",
32
- "@remotion/player": "4.0.446",
33
- "@remotion/media": "4.0.446",
34
- "@remotion/three": "4.0.446",
31
+ "@remotion/eslint-config-internal": "4.0.448",
32
+ "@remotion/player": "4.0.448",
33
+ "@remotion/media": "4.0.448",
34
+ "@remotion/three": "4.0.448",
35
35
  "@types/three": "0.170.0",
36
36
  "@typescript/native-preview": "7.0.0-dev.20260217.1",
37
37
  "@vitejs/plugin-react": "4.3.4",