@remotion/web-renderer 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.
@@ -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
  };
@@ -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
@@ -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) => {
@@ -1059,8 +1137,12 @@ function createScaffold({
1059
1137
  delayRenderScope,
1060
1138
  div,
1061
1139
  errorHolder,
1140
+ htmlInCanvasContext,
1062
1141
  [Symbol.dispose]: () => {
1063
1142
  root.unmount();
1143
+ if (htmlInCanvasContext) {
1144
+ teardownHtmlInCanvas({ htmlInCanvasContext, wrapper, div });
1145
+ }
1064
1146
  div.remove();
1065
1147
  wrapper.remove();
1066
1148
  cleanupCSS();
@@ -1274,6 +1356,9 @@ var sendUsageEvent = async ({
1274
1356
  });
1275
1357
  };
1276
1358
 
1359
+ // src/take-screenshot.ts
1360
+ import { Internals as Internals8 } from "remotion";
1361
+
1277
1362
  // src/tree-walker-cleanup-after-children.ts
1278
1363
  var createTreeWalkerCleanupAfterChildren = (treeWalker) => {
1279
1364
  const cleanupAfterChildren = [];
@@ -3918,10 +4003,32 @@ var createLayer = async ({
3918
4003
  logLevel,
3919
4004
  internalState,
3920
4005
  onlyBackgroundClipText,
3921
- cutout
4006
+ cutout,
4007
+ htmlInCanvasContext,
4008
+ onHtmlInCanvasLayerOutcome
3922
4009
  }) => {
3923
4010
  const scaledWidth = Math.ceil(cutout.width * scale);
3924
4011
  const scaledHeight = Math.ceil(cutout.height * scale);
4012
+ if (!onlyBackgroundClipText && element instanceof HTMLElement && htmlInCanvasContext && onHtmlInCanvasLayerOutcome) {
4013
+ try {
4014
+ const offCtx = await drawWithHtmlInCanvas({
4015
+ htmlInCanvasContext,
4016
+ element,
4017
+ scaledWidth,
4018
+ scaledHeight
4019
+ });
4020
+ onHtmlInCanvasLayerOutcome({ native: true });
4021
+ return offCtx;
4022
+ } catch (err) {
4023
+ const detail = err instanceof Error ? err.message : JSON.stringify(err);
4024
+ onHtmlInCanvasLayerOutcome({
4025
+ native: false,
4026
+ reason: `drawElementImage failed (${detail}); falling back to the built-in DOM composer.`,
4027
+ shouldWarn: true
4028
+ });
4029
+ Internals8.Log.verbose({ logLevel, tag: "@remotion/web-renderer" }, "HTML-in-canvas capture failed, falling back to software compose", err);
4030
+ }
4031
+ }
3925
4032
  const canvas = new OffscreenCanvas(scaledWidth, scaledHeight);
3926
4033
  const context = canvas.getContext("2d");
3927
4034
  if (!context) {
@@ -4166,11 +4273,24 @@ var internalRenderMediaOnWeb = async ({
4166
4273
  licenseKey,
4167
4274
  muted,
4168
4275
  scale,
4169
- isProduction
4276
+ isProduction,
4277
+ allowHtmlInCanvas
4170
4278
  }) => {
4171
4279
  let __stack2 = [];
4172
4280
  try {
4173
4281
  validateScale(scale);
4282
+ let htmlInCanvasLayerOutcomeReported = false;
4283
+ const onHtmlInCanvasLayerOutcome = (outcome) => {
4284
+ if (htmlInCanvasLayerOutcomeReported) {
4285
+ return;
4286
+ }
4287
+ htmlInCanvasLayerOutcomeReported = true;
4288
+ if (outcome.native) {
4289
+ 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");
4290
+ } else if (outcome.shouldWarn) {
4291
+ Internals9.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, `Not using HTML-in-canvas: ${outcome.reason}`);
4292
+ }
4293
+ };
4174
4294
  const outputTarget = userDesiredOutputTarget === null ? await canUseWebFsWriter() ? "web-fs" : "arraybuffer" : userDesiredOutputTarget;
4175
4295
  if (outputTarget === "web-fs") {
4176
4296
  await cleanupStaleOpfsFiles();
@@ -4201,11 +4321,11 @@ var internalRenderMediaOnWeb = async ({
4201
4321
  if (issue.severity === "error") {
4202
4322
  return Promise.reject(new Error(issue.message));
4203
4323
  }
4204
- Internals8.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, issue.message);
4324
+ Internals9.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, issue.message);
4205
4325
  }
4206
4326
  finalAudioCodec = audioResult.codec;
4207
4327
  }
4208
- const resolved = await Internals8.resolveVideoConfig({
4328
+ const resolved = await Internals9.resolveVideoConfig({
4209
4329
  calculateMetadata: composition.calculateMetadata ?? null,
4210
4330
  signal: signal ?? new AbortController().signal,
4211
4331
  defaultProps: composition.defaultProps ?? {},
@@ -4236,9 +4356,38 @@ var internalRenderMediaOnWeb = async ({
4236
4356
  videoEnabled,
4237
4357
  initialFrame: 0,
4238
4358
  defaultCodec: resolved.defaultCodec,
4239
- defaultOutName: resolved.defaultOutName
4359
+ defaultOutName: resolved.defaultOutName,
4360
+ allowHtmlInCanvas
4240
4361
  }), 0);
4241
- const { delayRenderScope, div, timeUpdater, collectAssets, errorHolder } = scaffold;
4362
+ const {
4363
+ delayRenderScope,
4364
+ div,
4365
+ timeUpdater,
4366
+ collectAssets,
4367
+ errorHolder,
4368
+ htmlInCanvasContext
4369
+ } = scaffold;
4370
+ if (allowHtmlInCanvas && !htmlInCanvasContext) {
4371
+ if (!supportsNativeHtmlInCanvas()) {
4372
+ onHtmlInCanvasLayerOutcome({
4373
+ native: false,
4374
+ 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.",
4375
+ shouldWarn: false
4376
+ });
4377
+ } else {
4378
+ onHtmlInCanvasLayerOutcome({
4379
+ native: false,
4380
+ reason: "drawElementImage is available but canvas.requestPaint() is missing. Use a Chromium version that ships requestPaint.",
4381
+ shouldWarn: true
4382
+ });
4383
+ }
4384
+ } else if (!allowHtmlInCanvas) {
4385
+ onHtmlInCanvasLayerOutcome({
4386
+ native: false,
4387
+ reason: "allowHtmlInCanvas is false; using the built-in DOM composer.",
4388
+ shouldWarn: false
4389
+ });
4390
+ }
4242
4391
  const internalState = __using(__stack2, makeInternalState(), 0);
4243
4392
  const keepalive = __using(__stack2, createBackgroundKeepalive({
4244
4393
  fps: resolved.fps,
@@ -4351,7 +4500,9 @@ var internalRenderMediaOnWeb = async ({
4351
4500
  logLevel,
4352
4501
  internalState,
4353
4502
  onlyBackgroundClipText: false,
4354
- cutout: new DOMRect(0, 0, resolved.width, resolved.height)
4503
+ cutout: new DOMRect(0, 0, resolved.width, resolved.height),
4504
+ htmlInCanvasContext,
4505
+ onHtmlInCanvasLayerOutcome: htmlInCanvasContext ? onHtmlInCanvasLayerOutcome : undefined
4355
4506
  });
4356
4507
  internalState.addCreateFrameTime(performance.now() - createFrameStart);
4357
4508
  layerCanvas = layer.canvas;
@@ -4427,7 +4578,7 @@ var internalRenderMediaOnWeb = async ({
4427
4578
  videoSampleSource?.videoSampleSource.close();
4428
4579
  audioSampleSource?.audioSampleSource.close();
4429
4580
  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`);
4581
+ 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
4582
  if (webFsTarget) {
4432
4583
  sendUsageEvent({
4433
4584
  licenseKey: licenseKey ?? null,
@@ -4478,7 +4629,7 @@ var internalRenderMediaOnWeb = async ({
4478
4629
  isStill: false,
4479
4630
  isProduction: isProduction ?? true
4480
4631
  }).catch((err2) => {
4481
- Internals8.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
4632
+ Internals9.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
4482
4633
  });
4483
4634
  }
4484
4635
  throw err;
@@ -4515,33 +4666,79 @@ var renderMediaOnWeb = (options) => {
4515
4666
  licenseKey: options.licenseKey ?? null,
4516
4667
  muted: options.muted ?? false,
4517
4668
  scale: options.scale ?? 1,
4518
- isProduction: options.isProduction ?? true
4669
+ isProduction: options.isProduction ?? true,
4670
+ allowHtmlInCanvas: options.allowHtmlInCanvas ?? false
4519
4671
  }));
4520
4672
  return onlyOneRenderAtATimeQueue.ref;
4521
4673
  };
4522
4674
  // src/render-still-on-web.tsx
4523
4675
  import {
4524
- Internals as Internals9
4676
+ Internals as Internals10
4525
4677
  } from "remotion";
4678
+
4679
+ // src/render-still-screenshot-task.ts
4680
+ var mimeTypeForFormat = (format) => {
4681
+ if (format === "jpeg") {
4682
+ return "image/jpeg";
4683
+ }
4684
+ if (format === "webp") {
4685
+ return "image/webp";
4686
+ }
4687
+ return "image/png";
4688
+ };
4689
+ var encodeCanvasToBlob = async (canvas, options) => {
4690
+ const format = options?.format ?? "png";
4691
+ const type = mimeTypeForFormat(format);
4692
+ if (format === "png") {
4693
+ return canvas.convertToBlob({ type });
4694
+ }
4695
+ return canvas.convertToBlob({
4696
+ type,
4697
+ quality: options?.quality
4698
+ });
4699
+ };
4700
+ var createRenderStillOnWebResult = ({
4701
+ canvas,
4702
+ internalState
4703
+ }) => {
4704
+ return {
4705
+ internalState,
4706
+ canvas: () => Promise.resolve(canvas),
4707
+ blob: (options) => encodeCanvasToBlob(canvas, options),
4708
+ url: async (options) => {
4709
+ const blob = await encodeCanvasToBlob(canvas, options);
4710
+ return URL.createObjectURL(blob);
4711
+ }
4712
+ };
4713
+ };
4714
+
4715
+ // src/render-still-on-web.tsx
4526
4716
  async function internalRenderStillOnWeb({
4527
4717
  frame,
4528
4718
  delayRenderTimeoutInMilliseconds,
4529
4719
  logLevel,
4530
4720
  inputProps,
4531
4721
  schema,
4532
- imageFormat,
4533
4722
  mediaCacheSizeInBytes,
4534
4723
  composition,
4535
4724
  signal,
4536
4725
  onArtifact,
4537
4726
  licenseKey,
4538
4727
  scale,
4539
- isProduction
4728
+ isProduction,
4729
+ allowHtmlInCanvas
4540
4730
  }) {
4541
4731
  let __stack = [];
4542
4732
  try {
4543
4733
  validateScale(scale);
4544
- const resolved = await Internals9.resolveVideoConfig({
4734
+ const onHtmlInCanvasLayerOutcome = (outcome) => {
4735
+ if (outcome.native) {
4736
+ 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");
4737
+ } else if (outcome.shouldWarn) {
4738
+ Internals10.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, `Not using HTML-in-canvas: ${outcome.reason}`);
4739
+ }
4740
+ };
4741
+ const resolved = await Internals10.resolveVideoConfig({
4545
4742
  calculateMetadata: composition.calculateMetadata ?? null,
4546
4743
  signal: signal ?? new AbortController().signal,
4547
4744
  defaultProps: composition.defaultProps ?? {},
@@ -4572,9 +4769,37 @@ async function internalRenderStillOnWeb({
4572
4769
  schema: schema ?? null,
4573
4770
  initialFrame: frame,
4574
4771
  defaultCodec: resolved.defaultCodec,
4575
- defaultOutName: resolved.defaultOutName
4772
+ defaultOutName: resolved.defaultOutName,
4773
+ allowHtmlInCanvas
4576
4774
  }), 0);
4577
- const { delayRenderScope, div, collectAssets, errorHolder } = scaffold;
4775
+ const {
4776
+ delayRenderScope,
4777
+ div,
4778
+ collectAssets,
4779
+ errorHolder,
4780
+ htmlInCanvasContext
4781
+ } = scaffold;
4782
+ if (allowHtmlInCanvas && !htmlInCanvasContext) {
4783
+ if (!supportsNativeHtmlInCanvas()) {
4784
+ onHtmlInCanvasLayerOutcome({
4785
+ native: false,
4786
+ 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.",
4787
+ shouldWarn: false
4788
+ });
4789
+ } else {
4790
+ onHtmlInCanvasLayerOutcome({
4791
+ native: false,
4792
+ reason: "drawElementImage is available but canvas.requestPaint() is missing. Use a Chromium version that ships requestPaint.",
4793
+ shouldWarn: true
4794
+ });
4795
+ }
4796
+ } else if (!allowHtmlInCanvas) {
4797
+ onHtmlInCanvasLayerOutcome({
4798
+ native: false,
4799
+ reason: "allowHtmlInCanvas is false; using the built-in DOM composer.",
4800
+ shouldWarn: false
4801
+ });
4802
+ }
4578
4803
  const artifactsHandler = handleArtifacts();
4579
4804
  try {
4580
4805
  if (signal?.aborted) {
@@ -4598,14 +4823,19 @@ async function internalRenderStillOnWeb({
4598
4823
  logLevel,
4599
4824
  internalState,
4600
4825
  onlyBackgroundClipText: false,
4601
- cutout: new DOMRect(0, 0, resolved.width, resolved.height)
4602
- });
4603
- const imageData = await capturedFrame.canvas.convertToBlob({
4604
- type: `image/${imageFormat}`
4826
+ cutout: new DOMRect(0, 0, resolved.width, resolved.height),
4827
+ htmlInCanvasContext,
4828
+ onHtmlInCanvasLayerOutcome: htmlInCanvasContext ? onHtmlInCanvasLayerOutcome : undefined
4605
4829
  });
4830
+ const { canvas } = capturedFrame;
4606
4831
  const assets = collectAssets.current.collectAssets();
4607
4832
  if (onArtifact) {
4608
- await artifactsHandler.handle({ imageData, frame, assets, onArtifact });
4833
+ await artifactsHandler.handle({
4834
+ imageData: canvas,
4835
+ frame,
4836
+ assets,
4837
+ onArtifact
4838
+ });
4609
4839
  }
4610
4840
  sendUsageEvent({
4611
4841
  licenseKey: licenseKey ?? null,
@@ -4614,7 +4844,7 @@ async function internalRenderStillOnWeb({
4614
4844
  isStill: true,
4615
4845
  isProduction
4616
4846
  });
4617
- return { blob: imageData, internalState };
4847
+ return createRenderStillOnWebResult({ canvas, internalState });
4618
4848
  } catch (err) {
4619
4849
  if (!signal?.aborted) {
4620
4850
  sendUsageEvent({
@@ -4624,7 +4854,7 @@ async function internalRenderStillOnWeb({
4624
4854
  isStill: true,
4625
4855
  isProduction
4626
4856
  }).catch((err2) => {
4627
- Internals9.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
4857
+ Internals10.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
4628
4858
  });
4629
4859
  }
4630
4860
  throw err;
@@ -4646,7 +4876,8 @@ var renderStillOnWeb = (options) => {
4646
4876
  onArtifact: options.onArtifact ?? null,
4647
4877
  licenseKey: options.licenseKey ?? null,
4648
4878
  scale: options.scale ?? 1,
4649
- isProduction: options.isProduction ?? true
4879
+ isProduction: options.isProduction ?? true,
4880
+ allowHtmlInCanvas: options.allowHtmlInCanvas ?? false
4650
4881
  }));
4651
4882
  return onlyOneRenderAtATimeQueue.ref;
4652
4883
  };
@@ -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,7 @@ type OptionalRenderMediaOnWebOptions<Schema extends $ZodObject> = {
65
65
  isProduction: boolean;
66
66
  muted: boolean;
67
67
  scale: number;
68
+ allowHtmlInCanvas: boolean;
68
69
  };
69
70
  export type RenderMediaOnWebOptions<Schema extends $ZodObject, Props extends Record<string, unknown>> = MandatoryRenderMediaOnWebOptions<Schema, Props> & Partial<OptionalRenderMediaOnWebOptions<Schema>> & InputPropsIfHasProps<Schema, Props>;
70
71
  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.445",
6
+ "version": "4.0.447",
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.445",
26
- "remotion": "4.0.445",
25
+ "@remotion/licensing": "4.0.447",
26
+ "remotion": "4.0.447",
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.445",
32
- "@remotion/player": "4.0.445",
33
- "@remotion/media": "4.0.445",
34
- "@remotion/three": "4.0.445",
31
+ "@remotion/eslint-config-internal": "4.0.447",
32
+ "@remotion/player": "4.0.447",
33
+ "@remotion/media": "4.0.447",
34
+ "@remotion/three": "4.0.447",
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",