Files
story-studio/remotion/node_modules/@remotion/web-renderer/dist/esm/index.mjs
2026-02-21 10:33:18 +01:00

4164 lines
131 KiB
JavaScript

var __dispose = Symbol.dispose || /* @__PURE__ */ Symbol.for("Symbol.dispose");
var __asyncDispose = Symbol.asyncDispose || /* @__PURE__ */ Symbol.for("Symbol.asyncDispose");
var __using = (stack, value, async) => {
if (value != null) {
if (typeof value !== "object" && typeof value !== "function")
throw TypeError('Object expected to be assigned to "using" declaration');
var dispose;
if (async)
dispose = value[__asyncDispose];
if (dispose === undefined)
dispose = value[__dispose];
if (typeof dispose !== "function")
throw TypeError("Object not disposable");
stack.push([async, dispose, value]);
} else if (async) {
stack.push([async]);
}
return value;
};
var __callDispose = (stack, error, hasError) => {
var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
}, fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e), next = (it) => {
while (it = stack.pop()) {
try {
var result = it[1] && it[1].call(it[2]);
if (it[0])
return Promise.resolve(result).then(next, (e) => (fail(e), next()));
} catch (e) {
fail(e);
}
}
if (hasError)
throw error;
};
return next();
};
// src/can-render-media-on-web.ts
import { canEncodeVideo } from "mediabunny";
// src/can-use-webfs-target.ts
var canUseWebFsWriter = async () => {
if (!("storage" in navigator)) {
return false;
}
if (!("getDirectory" in navigator.storage)) {
return false;
}
try {
const directoryHandle = await navigator.storage.getDirectory();
const fileHandle = await directoryHandle.getFileHandle("remotion-probe-web-fs-support", {
create: true
});
const canUse = fileHandle.createWritable !== undefined;
return canUse;
} catch {
return false;
}
};
// src/check-webgl-support.ts
var checkWebGLSupport = () => {
try {
const canvas = new OffscreenCanvas(1, 1);
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
if (!gl) {
return {
type: "webgl-unsupported",
message: "WebGL is not supported. 3D CSS transforms will fail.",
severity: "error"
};
}
return null;
} catch {
return {
type: "webgl-unsupported",
message: "WebGL is not supported. 3D CSS transforms will fail.",
severity: "error"
};
}
};
// src/mediabunny-mappings.ts
import {
Mp4OutputFormat,
QUALITY_HIGH,
QUALITY_LOW,
QUALITY_MEDIUM,
QUALITY_VERY_HIGH,
QUALITY_VERY_LOW,
WebMOutputFormat
} from "mediabunny";
var codecToMediabunnyCodec = (codec) => {
switch (codec) {
case "h264":
return "avc";
case "h265":
return "hevc";
case "vp8":
return "vp8";
case "vp9":
return "vp9";
case "av1":
return "av1";
default:
throw new Error(`Unsupported codec: ${codec}`);
}
};
var containerToMediabunnyContainer = (container) => {
switch (container) {
case "mp4":
return new Mp4OutputFormat;
case "webm":
return new WebMOutputFormat;
default:
throw new Error(`Unsupported container: ${container}`);
}
};
var getDefaultVideoCodecForContainer = (container) => {
switch (container) {
case "mp4":
return "h264";
case "webm":
return "vp8";
default:
throw new Error(`Unsupported container: ${container}`);
}
};
var getQualityForWebRendererQuality = (quality) => {
switch (quality) {
case "very-low":
return QUALITY_VERY_LOW;
case "low":
return QUALITY_LOW;
case "medium":
return QUALITY_MEDIUM;
case "high":
return QUALITY_HIGH;
case "very-high":
return QUALITY_VERY_HIGH;
default:
throw new Error(`Unsupported quality: ${quality}`);
}
};
var getMimeType = (container) => {
switch (container) {
case "mp4":
return "video/mp4";
case "webm":
return "video/webm";
default:
throw new Error(`Unsupported container: ${container}`);
}
};
var getDefaultAudioCodecForContainer = (container) => {
switch (container) {
case "mp4":
return "aac";
case "webm":
return "opus";
default:
throw new Error(`Unsupported container: ${container}`);
}
};
var WEB_RENDERER_VIDEO_CODECS = [
"h264",
"h265",
"vp8",
"vp9",
"av1"
];
var getSupportedVideoCodecsForContainer = (container) => {
const format = containerToMediabunnyContainer(container);
const allSupported = format.getSupportedVideoCodecs();
return WEB_RENDERER_VIDEO_CODECS.filter((codec) => allSupported.includes(codecToMediabunnyCodec(codec)));
};
var WEB_RENDERER_AUDIO_CODECS = ["aac", "opus"];
var getSupportedAudioCodecsForContainer = (container) => {
const format = containerToMediabunnyContainer(container);
const allSupported = format.getSupportedAudioCodecs();
return WEB_RENDERER_AUDIO_CODECS.filter((codec) => allSupported.includes(codec));
};
var audioCodecToMediabunnyAudioCodec = (audioCodec) => {
return audioCodec;
};
// src/resolve-audio-codec.ts
import { canEncodeAudio } from "mediabunny";
var resolveAudioCodec = async (options) => {
const issues = [];
const { container, requestedCodec, userSpecifiedAudioCodec, bitrate } = options;
const audioCodec = requestedCodec ?? getDefaultAudioCodecForContainer(container);
const supportedAudioCodecs = getSupportedAudioCodecsForContainer(container);
if (!supportedAudioCodecs.includes(audioCodec)) {
issues.push({
type: "audio-codec-unsupported",
message: `Audio codec "${audioCodec}" is not supported for container "${container}". Supported: ${supportedAudioCodecs.join(", ")}`,
severity: "error"
});
return { codec: null, issues };
}
const mediabunnyAudioCodec = audioCodecToMediabunnyAudioCodec(audioCodec);
const canEncode = await canEncodeAudio(mediabunnyAudioCodec, { bitrate });
if (canEncode) {
return { codec: audioCodec, issues };
}
if (userSpecifiedAudioCodec) {
issues.push({
type: "audio-codec-unsupported",
message: `Audio codec "${audioCodec}" cannot be encoded by this browser. This is common for AAC on Firefox. Try using "opus" instead.`,
severity: "error"
});
return { codec: null, issues };
}
for (const fallbackCodec of supportedAudioCodecs) {
if (fallbackCodec !== audioCodec) {
const fallbackMediabunnyCodec = audioCodecToMediabunnyAudioCodec(fallbackCodec);
const canEncodeFallback = await canEncodeAudio(fallbackMediabunnyCodec, {
bitrate
});
if (canEncodeFallback) {
issues.push({
type: "audio-codec-unsupported",
message: `Falling back from audio codec "${audioCodec}" to "${fallbackCodec}" because the original codec cannot be encoded by this browser.`,
severity: "warning"
});
return { codec: fallbackCodec, issues };
}
}
}
issues.push({
type: "audio-codec-unsupported",
message: `No audio codec can be encoded by this browser for container "${container}".`,
severity: "error"
});
return { codec: null, issues };
};
// src/validate-dimensions.ts
var validateDimensions = (options) => {
const { width, height, codec } = options;
if (codec === "h264" || codec === "h265") {
if (width % 2 !== 0 || height % 2 !== 0) {
return {
type: "invalid-dimensions",
message: `${codec.toUpperCase()} codec requires width and height to be multiples of 2. Got ${width}x${height}`,
severity: "error"
};
}
}
return null;
};
// src/can-render-media-on-web.ts
var canRenderMediaOnWeb = async (options) => {
const issues = [];
if (typeof VideoEncoder === "undefined") {
issues.push({
type: "webcodecs-unavailable",
message: "WebCodecs API is not available in this browser. A modern browser with WebCodecs support is required.",
severity: "error"
});
}
const container = options.container ?? "mp4";
const videoCodec = options.videoCodec ?? getDefaultVideoCodecForContainer(container);
const transparent = options.transparent ?? false;
const muted = options.muted ?? false;
const { width, height } = options;
const resolvedVideoBitrate = typeof options.videoBitrate === "number" ? options.videoBitrate : getQualityForWebRendererQuality(options.videoBitrate ?? "medium");
const resolvedAudioBitrate = typeof options.audioBitrate === "number" ? options.audioBitrate : getQualityForWebRendererQuality(options.audioBitrate ?? "medium");
const format = containerToMediabunnyContainer(container);
if (!format.getSupportedCodecs().includes(codecToMediabunnyCodec(videoCodec))) {
issues.push({
type: "container-codec-mismatch",
message: `Codec ${videoCodec} is not supported for container ${container}`,
severity: "error"
});
}
const dimensionIssue = validateDimensions({ width, height, codec: videoCodec });
if (dimensionIssue) {
issues.push(dimensionIssue);
}
const canEncodeVideoResult = await canEncodeVideo(codecToMediabunnyCodec(videoCodec), { bitrate: resolvedVideoBitrate });
if (!canEncodeVideoResult) {
issues.push({
type: "video-codec-unsupported",
message: `Video codec "${videoCodec}" cannot be encoded by this browser`,
severity: "error"
});
}
if (transparent && !["vp8", "vp9"].includes(videoCodec)) {
issues.push({
type: "transparent-video-unsupported",
message: `Transparent video requires VP8 or VP9 codec with WebM container. ${videoCodec} does not support alpha channel.`,
severity: "error"
});
}
let resolvedAudioCodec = null;
if (!muted) {
const audioResult = await resolveAudioCodec({
container,
requestedCodec: options.audioCodec,
userSpecifiedAudioCodec: options.audioCodec !== undefined && options.audioCodec !== null,
bitrate: resolvedAudioBitrate
});
resolvedAudioCodec = audioResult.codec;
issues.push(...audioResult.issues);
}
const webglIssue = checkWebGLSupport();
if (webglIssue) {
issues.push(webglIssue);
}
const canUseWebFs = await canUseWebFsWriter();
let resolvedOutputTarget;
if (options.outputTarget === "web-fs") {
if (!canUseWebFs) {
issues.push({
type: "output-target-unsupported",
message: 'The "web-fs" output target is not supported in this browser. The File System Access API is required.',
severity: "error"
});
}
resolvedOutputTarget = "web-fs";
} else if (options.outputTarget === "arraybuffer") {
resolvedOutputTarget = "arraybuffer";
} else {
resolvedOutputTarget = canUseWebFs ? "web-fs" : "arraybuffer";
}
return {
canRender: issues.filter((i) => i.severity === "error").length === 0,
issues,
resolvedVideoCodec: videoCodec,
resolvedAudioCodec,
resolvedOutputTarget
};
};
// src/get-encodable-codecs.ts
import {
getEncodableAudioCodecs as mediabunnyGetEncodableAudioCodecs,
getEncodableVideoCodecs as mediabunnyGetEncodableVideoCodecs
} from "mediabunny";
var getEncodableVideoCodecs = async (container, options) => {
const supported = getSupportedVideoCodecsForContainer(container);
const mediabunnyCodecs = supported.map(codecToMediabunnyCodec);
const resolvedBitrate = options?.videoBitrate ? typeof options.videoBitrate === "number" ? options.videoBitrate : getQualityForWebRendererQuality(options.videoBitrate) : undefined;
const encodable = await mediabunnyGetEncodableVideoCodecs(mediabunnyCodecs, {
bitrate: resolvedBitrate
});
return supported.filter((c) => encodable.includes(codecToMediabunnyCodec(c)));
};
var getEncodableAudioCodecs = async (container, options) => {
const supported = getSupportedAudioCodecsForContainer(container);
const resolvedBitrate = options?.audioBitrate ? typeof options.audioBitrate === "number" ? options.audioBitrate : getQualityForWebRendererQuality(options.audioBitrate) : undefined;
const encodable = await mediabunnyGetEncodableAudioCodecs(supported, {
bitrate: resolvedBitrate
});
return supported.filter((c) => encodable.includes(c));
};
// src/render-media-on-web.tsx
import { BufferTarget, StreamTarget } from "mediabunny";
import { Internals as Internals8 } from "remotion";
// src/add-sample.ts
import { AudioSample, VideoSample } from "mediabunny";
var addVideoSampleAndCloseFrame = async (frameToEncode, videoSampleSource) => {
const sample = new VideoSample(frameToEncode);
try {
await videoSampleSource.add(sample);
} finally {
sample.close();
frameToEncode.close();
}
};
var addAudioSample = async (audio, audioSampleSource) => {
const sample = new AudioSample(audio);
try {
await audioSampleSource.add(sample);
} finally {
sample.close();
}
};
// src/artifact.ts
import { NoReactInternals } from "remotion/no-react";
var onlyArtifact = async ({
assets,
frameBuffer
}) => {
const artifacts = assets.filter((asset) => asset.type === "artifact");
let frameBufferUint8 = null;
const result = [];
for (const artifact of artifacts) {
if (artifact.contentType === "binary" || artifact.contentType === "text") {
result.push({
frame: artifact.frame,
content: artifact.content,
filename: artifact.filename,
downloadBehavior: artifact.downloadBehavior
});
continue;
}
if (artifact.contentType === "thumbnail") {
if (frameBuffer === null) {
continue;
}
const ab = frameBuffer instanceof Blob ? await frameBuffer.arrayBuffer() : new Uint8Array(await (await frameBuffer.convertToBlob({ type: "image/png" })).arrayBuffer());
frameBufferUint8 = new Uint8Array(ab);
result.push({
frame: artifact.frame,
content: frameBufferUint8,
filename: artifact.filename,
downloadBehavior: artifact.downloadBehavior
});
continue;
}
throw new Error("Unknown artifact type: " + artifact);
}
return result.filter(NoReactInternals.truthy);
};
var handleArtifacts = () => {
const previousArtifacts = [];
const handle = async ({
imageData,
frame,
assets: artifactAssets,
onArtifact
}) => {
const artifacts = await onlyArtifact({
assets: artifactAssets,
frameBuffer: imageData
});
for (const artifact of artifacts) {
const previousArtifact = previousArtifacts.find((a) => a.filename === artifact.filename);
if (previousArtifact) {
throw new Error(`An artifact with output "${artifact.filename}" was already registered at frame ${previousArtifact.frame}, but now registered again at frame ${frame}. Artifacts must have unique names. https://remotion.dev/docs/artifacts`);
}
onArtifact(artifact);
previousArtifacts.push({ frame, filename: artifact.filename });
}
};
return { handle };
};
// src/audio.ts
var TARGET_NUMBER_OF_CHANNELS = 2;
var TARGET_SAMPLE_RATE = 48000;
function mixAudio(waves, length) {
if (waves.length === 1 && waves[0].length === length) {
return waves[0];
}
const mixed = new Int16Array(length);
if (waves.length === 1) {
mixed.set(waves[0].subarray(0, length));
return mixed;
}
for (let i = 0;i < length; i++) {
const sum = waves.reduce((acc, wave) => {
return acc + (wave[i] ?? 0);
}, 0);
mixed[i] = Math.max(-32768, Math.min(32767, sum));
}
return mixed;
}
var onlyInlineAudio = ({
assets,
fps,
timestamp
}) => {
const inlineAudio = assets.filter((asset) => asset.type === "inline-audio");
if (inlineAudio.length === 0) {
return null;
}
const expectedLength = Math.round(TARGET_NUMBER_OF_CHANNELS * TARGET_SAMPLE_RATE / fps);
for (const asset of inlineAudio) {
if (asset.toneFrequency !== 1) {
throw new Error("Setting the toneFrequency is not supported yet in web rendering.");
}
}
const mixedAudio = mixAudio(inlineAudio.map((asset) => asset.audio), expectedLength);
return new AudioData({
data: mixedAudio,
format: "s16",
numberOfChannels: TARGET_NUMBER_OF_CHANNELS,
numberOfFrames: expectedLength / TARGET_NUMBER_OF_CHANNELS,
sampleRate: TARGET_SAMPLE_RATE,
timestamp
});
};
// src/background-keepalive.ts
import { Internals } from "remotion";
var WORKER_CODE = `
let intervalId = null;
self.onmessage = (e) => {
if (e.data.type === 'start') {
if (intervalId !== null) {
clearInterval(intervalId);
}
intervalId = setInterval(() => self.postMessage('tick'), e.data.intervalMs);
} else if (e.data.type === 'stop') {
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
}
};
`;
function createBackgroundKeepalive({
fps,
logLevel
}) {
const intervalMs = Math.round(1000 / fps);
let pendingResolvers = [];
let worker = null;
let disposed = false;
if (typeof Worker === "undefined") {
Internals.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, "Web Workers not available. Rendering may pause when tab is backgrounded.");
return {
waitForTick: () => {
return new Promise((resolve) => {
setTimeout(resolve, intervalMs);
});
},
[Symbol.dispose]: () => {}
};
}
const blob = new Blob([WORKER_CODE], { type: "application/javascript" });
const workerUrl = URL.createObjectURL(blob);
worker = new Worker(workerUrl);
worker.onmessage = () => {
const resolvers = pendingResolvers;
pendingResolvers = [];
for (const resolve of resolvers) {
resolve();
}
};
worker.onerror = (event) => {
Internals.Log.error({ logLevel, tag: "@remotion/web-renderer" }, "Background keepalive worker encountered an error and will be terminated.", event);
const resolvers = pendingResolvers;
pendingResolvers = [];
for (const resolve of resolvers) {
resolve();
}
if (!disposed) {
disposed = true;
worker?.terminate();
worker = null;
URL.revokeObjectURL(workerUrl);
}
};
worker.postMessage({ type: "start", intervalMs });
return {
waitForTick: () => {
return new Promise((resolve) => {
pendingResolvers.push(resolve);
});
},
[Symbol.dispose]: () => {
if (disposed) {
return;
}
disposed = true;
worker?.postMessage({ type: "stop" });
worker?.terminate();
worker = null;
URL.revokeObjectURL(workerUrl);
const resolvers = pendingResolvers;
pendingResolvers = [];
for (const resolve of resolvers) {
resolve();
}
}
};
}
// src/create-audio-sample-source.ts
import { AudioSampleSource } from "mediabunny";
var createAudioSampleSource = ({
muted,
codec,
bitrate
}) => {
if (muted || codec === null) {
return null;
}
const audioSampleSource = new AudioSampleSource({
codec,
bitrate
});
return { audioSampleSource, [Symbol.dispose]: () => audioSampleSource.close() };
};
// src/create-scaffold.tsx
import { createRef } from "react";
import { flushSync as flushSync2 } from "react-dom";
import ReactDOM from "react-dom/client";
import { Internals as Internals3 } from "remotion";
// src/update-time.tsx
import { useImperativeHandle, useState } from "react";
import { flushSync } from "react-dom";
import { Internals as Internals2 } from "remotion";
import { jsx } from "react/jsx-runtime";
var UpdateTime = ({
children,
audioEnabled,
videoEnabled,
logLevel,
compId,
initialFrame,
timeUpdater
}) => {
const [frame, setFrame] = useState(initialFrame);
useImperativeHandle(timeUpdater, () => ({
update: (f) => {
flushSync(() => {
setFrame(f);
});
}
}));
return /* @__PURE__ */ jsx(Internals2.RemotionRootContexts, {
audioEnabled,
videoEnabled,
logLevel,
numberOfAudioTags: 0,
audioLatencyHint: "interactive",
frameState: {
[compId]: frame
},
nonceContextSeed: 0,
children
});
};
// src/create-scaffold.tsx
import { jsx as jsx2 } from "react/jsx-runtime";
function checkForError(errorHolder) {
if (errorHolder.error) {
throw errorHolder.error;
}
}
function createScaffold({
width,
height,
delayRenderTimeoutInMilliseconds,
logLevel,
resolvedProps,
id,
mediaCacheSizeInBytes,
durationInFrames,
fps,
initialFrame,
schema,
Component,
audioEnabled,
videoEnabled,
defaultCodec,
defaultOutName
}) {
if (!ReactDOM.createRoot) {
throw new Error("@remotion/web-renderer requires React 18 or higher");
}
const div = document.createElement("div");
div.style.position = "fixed";
div.style.display = "flex";
div.style.flexDirection = "column";
div.style.backgroundColor = "transparent";
div.style.width = `${width}px`;
div.style.height = `${height}px`;
div.style.zIndex = "-9999";
div.style.top = "0";
div.style.left = "0";
div.style.right = "0";
div.style.bottom = "0";
div.style.visibility = "hidden";
div.style.pointerEvents = "none";
const scaffoldClassName = `remotion-scaffold-${Math.random().toString(36).substring(2, 15)}`;
div.className = scaffoldClassName;
const cleanupCSS = Internals3.CSSUtils.injectCSS(Internals3.CSSUtils.makeDefaultPreviewCSS(`.${scaffoldClassName}`, "white"));
document.body.appendChild(div);
const errorHolder = { error: null };
const root = ReactDOM.createRoot(div, {
onUncaughtError: (err) => {
errorHolder.error = err instanceof Error ? err : new Error(String(err));
}
});
const delayRenderScope = {
remotion_renderReady: true,
remotion_delayRenderTimeouts: {},
remotion_puppeteerTimeout: delayRenderTimeoutInMilliseconds,
remotion_attempt: 0,
remotion_delayRenderHandles: []
};
const timeUpdater = createRef();
const collectAssets = createRef();
flushSync2(() => {
root.render(/* @__PURE__ */ jsx2(Internals3.MaxMediaCacheSizeContext.Provider, {
value: mediaCacheSizeInBytes,
children: /* @__PURE__ */ jsx2(Internals3.RemotionEnvironmentContext.Provider, {
value: {
isStudio: false,
isRendering: true,
isPlayer: false,
isReadOnlyStudio: false,
isClientSideRendering: true
},
children: /* @__PURE__ */ jsx2(Internals3.DelayRenderContextType.Provider, {
value: delayRenderScope,
children: /* @__PURE__ */ jsx2(Internals3.CompositionManager.Provider, {
value: {
compositions: [
{
id,
component: Component,
nonce: 0,
defaultProps: {},
folderName: null,
parentFolderName: null,
schema: schema ?? null,
calculateMetadata: null,
durationInFrames,
fps,
height,
width
}
],
canvasContent: {
type: "composition",
compositionId: id
},
currentCompositionMetadata: {
props: resolvedProps,
durationInFrames,
fps,
height,
width,
defaultCodec: defaultCodec ?? null,
defaultOutName: defaultOutName ?? null,
defaultVideoImageFormat: null,
defaultPixelFormat: null,
defaultProResProfile: null
},
folders: []
},
children: /* @__PURE__ */ jsx2(Internals3.RenderAssetManagerProvider, {
collectAssets,
children: /* @__PURE__ */ jsx2(UpdateTime, {
audioEnabled,
videoEnabled,
logLevel,
compId: id,
initialFrame,
timeUpdater,
children: /* @__PURE__ */ jsx2(Internals3.CanUseRemotionHooks.Provider, {
value: true,
children: /* @__PURE__ */ jsx2(Component, {
...resolvedProps
})
})
})
})
})
})
})
}));
});
return {
delayRenderScope,
div,
errorHolder,
[Symbol.dispose]: () => {
root.unmount();
div.remove();
cleanupCSS();
},
timeUpdater,
collectAssets
};
}
// src/frame-range.ts
var getRealFrameRange = (durationInFrames, frameRange) => {
if (frameRange === null) {
return [0, durationInFrames - 1];
}
if (typeof frameRange === "number") {
if (frameRange < 0 || frameRange >= durationInFrames) {
throw new Error(`Frame number is out of range, must be between 0 and ${durationInFrames - 1} but got ${frameRange}`);
}
return [frameRange, frameRange];
}
if (frameRange[1] >= durationInFrames || frameRange[0] < 0) {
throw new Error(`The "durationInFrames" of the composition was evaluated to be ${durationInFrames}, but frame range ${frameRange.join("-")} is not inbetween 0-${durationInFrames - 1}`);
}
return frameRange;
};
// src/internal-state.ts
var makeInternalState = () => {
let drawnPrecomposedPixels = 0;
let precomposedTextures = 0;
let waitForReadyTime = 0;
let addSampleTime = 0;
let createFrameTime = 0;
let audioMixingTime = 0;
const helperCanvasState = {
current: null
};
return {
getDrawn3dPixels: () => drawnPrecomposedPixels,
getPrecomposedTiles: () => precomposedTextures,
addPrecompose: ({
canvasWidth,
canvasHeight
}) => {
drawnPrecomposedPixels += canvasWidth * canvasHeight;
precomposedTextures++;
},
helperCanvasState,
[Symbol.dispose]: () => {
if (helperCanvasState.current) {
helperCanvasState.current.cleanup();
}
},
getWaitForReadyTime: () => waitForReadyTime,
addWaitForReadyTime: (time) => {
waitForReadyTime += time;
},
getAddSampleTime: () => addSampleTime,
addAddSampleTime: (time) => {
addSampleTime += time;
},
getCreateFrameTime: () => createFrameTime,
addCreateFrameTime: (time) => {
createFrameTime += time;
},
getAudioMixingTime: () => audioMixingTime,
addAudioMixingTime: (time) => {
audioMixingTime += time;
}
};
};
// src/mediabunny-cleanups.ts
import { Output, VideoSampleSource } from "mediabunny";
var makeOutputWithCleanup = (options) => {
const output = new Output(options);
return {
output,
[Symbol.dispose]: () => {
if (output.state === "finalized" || output.state === "canceled") {
return;
}
output.cancel();
}
};
};
var makeVideoSampleSourceCleanup = (encodingConfig) => {
const videoSampleSource = new VideoSampleSource(encodingConfig);
return {
videoSampleSource,
[Symbol.dispose]: () => {
videoSampleSource.close();
}
};
};
// src/render-operations-queue.ts
var onlyOneRenderAtATimeQueue = {
ref: Promise.resolve()
};
// ../licensing/dist/esm/index.mjs
function isNetworkError(error) {
if (error.message.includes("Failed to fetch") || error.message.includes("Load failed") || error.message.includes("NetworkError when attempting to fetch resource")) {
return true;
}
return false;
}
var HOST = "https://www.remotion.pro";
var DEFAULT_MAX_RETRIES = 3;
var exponentialBackoffMs = (attempt) => {
return 1000 * 2 ** (attempt - 1);
};
var sleep = (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
var internalRegisterUsageEvent = async ({
host,
succeeded,
event,
isStill,
isProduction,
licenseKey
}) => {
let lastError;
const totalAttempts = DEFAULT_MAX_RETRIES + 1;
for (let attempt = 1;attempt <= totalAttempts; attempt++) {
const abortController = new AbortController;
const timeout = setTimeout(() => {
abortController.abort();
}, 1e4);
try {
const res = await fetch(`${HOST}/api/track/register-usage-point`, {
method: "POST",
body: JSON.stringify({
event,
apiKey: licenseKey,
host,
succeeded,
isStill,
isProduction
}),
headers: {
"Content-Type": "application/json"
},
signal: abortController.signal
});
clearTimeout(timeout);
const json = await res.json();
if (json.success) {
return {
billable: json.billable,
classification: json.classification
};
}
if (!res.ok) {
throw new Error(json.error);
}
throw new Error(`Unexpected response from server: ${JSON.stringify(json)}`);
} catch (err) {
clearTimeout(timeout);
const error = err;
const isTimeout = error.name === "AbortError";
const isRetryable = isNetworkError(error) || isTimeout;
if (!isRetryable) {
throw err;
}
lastError = isTimeout ? new Error("Request timed out after 10 seconds") : error;
if (attempt < totalAttempts) {
const backoffMs = exponentialBackoffMs(attempt);
console.log(`Failed to send usage event (attempt ${attempt}/${totalAttempts}), retrying in ${backoffMs}ms...`, err);
await sleep(backoffMs);
}
}
}
throw lastError;
};
var LicensingInternals = {
internalRegisterUsageEvent
};
// src/send-telemetry-event.ts
import { Internals as Internals4 } from "remotion";
var sendUsageEvent = async ({
licenseKey,
succeeded,
apiName,
isStill,
isProduction
}) => {
const host = typeof window === "undefined" ? null : typeof window.location === "undefined" ? null : window.location.origin ?? null;
if (host === null) {
return;
}
if (licenseKey === null) {
Internals4.Log.warn({ logLevel: "warn", tag: "web-renderer" }, `Pass "licenseKey" to ${apiName}(). If you qualify for the Free License (https://remotion.dev/license), pass "free-license" instead.`);
}
await LicensingInternals.internalRegisterUsageEvent({
licenseKey: licenseKey === "free-license" ? null : licenseKey,
event: "webcodec-conversion",
host,
succeeded,
isStill,
isProduction
});
};
// src/tree-walker-cleanup-after-children.ts
var createTreeWalkerCleanupAfterChildren = (treeWalker) => {
const cleanupAfterChildren = [];
const checkCleanUpAtBeginningOfIteration = () => {
for (let i = 0;i < cleanupAfterChildren.length; ) {
const cleanup = cleanupAfterChildren[i];
if (!(cleanup.element === treeWalker.currentNode || cleanup.element.contains(treeWalker.currentNode))) {
cleanup.cleanupFn();
cleanupAfterChildren.splice(i, 1);
} else {
i++;
}
}
};
const addCleanup = (element, cleanupFn) => {
cleanupAfterChildren.unshift({
element,
cleanupFn
});
};
const cleanupInTheEndOfTheIteration = () => {
for (const cleanup of cleanupAfterChildren) {
cleanup.cleanupFn();
}
};
return {
checkCleanUpAtBeginningOfIteration,
addCleanup,
[Symbol.dispose]: cleanupInTheEndOfTheIteration
};
};
// src/drawing/calculate-object-fit.ts
var calculateFill = ({
containerSize,
intrinsicSize
}) => {
return {
sourceX: 0,
sourceY: 0,
sourceWidth: intrinsicSize.width,
sourceHeight: intrinsicSize.height,
destX: containerSize.left,
destY: containerSize.top,
destWidth: containerSize.width,
destHeight: containerSize.height
};
};
var calculateContain = ({
containerSize,
intrinsicSize
}) => {
const containerAspect = containerSize.width / containerSize.height;
const imageAspect = intrinsicSize.width / intrinsicSize.height;
let destWidth;
let destHeight;
if (imageAspect > containerAspect) {
destWidth = containerSize.width;
destHeight = containerSize.width / imageAspect;
} else {
destHeight = containerSize.height;
destWidth = containerSize.height * imageAspect;
}
const destX = containerSize.left + (containerSize.width - destWidth) / 2;
const destY = containerSize.top + (containerSize.height - destHeight) / 2;
return {
sourceX: 0,
sourceY: 0,
sourceWidth: intrinsicSize.width,
sourceHeight: intrinsicSize.height,
destX,
destY,
destWidth,
destHeight
};
};
var calculateCover = ({
containerSize,
intrinsicSize
}) => {
if (containerSize.height <= 0 || intrinsicSize.height <= 0) {
return {
sourceX: 0,
sourceY: 0,
sourceWidth: 0,
sourceHeight: 0,
destX: containerSize.left,
destY: containerSize.top,
destWidth: 0,
destHeight: 0
};
}
const containerAspect = containerSize.width / containerSize.height;
const imageAspect = intrinsicSize.width / intrinsicSize.height;
let sourceX = 0;
let sourceY = 0;
let sourceWidth = intrinsicSize.width;
let sourceHeight = intrinsicSize.height;
if (imageAspect > containerAspect) {
sourceWidth = intrinsicSize.height * containerAspect;
sourceX = (intrinsicSize.width - sourceWidth) / 2;
} else {
sourceHeight = intrinsicSize.width / containerAspect;
sourceY = (intrinsicSize.height - sourceHeight) / 2;
}
return {
sourceX,
sourceY,
sourceWidth,
sourceHeight,
destX: containerSize.left,
destY: containerSize.top,
destWidth: containerSize.width,
destHeight: containerSize.height
};
};
var calculateNone = ({
containerSize,
intrinsicSize
}) => {
const centeredX = containerSize.left + (containerSize.width - intrinsicSize.width) / 2;
const centeredY = containerSize.top + (containerSize.height - intrinsicSize.height) / 2;
let sourceX = 0;
let sourceY = 0;
let sourceWidth = intrinsicSize.width;
let sourceHeight = intrinsicSize.height;
let destX = centeredX;
let destY = centeredY;
let destWidth = intrinsicSize.width;
let destHeight = intrinsicSize.height;
if (destX < containerSize.left) {
const clipAmount = containerSize.left - destX;
sourceX = clipAmount;
sourceWidth -= clipAmount;
destX = containerSize.left;
destWidth -= clipAmount;
}
if (destY < containerSize.top) {
const clipAmount = containerSize.top - destY;
sourceY = clipAmount;
sourceHeight -= clipAmount;
destY = containerSize.top;
destHeight -= clipAmount;
}
const containerRight = containerSize.left + containerSize.width;
if (destX + destWidth > containerRight) {
const clipAmount = destX + destWidth - containerRight;
sourceWidth -= clipAmount;
destWidth -= clipAmount;
}
const containerBottom = containerSize.top + containerSize.height;
if (destY + destHeight > containerBottom) {
const clipAmount = destY + destHeight - containerBottom;
sourceHeight -= clipAmount;
destHeight -= clipAmount;
}
return {
sourceX,
sourceY,
sourceWidth,
sourceHeight,
destX,
destY,
destWidth,
destHeight
};
};
var calculateObjectFit = ({
objectFit,
containerSize,
intrinsicSize
}) => {
switch (objectFit) {
case "fill":
return calculateFill({ containerSize, intrinsicSize });
case "contain":
return calculateContain({ containerSize, intrinsicSize });
case "cover":
return calculateCover({ containerSize, intrinsicSize });
case "none":
return calculateNone({ containerSize, intrinsicSize });
case "scale-down": {
const containResult = calculateContain({ containerSize, intrinsicSize });
const noneResult = calculateNone({ containerSize, intrinsicSize });
const containArea = containResult.destWidth * containResult.destHeight;
const noneArea = noneResult.destWidth * noneResult.destHeight;
return containArea < noneArea ? containResult : noneResult;
}
default: {
const exhaustiveCheck = objectFit;
throw new Error(`Unknown object-fit value: ${exhaustiveCheck}`);
}
}
};
var parseObjectFit = (value) => {
if (!value) {
return "fill";
}
const normalized = value.trim().toLowerCase();
switch (normalized) {
case "fill":
case "contain":
case "cover":
case "none":
case "scale-down":
return normalized;
default:
return "fill";
}
};
// src/drawing/fit-svg-into-its-dimensions.ts
var fitSvgIntoItsContainer = ({
containerSize,
elementSize
}) => {
if (Math.round(containerSize.width) === Math.round(elementSize.width) && Math.round(containerSize.height) === Math.round(elementSize.height)) {
return {
width: containerSize.width,
height: containerSize.height,
top: containerSize.top,
left: containerSize.left
};
}
if (containerSize.width <= 0 || containerSize.height <= 0) {
throw new Error(`Container must have positive dimensions, but got ${containerSize.width}x${containerSize.height}`);
}
if (elementSize.width <= 0 || elementSize.height <= 0) {
throw new Error(`Element must have positive dimensions, but got ${elementSize.width}x${elementSize.height}`);
}
const heightRatio = containerSize.height / elementSize.height;
const widthRatio = containerSize.width / elementSize.width;
const ratio = Math.min(heightRatio, widthRatio);
const newWidth = elementSize.width * ratio;
const newHeight = elementSize.height * ratio;
if (newWidth > containerSize.width + 0.000001 || newHeight > containerSize.height + 0.000001) {
throw new Error(`Element is too big to fit into the container. Max size: ${containerSize.width}x${containerSize.height}, element size: ${newWidth}x${newHeight}`);
}
return {
width: newWidth,
height: newHeight,
top: (containerSize.height - newHeight) / 2 + containerSize.top,
left: (containerSize.width - newWidth) / 2 + containerSize.left
};
};
// src/drawing/turn-svg-into-drawable.ts
var turnSvgIntoDrawable = (svg) => {
const { fill, color } = getComputedStyle(svg);
const originalTransform = svg.style.transform;
const originalTransformOrigin = svg.style.transformOrigin;
const originalMarginLeft = svg.style.marginLeft;
const originalMarginRight = svg.style.marginRight;
const originalMarginTop = svg.style.marginTop;
const originalMarginBottom = svg.style.marginBottom;
const originalFill = svg.style.fill;
const originalColor = svg.style.color;
svg.style.transform = "none";
svg.style.transformOrigin = "";
svg.style.marginLeft = "0";
svg.style.marginRight = "0";
svg.style.marginTop = "0";
svg.style.marginBottom = "0";
svg.style.fill = fill;
svg.style.color = color;
const svgData = new XMLSerializer().serializeToString(svg);
svg.style.marginLeft = originalMarginLeft;
svg.style.marginRight = originalMarginRight;
svg.style.marginTop = originalMarginTop;
svg.style.marginBottom = originalMarginBottom;
svg.style.transform = originalTransform;
svg.style.transformOrigin = originalTransformOrigin;
svg.style.fill = originalFill;
svg.style.color = originalColor;
return new Promise((resolve, reject) => {
const image = new Image;
const url = `data:image/svg+xml;base64,${btoa(svgData)}`;
image.onload = function() {
resolve(image);
};
image.onerror = () => {
reject(new Error("Failed to convert SVG to image"));
};
image.src = url;
});
};
// src/drawing/draw-dom-element.ts
var getReadableImageError = (err, node) => {
if (!(err instanceof DOMException)) {
return null;
}
if (err.name === "SecurityError") {
return new Error(`Could not draw image with src="${node.src}" to canvas: ` + `The image is tainted due to CORS restrictions. ` + `The server hosting this image must respond with the "Access-Control-Allow-Origin" header. ` + `See: https://remotion.dev/docs/client-side-rendering/migration`);
}
if (err.name === "InvalidStateError") {
return new Error(`Could not draw image with src="${node.src}" to canvas: ` + `The image is in a broken state. ` + `This usually means the image failed to load - check that the URL is valid and accessible.`);
}
return null;
};
var drawSvg = ({
drawable,
dimensions,
contextToDraw
}) => {
const fitted = fitSvgIntoItsContainer({
containerSize: dimensions,
elementSize: {
width: drawable.width,
height: drawable.height
}
});
contextToDraw.drawImage(drawable, fitted.left, fitted.top, fitted.width, fitted.height);
};
var drawReplacedElement = ({
drawable,
dimensions,
computedStyle,
contextToDraw
}) => {
const objectFit = parseObjectFit(computedStyle.objectFit);
const intrinsicSize = drawable instanceof HTMLImageElement ? { width: drawable.naturalWidth, height: drawable.naturalHeight } : { width: drawable.width, height: drawable.height };
const result = calculateObjectFit({
objectFit,
containerSize: {
width: dimensions.width,
height: dimensions.height,
left: dimensions.left,
top: dimensions.top
},
intrinsicSize
});
contextToDraw.drawImage(drawable, result.sourceX, result.sourceY, result.sourceWidth, result.sourceHeight, result.destX, result.destY, result.destWidth, result.destHeight);
};
var drawDomElement = (node) => {
const domDrawFn = async ({
dimensions,
contextToDraw,
computedStyle
}) => {
if (node instanceof SVGSVGElement) {
const drawable = await turnSvgIntoDrawable(node);
drawSvg({ drawable, dimensions, contextToDraw });
return;
}
if (node instanceof HTMLImageElement || node instanceof HTMLCanvasElement) {
try {
drawReplacedElement({
drawable: node,
dimensions,
computedStyle,
contextToDraw
});
} catch (err) {
if (node instanceof HTMLImageElement) {
const readableError = getReadableImageError(err, node);
if (readableError) {
throw readableError;
}
}
throw err;
}
}
};
return domDrawFn;
};
// src/drawing/process-node.ts
import { Internals as Internals6 } from "remotion";
// src/drawing/has-transform.ts
var hasTransformCssValue = (style) => {
return style.transform !== "none" && style.transform !== "";
};
var hasRotateCssValue = (style) => {
return style.rotate !== "none" && style.rotate !== "";
};
var hasScaleCssValue = (style) => {
return style.scale !== "none" && style.scale !== "";
};
var hasAnyTransformCssValue = (style) => {
return hasTransformCssValue(style) || hasRotateCssValue(style) || hasScaleCssValue(style);
};
// src/drawing/parse-linear-gradient.ts
import { NoReactInternals as NoReactInternals2 } from "remotion/no-react";
var isValidColor = (color) => {
try {
const result = NoReactInternals2.processColor(color);
return result !== null && result !== undefined;
} catch {
return false;
}
};
var parseDirection = (directionStr) => {
const trimmed = directionStr.trim().toLowerCase();
if (trimmed.startsWith("to ")) {
const direction = trimmed.substring(3).trim();
switch (direction) {
case "top":
return 0;
case "right":
return 90;
case "bottom":
return 180;
case "left":
return 270;
case "top right":
case "right top":
return 45;
case "bottom right":
case "right bottom":
return 135;
case "bottom left":
case "left bottom":
return 225;
case "top left":
case "left top":
return 315;
default:
return 180;
}
}
const angleMatch = trimmed.match(/^(-?\d+\.?\d*)(deg|rad|grad|turn)$/);
if (angleMatch) {
const value = parseFloat(angleMatch[1]);
const unit = angleMatch[2];
switch (unit) {
case "deg":
return value;
case "rad":
return value * 180 / Math.PI;
case "grad":
return value * 360 / 400;
case "turn":
return value * 360;
default:
return value;
}
}
return 180;
};
var parseColorStops = (colorStopsStr) => {
const parts = colorStopsStr.split(/,(?![^(]*\))/);
const stops = [];
for (const part of parts) {
const trimmed = part.trim();
if (!trimmed)
continue;
const colorMatch = trimmed.match(/(rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-f]{3,8}|[a-z]+)/i);
if (!colorMatch) {
continue;
}
const colorStr = colorMatch[0];
if (!isValidColor(colorStr)) {
continue;
}
const remaining = trimmed.substring(colorMatch.index + colorStr.length).trim();
const normalizedColor = colorStr;
let position = null;
if (remaining) {
const posMatch = remaining.match(/(-?\d+\.?\d*)(%|px)?/);
if (posMatch) {
const value = parseFloat(posMatch[1]);
const unit = posMatch[2];
if (unit === "%") {
position = value / 100;
} else if (unit === "px") {
position = null;
} else {
position = value / 100;
}
}
}
stops.push({
color: normalizedColor,
position: position !== null ? position : -1
});
}
if (stops.length === 0) {
return null;
}
let lastExplicitIndex = -1;
let lastExplicitPosition = 0;
for (let i = 0;i < stops.length; i++) {
if (stops[i].position !== -1) {
if (lastExplicitIndex >= 0) {
const numImplicit = i - lastExplicitIndex - 1;
if (numImplicit > 0) {
const step = (stops[i].position - lastExplicitPosition) / (numImplicit + 1);
for (let j = lastExplicitIndex + 1;j < i; j++) {
stops[j].position = lastExplicitPosition + step * (j - lastExplicitIndex);
}
}
} else {
const numImplicit = i;
if (numImplicit > 0) {
const step = stops[i].position / (numImplicit + 1);
for (let j = 0;j < i; j++) {
stops[j].position = step * (j + 1);
}
}
}
lastExplicitIndex = i;
lastExplicitPosition = stops[i].position;
}
}
if (stops.every((s) => s.position === -1)) {
if (stops.length === 1) {
stops[0].position = 0.5;
} else {
for (let i = 0;i < stops.length; i++) {
stops[i].position = i / (stops.length - 1);
}
}
} else if (lastExplicitIndex < stops.length - 1) {
const numImplicit = stops.length - 1 - lastExplicitIndex;
const step = (1 - lastExplicitPosition) / (numImplicit + 1);
for (let i = lastExplicitIndex + 1;i < stops.length; i++) {
stops[i].position = lastExplicitPosition + step * (i - lastExplicitIndex);
}
}
for (const stop of stops) {
stop.position = Math.max(0, Math.min(1, stop.position));
}
return stops;
};
var extractGradientContent = (backgroundImage) => {
const prefix = "linear-gradient(";
const startIndex = backgroundImage.toLowerCase().indexOf(prefix);
if (startIndex === -1) {
return null;
}
let depth = 0;
const contentStart = startIndex + prefix.length;
for (let i = contentStart;i < backgroundImage.length; i++) {
const char = backgroundImage[i];
if (char === "(") {
depth++;
} else if (char === ")") {
if (depth === 0) {
return backgroundImage.substring(contentStart, i).trim();
}
depth--;
}
}
return null;
};
var parseLinearGradient = (backgroundImage) => {
if (!backgroundImage || backgroundImage === "none") {
return null;
}
const content = extractGradientContent(backgroundImage);
if (!content) {
return null;
}
const parts = content.split(/,(?![^(]*\))/);
let angle = 180;
let colorStopsStart = 0;
if (parts.length > 0) {
const firstPart = parts[0].trim();
const isDirection = firstPart.startsWith("to ") || /^-?\d+\.?\d*(deg|rad|grad|turn)$/.test(firstPart);
if (isDirection) {
angle = parseDirection(firstPart);
colorStopsStart = 1;
}
}
const colorStopsStr = parts.slice(colorStopsStart).join(",");
const colorStops = parseColorStops(colorStopsStr);
if (!colorStops || colorStops.length === 0) {
return null;
}
return {
angle,
colorStops
};
};
var createCanvasGradient = ({
ctx,
rect,
gradientInfo,
offsetLeft,
offsetTop
}) => {
const angleRad = (gradientInfo.angle - 90) * Math.PI / 180;
const centerX = rect.left - offsetLeft + rect.width / 2;
const centerY = rect.top - offsetTop + rect.height / 2;
const cos = Math.cos(angleRad);
const sin = Math.sin(angleRad);
const halfWidth = rect.width / 2;
const halfHeight = rect.height / 2;
let length = Math.abs(cos) * halfWidth + Math.abs(sin) * halfHeight;
if (!Number.isFinite(length) || length === 0) {
length = Math.sqrt(halfWidth ** 2 + halfHeight ** 2);
}
const x0 = centerX - cos * length;
const y0 = centerY - sin * length;
const x1 = centerX + cos * length;
const y1 = centerY + sin * length;
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
for (const stop of gradientInfo.colorStops) {
gradient.addColorStop(stop.position, stop.color);
}
return gradient;
};
// src/drawing/mask-image.ts
var getMaskImageValue = (computedStyle) => {
const { maskImage, webkitMaskImage } = computedStyle;
const value = maskImage || webkitMaskImage;
if (!value || value === "none") {
return null;
}
return value;
};
var parseMaskImage = (maskImageValue) => {
return parseLinearGradient(maskImageValue);
};
// src/drawing/parse-transform-origin.ts
var parseTransformOrigin = (transformOrigin) => {
if (transformOrigin.trim() === "") {
return null;
}
const [x, y] = transformOrigin.split(" ");
return { x: parseFloat(x), y: parseFloat(y) };
};
// src/drawing/calculate-transforms.ts
var getInternalTransformOrigin = (transform) => {
const centerX = transform.boundingClientRect.width / 2;
const centerY = transform.boundingClientRect.height / 2;
const origin = parseTransformOrigin(transform.transformOrigin) ?? {
x: centerX,
y: centerY
};
return origin;
};
var getGlobalTransformOrigin = ({ transform }) => {
const { x: originX, y: originY } = getInternalTransformOrigin(transform);
return {
x: originX + transform.boundingClientRect.left,
y: originY + transform.boundingClientRect.top
};
};
var calculateTransforms = ({
element,
rootElement
}) => {
let parent = element;
const transforms = [];
const toReset = [];
let opacity = 1;
let elementComputedStyle = null;
let maskImageInfo = null;
while (parent) {
const computedStyle = getComputedStyle(parent);
if (parent === element) {
elementComputedStyle = computedStyle;
opacity = parseFloat(computedStyle.opacity);
const maskImageValue = getMaskImageValue(computedStyle);
maskImageInfo = maskImageValue ? parseMaskImage(maskImageValue) : null;
const originalMaskImage = parent.style.maskImage;
const originalWebkitMaskImage = parent.style.webkitMaskImage;
parent.style.maskImage = "none";
parent.style.webkitMaskImage = "none";
const parentRef = parent;
toReset.push(() => {
parentRef.style.maskImage = originalMaskImage;
parentRef.style.webkitMaskImage = originalWebkitMaskImage;
});
}
if (hasAnyTransformCssValue(computedStyle) || parent === element) {
const toParse = hasTransformCssValue(computedStyle) ? computedStyle.transform : undefined;
const matrix = new DOMMatrix(toParse);
const { transform, scale, rotate } = parent.style;
const additionalMatrices = [];
if (rotate !== "" && rotate !== "none") {
additionalMatrices.push(new DOMMatrix(`rotate(${rotate})`));
}
if (scale !== "" && scale !== "none") {
additionalMatrices.push(new DOMMatrix(`scale(${scale})`));
}
additionalMatrices.push(matrix);
parent.style.transform = "none";
parent.style.scale = "none";
parent.style.rotate = "none";
transforms.push({
element: parent,
transformOrigin: computedStyle.transformOrigin,
boundingClientRect: null,
matrices: additionalMatrices
});
const parentRef = parent;
toReset.push(() => {
parentRef.style.transform = transform;
parentRef.style.scale = scale;
parentRef.style.rotate = rotate;
});
}
if (parent === rootElement) {
break;
}
parent = parent.parentElement;
}
for (const transform of transforms) {
transform.boundingClientRect = transform.element.getBoundingClientRect();
}
const dimensions = transforms[0].boundingClientRect;
const nativeTransformOrigin = getInternalTransformOrigin(transforms[0]);
const totalMatrix = new DOMMatrix;
for (const transform of transforms.slice().reverse()) {
for (const matrix of transform.matrices) {
const globalTransformOrigin = getGlobalTransformOrigin({
transform
});
const transformMatrix = new DOMMatrix().translate(globalTransformOrigin.x, globalTransformOrigin.y).multiply(matrix).translate(-globalTransformOrigin.x, -globalTransformOrigin.y);
totalMatrix.multiplySelf(transformMatrix);
}
}
if (!elementComputedStyle) {
throw new Error("Element computed style not found");
}
const needs3DTransformViaWebGL = !totalMatrix.is2D;
const needsMaskImage = maskImageInfo !== null;
return {
dimensions,
totalMatrix,
[Symbol.dispose]: () => {
for (const reset of toReset) {
reset();
}
},
nativeTransformOrigin,
computedStyle: elementComputedStyle,
opacity,
maskImageInfo,
precompositing: {
needs3DTransformViaWebGL,
needsMaskImage: maskImageInfo,
needsPrecompositing: Boolean(needs3DTransformViaWebGL || needsMaskImage)
}
};
};
// src/drawing/round-to-expand-rect.ts
var roundToExpandRect = (rect) => {
const left = Math.floor(rect.left);
const top = Math.floor(rect.top);
const right = Math.ceil(rect.right);
const bottom = Math.ceil(rect.bottom);
return new DOMRect(left, top, right - left, bottom - top);
};
// src/drawing/clamp-rect-to-parent-bounds.ts
var getNarrowerRect = ({
firstRect,
secondRect
}) => {
const left = Math.max(firstRect.left, secondRect.left);
const top = Math.max(firstRect.top, secondRect.top);
const bottom = Math.min(firstRect.bottom, secondRect.bottom);
const right = Math.min(firstRect.right, secondRect.right);
return new DOMRect(left, top, right - left, bottom - top);
};
var getWiderRectAndExpand = ({
firstRect,
secondRect
}) => {
if (firstRect === null) {
return roundToExpandRect(secondRect);
}
const left = Math.min(firstRect.left, secondRect.left);
const top = Math.min(firstRect.top, secondRect.top);
const bottom = Math.max(firstRect.bottom, secondRect.bottom);
const right = Math.max(firstRect.right, secondRect.right);
return roundToExpandRect(new DOMRect(left, top, right - left, bottom - top));
};
// src/drawing/do-rects-intersect.ts
function doRectsIntersect(rect1, rect2) {
return !(rect1.right <= rect2.left || rect1.left >= rect2.right || rect1.bottom <= rect2.top || rect1.top >= rect2.bottom);
}
// src/drawing/draw-rounded.ts
var drawRoundedRectPath = ({
ctx,
x,
y,
width,
height,
borderRadius
}) => {
ctx.beginPath();
ctx.moveTo(x + borderRadius.topLeft.horizontal, y);
ctx.lineTo(x + width - borderRadius.topRight.horizontal, y);
if (borderRadius.topRight.horizontal > 0 || borderRadius.topRight.vertical > 0) {
ctx.ellipse(x + width - borderRadius.topRight.horizontal, y + borderRadius.topRight.vertical, borderRadius.topRight.horizontal, borderRadius.topRight.vertical, 0, -Math.PI / 2, 0);
}
ctx.lineTo(x + width, y + height - borderRadius.bottomRight.vertical);
if (borderRadius.bottomRight.horizontal > 0 || borderRadius.bottomRight.vertical > 0) {
ctx.ellipse(x + width - borderRadius.bottomRight.horizontal, y + height - borderRadius.bottomRight.vertical, borderRadius.bottomRight.horizontal, borderRadius.bottomRight.vertical, 0, 0, Math.PI / 2);
}
ctx.lineTo(x + borderRadius.bottomLeft.horizontal, y + height);
if (borderRadius.bottomLeft.horizontal > 0 || borderRadius.bottomLeft.vertical > 0) {
ctx.ellipse(x + borderRadius.bottomLeft.horizontal, y + height - borderRadius.bottomLeft.vertical, borderRadius.bottomLeft.horizontal, borderRadius.bottomLeft.vertical, 0, Math.PI / 2, Math.PI);
}
ctx.lineTo(x, y + borderRadius.topLeft.vertical);
if (borderRadius.topLeft.horizontal > 0 || borderRadius.topLeft.vertical > 0) {
ctx.ellipse(x + borderRadius.topLeft.horizontal, y + borderRadius.topLeft.vertical, borderRadius.topLeft.horizontal, borderRadius.topLeft.vertical, 0, Math.PI, Math.PI * 3 / 2);
}
ctx.closePath();
};
// src/drawing/get-padding-box.ts
var getPaddingBox = (rect, computedStyle) => {
const borderLeft = parseFloat(computedStyle.borderLeftWidth);
const borderRight = parseFloat(computedStyle.borderRightWidth);
const borderTop = parseFloat(computedStyle.borderTopWidth);
const borderBottom = parseFloat(computedStyle.borderBottomWidth);
return new DOMRect(rect.left + borderLeft, rect.top + borderTop, rect.width - borderLeft - borderRight, rect.height - borderTop - borderBottom);
};
var getContentBox = (rect, computedStyle) => {
const paddingBox = getPaddingBox(rect, computedStyle);
const paddingLeft = parseFloat(computedStyle.paddingLeft);
const paddingRight = parseFloat(computedStyle.paddingRight);
const paddingTop = parseFloat(computedStyle.paddingTop);
const paddingBottom = parseFloat(computedStyle.paddingBottom);
return new DOMRect(paddingBox.left + paddingLeft, paddingBox.top + paddingTop, paddingBox.width - paddingLeft - paddingRight, paddingBox.height - paddingTop - paddingBottom);
};
var getBoxBasedOnBackgroundClip = (rect, computedStyle, backgroundClip) => {
if (!backgroundClip) {
return rect;
}
if (backgroundClip.includes("text")) {
return rect;
}
if (backgroundClip.includes("padding-box")) {
return getPaddingBox(rect, computedStyle);
}
if (backgroundClip.includes("content-box")) {
return getContentBox(rect, computedStyle);
}
return rect;
};
// src/drawing/border-radius.ts
function parseValue({
value,
reference
}) {
value = value.trim();
if (value.endsWith("%")) {
const percentage = parseFloat(value);
return percentage / 100 * reference;
}
if (value.endsWith("px")) {
return parseFloat(value);
}
return parseFloat(value);
}
function expandShorthand(values) {
if (values.length === 1) {
return [values[0], values[0], values[0], values[0]];
}
if (values.length === 2) {
return [values[0], values[1], values[0], values[1]];
}
if (values.length === 3) {
return [values[0], values[1], values[2], values[1]];
}
return [values[0], values[1], values[2], values[3]];
}
function clampBorderRadius({
borderRadius,
width,
height
}) {
const clamped = {
topLeft: { ...borderRadius.topLeft },
topRight: { ...borderRadius.topRight },
bottomRight: { ...borderRadius.bottomRight },
bottomLeft: { ...borderRadius.bottomLeft }
};
const topSum = clamped.topLeft.horizontal + clamped.topRight.horizontal;
if (topSum > width) {
const factor = width / topSum;
clamped.topLeft.horizontal *= factor;
clamped.topRight.horizontal *= factor;
}
const rightSum = clamped.topRight.vertical + clamped.bottomRight.vertical;
if (rightSum > height) {
const factor = height / rightSum;
clamped.topRight.vertical *= factor;
clamped.bottomRight.vertical *= factor;
}
const bottomSum = clamped.bottomRight.horizontal + clamped.bottomLeft.horizontal;
if (bottomSum > width) {
const factor = width / bottomSum;
clamped.bottomRight.horizontal *= factor;
clamped.bottomLeft.horizontal *= factor;
}
const leftSum = clamped.bottomLeft.vertical + clamped.topLeft.vertical;
if (leftSum > height) {
const factor = height / leftSum;
clamped.bottomLeft.vertical *= factor;
clamped.topLeft.vertical *= factor;
}
return clamped;
}
function parseBorderRadius({
borderRadius,
width,
height
}) {
const parts = borderRadius.split("/").map((part) => part.trim());
const horizontalPart = parts[0];
const verticalPart = parts[1];
const horizontalValues = horizontalPart.split(/\s+/).filter((v) => v);
const verticalValues = verticalPart ? verticalPart.split(/\s+/).filter((v) => v) : horizontalValues;
const [hTopLeft, hTopRight, hBottomRight, hBottomLeft] = expandShorthand(horizontalValues);
const [vTopLeft, vTopRight, vBottomRight, vBottomLeft] = expandShorthand(verticalValues);
return clampBorderRadius({
borderRadius: {
topLeft: {
horizontal: parseValue({ value: hTopLeft, reference: width }),
vertical: parseValue({ value: vTopLeft, reference: height })
},
topRight: {
horizontal: parseValue({ value: hTopRight, reference: width }),
vertical: parseValue({ value: vTopRight, reference: height })
},
bottomRight: {
horizontal: parseValue({ value: hBottomRight, reference: width }),
vertical: parseValue({ value: vBottomRight, reference: height })
},
bottomLeft: {
horizontal: parseValue({ value: hBottomLeft, reference: width }),
vertical: parseValue({ value: vBottomLeft, reference: height })
}
},
width,
height
});
}
function setBorderRadius({
ctx,
rect,
borderRadius,
forceClipEvenWhenZero = false,
computedStyle,
backgroundClip
}) {
if (borderRadius.topLeft.horizontal === 0 && borderRadius.topLeft.vertical === 0 && borderRadius.topRight.horizontal === 0 && borderRadius.topRight.vertical === 0 && borderRadius.bottomRight.horizontal === 0 && borderRadius.bottomRight.vertical === 0 && borderRadius.bottomLeft.horizontal === 0 && borderRadius.bottomLeft.vertical === 0 && !forceClipEvenWhenZero) {
return () => {};
}
ctx.save();
const boundingRect = getBoxBasedOnBackgroundClip(rect, computedStyle, backgroundClip);
const actualBorderRadius = {
topLeft: {
horizontal: Math.max(0, borderRadius.topLeft.horizontal - (boundingRect.left - rect.left)),
vertical: Math.max(0, borderRadius.topLeft.vertical - (boundingRect.top - rect.top))
},
topRight: {
horizontal: Math.max(0, borderRadius.topRight.horizontal - (rect.right - boundingRect.right)),
vertical: Math.max(0, borderRadius.topRight.vertical - (boundingRect.top - rect.top))
},
bottomRight: {
horizontal: Math.max(0, borderRadius.bottomRight.horizontal - (rect.right - boundingRect.right)),
vertical: Math.max(0, borderRadius.bottomRight.vertical - (rect.bottom - boundingRect.bottom))
},
bottomLeft: {
horizontal: Math.max(0, borderRadius.bottomLeft.horizontal - (boundingRect.left - rect.left)),
vertical: Math.max(0, borderRadius.bottomLeft.vertical - (rect.bottom - boundingRect.bottom))
}
};
drawRoundedRectPath({
ctx,
x: boundingRect.left,
y: boundingRect.top,
width: boundingRect.width,
height: boundingRect.height,
borderRadius: actualBorderRadius
});
ctx.clip();
return () => {
ctx.restore();
};
}
// src/drawing/get-background-fill.ts
var isColorTransparent = (color) => {
return color === "transparent" || color.startsWith("rgba") && (color.endsWith(", 0)") || color.endsWith(",0"));
};
var getBackgroundFill = ({
backgroundColor,
backgroundImage,
contextToDraw,
boundingRect,
offsetLeft,
offsetTop
}) => {
if (backgroundImage && backgroundImage !== "none") {
const gradientInfo = parseLinearGradient(backgroundImage);
if (gradientInfo) {
const gradient = createCanvasGradient({
ctx: contextToDraw,
rect: boundingRect,
gradientInfo,
offsetLeft,
offsetTop
});
return gradient;
}
}
if (backgroundColor && backgroundColor !== "transparent" && !isColorTransparent(backgroundColor)) {
return backgroundColor;
}
return null;
};
// src/drawing/draw-background.ts
var drawBackground = async ({
backgroundImage,
context,
rect,
backgroundColor,
backgroundClip,
element,
logLevel,
internalState,
computedStyle,
offsetLeft: parentOffsetLeft,
offsetTop: parentOffsetTop,
scale
}) => {
let __stack = [];
try {
let contextToDraw = context;
const originalCompositeOperation = context.globalCompositeOperation;
let offsetLeft = 0;
let offsetTop = 0;
const _ = __using(__stack, {
[Symbol.dispose]: () => {
context.globalCompositeOperation = originalCompositeOperation;
if (context !== contextToDraw) {
context.drawImage(contextToDraw.canvas, offsetLeft, offsetTop, contextToDraw.canvas.width / scale, contextToDraw.canvas.height / scale);
}
}
}, 0);
const boundingRect = getBoxBasedOnBackgroundClip(rect, computedStyle, backgroundClip);
if (backgroundClip.includes("text")) {
offsetLeft = boundingRect.left;
offsetTop = boundingRect.top;
const originalBackgroundClip = element.style.backgroundClip;
const originalWebkitBackgroundClip = element.style.webkitBackgroundClip;
element.style.backgroundClip = "initial";
element.style.webkitBackgroundClip = "initial";
const onlyBackgroundClipText = await createLayer({
element,
cutout: new DOMRect(boundingRect.left + parentOffsetLeft, boundingRect.top + parentOffsetTop, boundingRect.width, boundingRect.height),
logLevel,
internalState,
scale,
onlyBackgroundClipText: true
});
onlyBackgroundClipText.setTransform(new DOMMatrix().scale(scale, scale));
element.style.backgroundClip = originalBackgroundClip;
element.style.webkitBackgroundClip = originalWebkitBackgroundClip;
contextToDraw = onlyBackgroundClipText;
contextToDraw.globalCompositeOperation = "source-in";
}
const backgroundFill = getBackgroundFill({
backgroundImage,
backgroundColor,
contextToDraw,
boundingRect,
offsetLeft,
offsetTop
});
if (!backgroundFill) {
return;
}
const originalFillStyle = contextToDraw.fillStyle;
contextToDraw.fillStyle = backgroundFill;
contextToDraw.fillRect(boundingRect.left - offsetLeft, boundingRect.top - offsetTop, boundingRect.width, boundingRect.height);
contextToDraw.fillStyle = originalFillStyle;
} catch (_catch) {
var _err = _catch, _hasErr = 1;
} finally {
__callDispose(__stack, _err, _hasErr);
}
};
// src/drawing/draw-border.ts
var parseBorderWidth = (value) => {
return parseFloat(value) || 0;
};
var getBorderSideProperties = (computedStyle) => {
return {
top: {
width: parseBorderWidth(computedStyle.borderTopWidth),
color: computedStyle.borderTopColor || computedStyle.borderColor || "black",
style: computedStyle.borderTopStyle || computedStyle.borderStyle || "solid"
},
right: {
width: parseBorderWidth(computedStyle.borderRightWidth),
color: computedStyle.borderRightColor || computedStyle.borderColor || "black",
style: computedStyle.borderRightStyle || computedStyle.borderStyle || "solid"
},
bottom: {
width: parseBorderWidth(computedStyle.borderBottomWidth),
color: computedStyle.borderBottomColor || computedStyle.borderColor || "black",
style: computedStyle.borderBottomStyle || computedStyle.borderStyle || "solid"
},
left: {
width: parseBorderWidth(computedStyle.borderLeftWidth),
color: computedStyle.borderLeftColor || computedStyle.borderColor || "black",
style: computedStyle.borderLeftStyle || computedStyle.borderStyle || "solid"
}
};
};
var getLineDashPattern = (style, width) => {
if (style === "dashed") {
return [width * 2, width];
}
if (style === "dotted") {
return [width, width];
}
return [];
};
var drawBorderSide = ({
ctx,
side,
x,
y,
width,
height,
borderRadius,
borderProperties
}) => {
const { width: borderWidth, color, style } = borderProperties;
if (borderWidth <= 0 || style === "none" || style === "hidden") {
return;
}
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = borderWidth;
ctx.setLineDash(getLineDashPattern(style, borderWidth));
const halfWidth = borderWidth / 2;
if (side === "top") {
const startX = x + borderRadius.topLeft.horizontal;
const startY = y + halfWidth;
const endX = x + width - borderRadius.topRight.horizontal;
const endY = y + halfWidth;
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
} else if (side === "right") {
const startX = x + width - halfWidth;
const startY = y + borderRadius.topRight.vertical;
const endX = x + width - halfWidth;
const endY = y + height - borderRadius.bottomRight.vertical;
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
} else if (side === "bottom") {
const startX = x + borderRadius.bottomLeft.horizontal;
const startY = y + height - halfWidth;
const endX = x + width - borderRadius.bottomRight.horizontal;
const endY = y + height - halfWidth;
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
} else if (side === "left") {
const startX = x + halfWidth;
const startY = y + borderRadius.topLeft.vertical;
const endX = x + halfWidth;
const endY = y + height - borderRadius.bottomLeft.vertical;
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
}
ctx.stroke();
};
var drawCorner = ({
ctx,
corner,
x,
y,
width,
height,
borderRadius,
topBorder,
rightBorder,
bottomBorder,
leftBorder
}) => {
const radius = borderRadius[corner];
if (radius.horizontal <= 0 && radius.vertical <= 0) {
return;
}
let border1;
let border2;
let centerX;
let centerY;
let startAngle;
let endAngle;
if (corner === "topLeft") {
border1 = leftBorder;
border2 = topBorder;
centerX = x + radius.horizontal;
centerY = y + radius.vertical;
startAngle = Math.PI;
endAngle = Math.PI * 3 / 2;
} else if (corner === "topRight") {
border1 = topBorder;
border2 = rightBorder;
centerX = x + width - radius.horizontal;
centerY = y + radius.vertical;
startAngle = -Math.PI / 2;
endAngle = 0;
} else if (corner === "bottomRight") {
border1 = rightBorder;
border2 = bottomBorder;
centerX = x + width - radius.horizontal;
centerY = y + height - radius.vertical;
startAngle = 0;
endAngle = Math.PI / 2;
} else {
border1 = bottomBorder;
border2 = leftBorder;
centerX = x + radius.horizontal;
centerY = y + height - radius.vertical;
startAngle = Math.PI / 2;
endAngle = Math.PI;
}
const avgWidth = (border1.width + border2.width) / 2;
const useColor = border1.width >= border2.width ? border1.color : border2.color;
const useStyle = border1.width >= border2.width ? border1.style : border2.style;
if (avgWidth > 0 && useStyle !== "none" && useStyle !== "hidden") {
ctx.beginPath();
ctx.strokeStyle = useColor;
ctx.lineWidth = avgWidth;
ctx.setLineDash(getLineDashPattern(useStyle, avgWidth));
const adjustedRadiusH = Math.max(0, radius.horizontal - avgWidth / 2);
const adjustedRadiusV = Math.max(0, radius.vertical - avgWidth / 2);
ctx.ellipse(centerX, centerY, adjustedRadiusH, adjustedRadiusV, 0, startAngle, endAngle);
ctx.stroke();
}
};
var drawUniformBorder = ({
ctx,
x,
y,
width,
height,
borderRadius,
borderWidth,
borderColor,
borderStyle
}) => {
ctx.beginPath();
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.setLineDash(getLineDashPattern(borderStyle, borderWidth));
const halfWidth = borderWidth / 2;
const borderX = x + halfWidth;
const borderY = y + halfWidth;
const borderW = width - borderWidth;
const borderH = height - borderWidth;
const adjustedBorderRadius = {
topLeft: {
horizontal: Math.max(0, borderRadius.topLeft.horizontal - halfWidth),
vertical: Math.max(0, borderRadius.topLeft.vertical - halfWidth)
},
topRight: {
horizontal: Math.max(0, borderRadius.topRight.horizontal - halfWidth),
vertical: Math.max(0, borderRadius.topRight.vertical - halfWidth)
},
bottomRight: {
horizontal: Math.max(0, borderRadius.bottomRight.horizontal - halfWidth),
vertical: Math.max(0, borderRadius.bottomRight.vertical - halfWidth)
},
bottomLeft: {
horizontal: Math.max(0, borderRadius.bottomLeft.horizontal - halfWidth),
vertical: Math.max(0, borderRadius.bottomLeft.vertical - halfWidth)
}
};
ctx.moveTo(borderX + adjustedBorderRadius.topLeft.horizontal, borderY);
ctx.lineTo(borderX + borderW - adjustedBorderRadius.topRight.horizontal, borderY);
if (adjustedBorderRadius.topRight.horizontal > 0 || adjustedBorderRadius.topRight.vertical > 0) {
ctx.ellipse(borderX + borderW - adjustedBorderRadius.topRight.horizontal, borderY + adjustedBorderRadius.topRight.vertical, adjustedBorderRadius.topRight.horizontal, adjustedBorderRadius.topRight.vertical, 0, -Math.PI / 2, 0);
}
ctx.lineTo(borderX + borderW, borderY + borderH - adjustedBorderRadius.bottomRight.vertical);
if (adjustedBorderRadius.bottomRight.horizontal > 0 || adjustedBorderRadius.bottomRight.vertical > 0) {
ctx.ellipse(borderX + borderW - adjustedBorderRadius.bottomRight.horizontal, borderY + borderH - adjustedBorderRadius.bottomRight.vertical, adjustedBorderRadius.bottomRight.horizontal, adjustedBorderRadius.bottomRight.vertical, 0, 0, Math.PI / 2);
}
ctx.lineTo(borderX + adjustedBorderRadius.bottomLeft.horizontal, borderY + borderH);
if (adjustedBorderRadius.bottomLeft.horizontal > 0 || adjustedBorderRadius.bottomLeft.vertical > 0) {
ctx.ellipse(borderX + adjustedBorderRadius.bottomLeft.horizontal, borderY + borderH - adjustedBorderRadius.bottomLeft.vertical, adjustedBorderRadius.bottomLeft.horizontal, adjustedBorderRadius.bottomLeft.vertical, 0, Math.PI / 2, Math.PI);
}
ctx.lineTo(borderX, borderY + adjustedBorderRadius.topLeft.vertical);
if (adjustedBorderRadius.topLeft.horizontal > 0 || adjustedBorderRadius.topLeft.vertical > 0) {
ctx.ellipse(borderX + adjustedBorderRadius.topLeft.horizontal, borderY + adjustedBorderRadius.topLeft.vertical, adjustedBorderRadius.topLeft.horizontal, adjustedBorderRadius.topLeft.vertical, 0, Math.PI, Math.PI * 3 / 2);
}
ctx.closePath();
ctx.stroke();
};
var drawBorder = ({
ctx,
rect,
borderRadius,
computedStyle
}) => {
const borders = getBorderSideProperties(computedStyle);
const hasBorder = borders.top.width > 0 || borders.right.width > 0 || borders.bottom.width > 0 || borders.left.width > 0;
if (!hasBorder) {
return;
}
const originalStrokeStyle = ctx.strokeStyle;
const originalLineWidth = ctx.lineWidth;
const originalLineDash = ctx.getLineDash();
const allSidesEqual = borders.top.width === borders.right.width && borders.top.width === borders.bottom.width && borders.top.width === borders.left.width && borders.top.color === borders.right.color && borders.top.color === borders.bottom.color && borders.top.color === borders.left.color && borders.top.style === borders.right.style && borders.top.style === borders.bottom.style && borders.top.style === borders.left.style && borders.top.width > 0;
if (allSidesEqual) {
drawUniformBorder({
ctx,
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
borderRadius,
borderWidth: borders.top.width,
borderColor: borders.top.color,
borderStyle: borders.top.style
});
} else {
drawCorner({
ctx,
corner: "topLeft",
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
borderRadius,
topBorder: borders.top,
rightBorder: borders.right,
bottomBorder: borders.bottom,
leftBorder: borders.left
});
drawCorner({
ctx,
corner: "topRight",
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
borderRadius,
topBorder: borders.top,
rightBorder: borders.right,
bottomBorder: borders.bottom,
leftBorder: borders.left
});
drawCorner({
ctx,
corner: "bottomRight",
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
borderRadius,
topBorder: borders.top,
rightBorder: borders.right,
bottomBorder: borders.bottom,
leftBorder: borders.left
});
drawCorner({
ctx,
corner: "bottomLeft",
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
borderRadius,
topBorder: borders.top,
rightBorder: borders.right,
bottomBorder: borders.bottom,
leftBorder: borders.left
});
drawBorderSide({
ctx,
side: "top",
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
borderRadius,
borderProperties: borders.top
});
drawBorderSide({
ctx,
side: "right",
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
borderRadius,
borderProperties: borders.right
});
drawBorderSide({
ctx,
side: "bottom",
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
borderRadius,
borderProperties: borders.bottom
});
drawBorderSide({
ctx,
side: "left",
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
borderRadius,
borderProperties: borders.left
});
}
ctx.strokeStyle = originalStrokeStyle;
ctx.lineWidth = originalLineWidth;
ctx.setLineDash(originalLineDash);
};
// src/drawing/draw-box-shadow.ts
import { Internals as Internals5 } from "remotion";
var parseBoxShadow = (boxShadowValue) => {
if (!boxShadowValue || boxShadowValue === "none") {
return [];
}
const shadows = [];
const shadowStrings = boxShadowValue.split(/,(?![^(]*\))/);
for (const shadowStr of shadowStrings) {
const trimmed = shadowStr.trim();
if (!trimmed || trimmed === "none") {
continue;
}
const shadow = {
offsetX: 0,
offsetY: 0,
blurRadius: 0,
color: "rgba(0, 0, 0, 0.5)",
inset: false
};
shadow.inset = /\binset\b/i.test(trimmed);
let remaining = trimmed.replace(/\binset\b/gi, "").trim();
const colorMatch = remaining.match(/(rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-f]{3,8}|[a-z]+)/i);
if (colorMatch) {
shadow.color = colorMatch[0];
remaining = remaining.replace(colorMatch[0], "").trim();
}
const numbers = remaining.match(/[+-]?\d*\.?\d+(?:px|em|rem|%)?/gi) || [];
const values = numbers.map((n) => parseFloat(n) || 0);
if (values.length >= 2) {
shadow.offsetX = values[0];
shadow.offsetY = values[1];
if (values.length >= 3) {
shadow.blurRadius = Math.max(0, values[2]);
}
}
shadows.push(shadow);
}
return shadows;
};
var drawBorderRadius = ({
ctx,
rect,
borderRadius,
computedStyle,
logLevel
}) => {
const shadows = parseBoxShadow(computedStyle.boxShadow);
if (shadows.length === 0) {
return;
}
for (let i = shadows.length - 1;i >= 0; i--) {
const shadow = shadows[i];
const newLeft = rect.left + Math.min(shadow.offsetX, 0) - shadow.blurRadius;
const newRight = rect.right + Math.max(shadow.offsetX, 0) + shadow.blurRadius;
const newTop = rect.top + Math.min(shadow.offsetY, 0) - shadow.blurRadius;
const newBottom = rect.bottom + Math.max(shadow.offsetY, 0) + shadow.blurRadius;
const newRect = new DOMRect(newLeft, newTop, newRight - newLeft, newBottom - newTop);
const leftOffset = rect.left - newLeft;
const topOffset = rect.top - newTop;
const newCanvas = new OffscreenCanvas(newRect.width, newRect.height);
const newCtx = newCanvas.getContext("2d");
if (!newCtx) {
throw new Error("Failed to get context");
}
if (shadow.inset) {
Internals5.Log.warn({
logLevel,
tag: "@remotion/web-renderer"
}, 'Detected "box-shadow" with "inset". This is not yet supported in @remotion/web-renderer');
continue;
}
newCtx.shadowBlur = shadow.blurRadius;
newCtx.shadowColor = shadow.color;
newCtx.shadowOffsetX = shadow.offsetX;
newCtx.shadowOffsetY = shadow.offsetY;
newCtx.fillStyle = "black";
drawRoundedRectPath({
ctx: newCtx,
x: leftOffset,
y: topOffset,
width: rect.width,
height: rect.height,
borderRadius
});
newCtx.fill();
newCtx.shadowColor = "transparent";
newCtx.globalCompositeOperation = "destination-out";
drawRoundedRectPath({
ctx: newCtx,
x: leftOffset,
y: topOffset,
width: rect.width,
height: rect.height,
borderRadius
});
newCtx.fill();
ctx.drawImage(newCanvas, rect.left - leftOffset, rect.top - topOffset);
}
};
// src/drawing/draw-outline.ts
var parseOutlineWidth = (value) => {
return parseFloat(value) || 0;
};
var parseOutlineOffset = (value) => {
return parseFloat(value) || 0;
};
var getLineDashPattern2 = (style, width) => {
if (style === "dashed") {
return [width * 2, width];
}
if (style === "dotted") {
return [width, width];
}
return [];
};
var drawOutline = ({
ctx,
rect,
borderRadius,
computedStyle
}) => {
const outlineWidth = parseOutlineWidth(computedStyle.outlineWidth);
const { outlineStyle } = computedStyle;
const outlineColor = computedStyle.outlineColor || "black";
const outlineOffset = parseOutlineOffset(computedStyle.outlineOffset);
if (outlineWidth <= 0 || outlineStyle === "none" || outlineStyle === "hidden") {
return;
}
const originalStrokeStyle = ctx.strokeStyle;
const originalLineWidth = ctx.lineWidth;
const originalLineDash = ctx.getLineDash();
ctx.strokeStyle = outlineColor;
ctx.lineWidth = outlineWidth;
ctx.setLineDash(getLineDashPattern2(outlineStyle, outlineWidth));
const halfWidth = outlineWidth / 2;
const offset = outlineOffset + halfWidth;
const outlineX = rect.left - offset;
const outlineY = rect.top - offset;
const outlineW = rect.width + offset * 2;
const outlineH = rect.height + offset * 2;
const adjustedBorderRadius = {
topLeft: {
horizontal: borderRadius.topLeft.horizontal === 0 ? 0 : Math.max(0, borderRadius.topLeft.horizontal + offset),
vertical: borderRadius.topLeft.vertical === 0 ? 0 : Math.max(0, borderRadius.topLeft.vertical + offset)
},
topRight: {
horizontal: borderRadius.topRight.horizontal === 0 ? 0 : Math.max(0, borderRadius.topRight.horizontal + offset),
vertical: borderRadius.topRight.vertical === 0 ? 0 : Math.max(0, borderRadius.topRight.vertical + offset)
},
bottomRight: {
horizontal: borderRadius.bottomRight.horizontal === 0 ? 0 : Math.max(0, borderRadius.bottomRight.horizontal + offset),
vertical: borderRadius.bottomRight.vertical === 0 ? 0 : Math.max(0, borderRadius.bottomRight.vertical + offset)
},
bottomLeft: {
horizontal: borderRadius.bottomLeft.horizontal === 0 ? 0 : Math.max(0, borderRadius.bottomLeft.horizontal + offset),
vertical: borderRadius.bottomLeft.vertical === 0 ? 0 : Math.max(0, borderRadius.bottomLeft.vertical + offset)
}
};
drawRoundedRectPath({
ctx,
x: outlineX,
y: outlineY,
width: outlineW,
height: outlineH,
borderRadius: adjustedBorderRadius
});
ctx.stroke();
ctx.strokeStyle = originalStrokeStyle;
ctx.lineWidth = originalLineWidth;
ctx.setLineDash(originalLineDash);
};
// src/drawing/opacity.ts
var setOpacity = ({
ctx,
opacity
}) => {
const previousAlpha = ctx.globalAlpha;
ctx.globalAlpha = previousAlpha * opacity;
return () => {
ctx.globalAlpha = previousAlpha;
};
};
// src/drawing/overflow.ts
var setOverflowHidden = ({
ctx,
rect,
borderRadius,
overflowHidden,
computedStyle,
backgroundClip
}) => {
if (!overflowHidden) {
return () => {};
}
return setBorderRadius({
ctx,
rect,
borderRadius,
forceClipEvenWhenZero: true,
computedStyle,
backgroundClip
});
};
// src/drawing/transform.ts
var setTransform = ({
ctx,
transform,
parentRect,
scale
}) => {
const offsetMatrix = new DOMMatrix().scale(scale, scale).translate(-parentRect.x, -parentRect.y).multiply(transform).translate(parentRect.x, parentRect.y);
ctx.setTransform(offsetMatrix);
return () => {
ctx.setTransform(new DOMMatrix);
};
};
// src/drawing/draw-element.ts
var drawElement = async ({
rect,
computedStyle,
context,
draw,
opacity,
totalMatrix,
parentRect,
logLevel,
element,
internalState,
scale
}) => {
const { backgroundImage, backgroundColor, backgroundClip } = computedStyle;
const borderRadius = parseBorderRadius({
borderRadius: computedStyle.borderRadius,
width: rect.width,
height: rect.height
});
const finishTransform = setTransform({
ctx: context,
transform: totalMatrix,
parentRect,
scale
});
const finishOpacity = setOpacity({
ctx: context,
opacity
});
drawBorderRadius({
ctx: context,
computedStyle,
rect,
borderRadius,
logLevel
});
const finishBorderRadius = setBorderRadius({
ctx: context,
rect,
borderRadius,
forceClipEvenWhenZero: false,
computedStyle,
backgroundClip
});
await drawBackground({
backgroundImage,
context,
rect,
backgroundColor,
backgroundClip,
element,
logLevel,
internalState,
computedStyle,
offsetLeft: parentRect.left,
offsetTop: parentRect.top,
scale
});
await draw({ dimensions: rect, computedStyle, contextToDraw: context });
finishBorderRadius();
drawBorder({
ctx: context,
rect,
borderRadius,
computedStyle
});
drawOutline({
ctx: context,
rect,
borderRadius,
computedStyle
});
const finishOverflowHidden = setOverflowHidden({
ctx: context,
rect,
borderRadius,
overflowHidden: computedStyle.overflow === "hidden",
computedStyle,
backgroundClip
});
finishTransform();
return {
cleanupAfterChildren: () => {
finishOpacity();
finishOverflowHidden();
}
};
};
// src/walk-tree.ts
function skipToNextNonDescendant(treeWalker) {
if (treeWalker.nextSibling()) {
return true;
}
while (treeWalker.parentNode()) {
if (treeWalker.nextSibling()) {
return true;
}
}
return false;
}
// src/get-biggest-bounding-client-rect.ts
var getBiggestBoundingClientRect = (element) => {
const treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT);
let mostLeft = Infinity;
let mostTop = Infinity;
let mostRight = -Infinity;
let mostBottom = -Infinity;
while (true) {
const computedStyle = getComputedStyle(treeWalker.currentNode);
const outlineWidth = parseOutlineWidth(computedStyle.outlineWidth);
const outlineOffset = parseOutlineOffset(computedStyle.outlineOffset);
const rect = treeWalker.currentNode.getBoundingClientRect();
const shadows = parseBoxShadow(computedStyle.boxShadow);
let shadowLeft = 0;
let shadowRight = 0;
let shadowTop = 0;
let shadowBottom = 0;
for (const shadow of shadows) {
if (!shadow.inset) {
shadowLeft = Math.max(shadowLeft, Math.abs(Math.min(shadow.offsetX, 0)) + shadow.blurRadius);
shadowRight = Math.max(shadowRight, Math.max(shadow.offsetX, 0) + shadow.blurRadius);
shadowTop = Math.max(shadowTop, Math.abs(Math.min(shadow.offsetY, 0)) + shadow.blurRadius);
shadowBottom = Math.max(shadowBottom, Math.max(shadow.offsetY, 0) + shadow.blurRadius);
}
}
mostLeft = Math.min(mostLeft, rect.left - outlineOffset - outlineWidth - shadowLeft);
mostTop = Math.min(mostTop, rect.top - outlineOffset - outlineWidth - shadowTop);
mostRight = Math.max(mostRight, rect.right + outlineOffset + outlineWidth + shadowRight);
mostBottom = Math.max(mostBottom, rect.bottom + outlineOffset + outlineWidth + shadowBottom);
if (computedStyle.overflow === "hidden") {
if (!skipToNextNonDescendant(treeWalker)) {
break;
}
}
if (!treeWalker.nextNode()) {
break;
}
}
return new DOMRect(mostLeft, mostTop, mostRight - mostLeft, mostBottom - mostTop);
};
// src/drawing/get-pretransform-rect.ts
var MAX_SCALE_FACTOR = 100;
var isScaleTooBig = (matrix) => {
const origin = new DOMPoint(0, 0).matrixTransform(matrix);
const unitX = new DOMPoint(1, 0).matrixTransform(matrix);
const unitY = new DOMPoint(0, 1).matrixTransform(matrix);
const basisX = { x: unitX.x - origin.x, y: unitX.y - origin.y };
const basisY = { x: unitY.x - origin.x, y: unitY.y - origin.y };
const scaleX = 1 / Math.hypot(basisX.x, basisX.y);
const scaleY = 1 / Math.hypot(basisY.x, basisY.y);
const maxScale = Math.max(scaleX, scaleY);
if (maxScale > MAX_SCALE_FACTOR) {
return true;
}
return false;
};
function invertProjectivePoint(xp, yp, matrix) {
const A = matrix.m11 - xp * matrix.m14;
const B = matrix.m21 - xp * matrix.m24;
const C = xp * matrix.m44 - matrix.m41;
const D = matrix.m12 - yp * matrix.m14;
const E = matrix.m22 - yp * matrix.m24;
const F = yp * matrix.m44 - matrix.m42;
const det = A * E - B * D;
if (Math.abs(det) < 0.0000000001) {
return null;
}
const x = (C * E - B * F) / det;
const y = (A * F - C * D) / det;
return { x, y };
}
function getPreTransformRect(targetRect, matrix) {
if (isScaleTooBig(matrix)) {
return null;
}
const corners = [
{ x: targetRect.x, y: targetRect.y },
{ x: targetRect.x + targetRect.width, y: targetRect.y },
{ x: targetRect.x + targetRect.width, y: targetRect.y + targetRect.height },
{ x: targetRect.x, y: targetRect.y + targetRect.height }
];
const invertedCorners = [];
for (const corner of corners) {
const inverted = invertProjectivePoint(corner.x, corner.y, matrix);
if (inverted === null) {
return null;
}
invertedCorners.push(inverted);
}
const xCoords = invertedCorners.map((p) => p.x);
const yCoords = invertedCorners.map((p) => p.y);
return new DOMRect(Math.min(...xCoords), Math.min(...yCoords), Math.max(...xCoords) - Math.min(...xCoords), Math.max(...yCoords) - Math.min(...yCoords));
}
// src/drawing/transform-in-3d.ts
var vsSource = `
attribute vec2 aPosition;
attribute vec2 aTexCoord;
uniform mat4 uTransform;
uniform vec2 uResolution;
uniform vec2 uOffset;
varying vec2 vTexCoord;
void main() {
vec4 pos = uTransform * vec4(aPosition, 0.0, 1.0);
pos.xy = pos.xy + uOffset * pos.w;
// Convert homogeneous coords to clip space
gl_Position = vec4(
(pos.x / uResolution.x) * 2.0 - pos.w, // x
pos.w - (pos.y / uResolution.y) * 2.0, // y (flipped)
0.0,
pos.w
);
vTexCoord = aTexCoord;
}
`;
var fsSource = `
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vTexCoord;
void main() {
gl_FragColor = texture2D(uTexture, vTexCoord);
}
`;
function compileShader(shaderGl, source, type) {
const shader = shaderGl.createShader(type);
if (!shader) {
throw new Error("Could not create shader");
}
shaderGl.shaderSource(shader, source);
shaderGl.compileShader(shader);
if (!shaderGl.getShaderParameter(shader, shaderGl.COMPILE_STATUS)) {
const log = shaderGl.getShaderInfoLog(shader);
shaderGl.deleteShader(shader);
throw new Error("Shader compile error: " + log);
}
return shader;
}
var createHelperCanvas = ({
canvasWidth,
canvasHeight,
helperCanvasState
}) => {
if (helperCanvasState.current) {
if (helperCanvasState.current.canvas.width !== canvasWidth || helperCanvasState.current.canvas.height !== canvasHeight) {
helperCanvasState.current.canvas.width = canvasWidth;
helperCanvasState.current.canvas.height = canvasHeight;
}
helperCanvasState.current.gl.viewport(0, 0, canvasWidth, canvasHeight);
helperCanvasState.current.gl.clearColor(0, 0, 0, 0);
helperCanvasState.current.gl.clear(helperCanvasState.current.gl.COLOR_BUFFER_BIT);
return helperCanvasState.current;
}
const canvas = new OffscreenCanvas(canvasWidth, canvasHeight);
const gl = canvas.getContext("webgl", {
premultipliedAlpha: true
}) ?? undefined;
if (!gl) {
throw new Error("WebGL not supported");
}
const vertexShader = compileShader(gl, vsSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fsSource, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
if (!program) {
throw new Error("Could not create program");
}
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error("Program link error: " + gl.getProgramInfoLog(program));
}
const locations = {
aPosition: gl.getAttribLocation(program, "aPosition"),
aTexCoord: gl.getAttribLocation(program, "aTexCoord"),
uTransform: gl.getUniformLocation(program, "uTransform"),
uResolution: gl.getUniformLocation(program, "uResolution"),
uOffset: gl.getUniformLocation(program, "uOffset"),
uTexture: gl.getUniformLocation(program, "uTexture")
};
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
const cleanup = () => {
gl.deleteProgram(program);
const loseContext = gl.getExtension("WEBGL_lose_context");
if (loseContext) {
loseContext.loseContext();
}
};
helperCanvasState.current = { canvas, gl, program, locations, cleanup };
return helperCanvasState.current;
};
var transformIn3d = ({
matrix,
sourceCanvas,
sourceRect,
destRect,
internalState,
scale
}) => {
const { canvas, gl, program, locations } = createHelperCanvas({
canvasWidth: destRect.width,
canvasHeight: destRect.height,
helperCanvasState: internalState.helperCanvasState
});
gl.useProgram(program);
gl.viewport(0, 0, destRect.width, destRect.height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = new Float32Array([
sourceRect.x,
sourceRect.y,
sourceRect.x + sourceRect.width,
sourceRect.y,
sourceRect.x,
sourceRect.y + sourceRect.height,
sourceRect.x,
sourceRect.y + sourceRect.height,
sourceRect.x + sourceRect.width,
sourceRect.y,
sourceRect.x + sourceRect.width,
sourceRect.y + sourceRect.height
]);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
gl.enableVertexAttribArray(locations.aPosition);
gl.vertexAttribPointer(locations.aPosition, 2, gl.FLOAT, false, 0, 0);
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
const texCoords = new Float32Array([
0,
0,
1,
0,
0,
1,
0,
1,
1,
0,
1,
1
]);
gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);
gl.enableVertexAttribArray(locations.aTexCoord);
gl.vertexAttribPointer(locations.aTexCoord, 2, gl.FLOAT, false, 0, 0);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, sourceCanvas);
const actualMatrix = scale !== 1 ? new DOMMatrix().scale(scale, scale).multiply(matrix) : matrix;
const transformMatrix = actualMatrix.toFloat32Array();
gl.uniformMatrix4fv(locations.uTransform, false, transformMatrix);
gl.uniform2f(locations.uResolution, destRect.width, destRect.height);
gl.uniform2f(locations.uOffset, -destRect.x, -destRect.y);
gl.uniform1i(locations.uTexture, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.disableVertexAttribArray(locations.aPosition);
gl.disableVertexAttribArray(locations.aTexCoord);
gl.deleteTexture(texture);
gl.deleteBuffer(positionBuffer);
gl.deleteBuffer(texCoordBuffer);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.disable(gl.BLEND);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
return canvas;
};
// src/drawing/handle-3d-transform.ts
var getPrecomposeRectFor3DTransform = ({
element,
parentRect,
matrix
}) => {
const unclampedBiggestBoundingClientRect = getBiggestBoundingClientRect(element);
const biggestPossiblePretransformRect = getPreTransformRect(parentRect, matrix);
if (!biggestPossiblePretransformRect) {
return null;
}
const preTransformRect = getNarrowerRect({
firstRect: unclampedBiggestBoundingClientRect,
secondRect: biggestPossiblePretransformRect
});
return preTransformRect;
};
var handle3dTransform = ({
matrix,
sourceRect,
tempCanvas,
rectAfterTransforms,
internalState,
scale
}) => {
if (rectAfterTransforms.width <= 0 || rectAfterTransforms.height <= 0) {
return null;
}
const transformed = transformIn3d({
sourceRect,
matrix,
sourceCanvas: tempCanvas,
destRect: rectAfterTransforms,
internalState,
scale
});
return transformed;
};
// src/drawing/handle-mask.ts
var getPrecomposeRectForMask = (element) => {
const boundingRect = getBiggestBoundingClientRect(element);
return boundingRect;
};
var handleMask = ({
gradientInfo,
rect,
precomposeRect,
tempContext,
scale
}) => {
const rectToFill = new DOMRect((rect.left - precomposeRect.left) * scale, (rect.top - precomposeRect.top) * scale, rect.width * scale, rect.height * scale);
const gradient = createCanvasGradient({
ctx: tempContext,
rect: rectToFill,
gradientInfo,
offsetLeft: 0,
offsetTop: 0
});
tempContext.globalCompositeOperation = "destination-in";
tempContext.fillStyle = gradient;
tempContext.fillRect(rectToFill.left, rectToFill.top, rectToFill.width, rectToFill.height);
};
// src/drawing/scale-rect.ts
var scaleRect = ({
rect,
scale
}) => {
return new DOMRect(rect.x * scale, rect.y * scale, rect.width * scale, rect.height * scale);
};
// src/drawing/transform-rect-with-matrix.ts
function transformDOMRect({
rect,
matrix
}) {
const topLeft = new DOMPointReadOnly(rect.left, rect.top);
const topRight = new DOMPointReadOnly(rect.right, rect.top);
const bottomLeft = new DOMPointReadOnly(rect.left, rect.bottom);
const bottomRight = new DOMPointReadOnly(rect.right, rect.bottom);
const transformedTopLeft = topLeft.matrixTransform(matrix);
const transformedTopRight = topRight.matrixTransform(matrix);
const transformedBottomLeft = bottomLeft.matrixTransform(matrix);
const transformedBottomRight = bottomRight.matrixTransform(matrix);
const minX = Math.min(transformedTopLeft.x / transformedTopLeft.w, transformedTopRight.x / transformedTopRight.w, transformedBottomLeft.x / transformedBottomLeft.w, transformedBottomRight.x / transformedBottomRight.w);
const maxX = Math.max(transformedTopLeft.x / transformedTopLeft.w, transformedTopRight.x / transformedTopRight.w, transformedBottomLeft.x / transformedBottomLeft.w, transformedBottomRight.x / transformedBottomRight.w);
const minY = Math.min(transformedTopLeft.y / transformedTopLeft.w, transformedTopRight.y / transformedTopRight.w, transformedBottomLeft.y / transformedBottomLeft.w, transformedBottomRight.y / transformedBottomRight.w);
const maxY = Math.max(transformedTopLeft.y / transformedTopLeft.w, transformedTopRight.y / transformedTopRight.w, transformedBottomLeft.y / transformedBottomLeft.w, transformedBottomRight.y / transformedBottomRight.w);
return new DOMRect(minX, minY, maxX - minX, maxY - minY);
}
// src/drawing/process-node.ts
var processNode = async ({
element,
context,
draw,
logLevel,
parentRect,
internalState,
rootElement,
scale
}) => {
let __stack = [];
try {
const transforms = __using(__stack, calculateTransforms({
element,
rootElement
}), 0);
const { opacity, computedStyle, totalMatrix, dimensions, precompositing } = transforms;
if (opacity === 0) {
return { type: "skip-children" };
}
if (computedStyle.backfaceVisibility === "hidden" && totalMatrix.m33 < 0) {
return { type: "skip-children" };
}
if (dimensions.width <= 0 || dimensions.height <= 0) {
return { type: "continue", cleanupAfterChildren: null };
}
const rect = new DOMRect(dimensions.left - parentRect.x, dimensions.top - parentRect.y, dimensions.width, dimensions.height);
if (precompositing.needsPrecompositing) {
const start = Date.now();
let precomposeRect = null;
if (precompositing.needsMaskImage) {
precomposeRect = roundToExpandRect(getPrecomposeRectForMask(element));
}
if (precompositing.needs3DTransformViaWebGL) {
const tentativePrecomposeRect = getPrecomposeRectFor3DTransform({
element,
parentRect,
matrix: totalMatrix
});
if (!tentativePrecomposeRect) {
return { type: "continue", cleanupAfterChildren: null };
}
precomposeRect = roundToExpandRect(getWiderRectAndExpand({
firstRect: precomposeRect,
secondRect: tentativePrecomposeRect
}));
}
if (!precomposeRect) {
throw new Error("Precompose rect not found");
}
if (precomposeRect.width <= 0 || precomposeRect.height <= 0) {
return { type: "continue", cleanupAfterChildren: null };
}
if (!doRectsIntersect(precomposeRect, parentRect)) {
return { type: "continue", cleanupAfterChildren: null };
}
const tempContext = await createLayer({
cutout: precomposeRect,
element,
logLevel,
internalState,
scale,
onlyBackgroundClipText: false
});
let drawable = tempContext.canvas;
const rectAfterTransforms = roundToExpandRect(scaleRect({
scale,
rect: transformDOMRect({
rect: precomposeRect,
matrix: totalMatrix
})
}));
if (precompositing.needsMaskImage) {
handleMask({
gradientInfo: precompositing.needsMaskImage,
rect,
precomposeRect,
tempContext,
scale
});
}
if (precompositing.needs3DTransformViaWebGL) {
const t = handle3dTransform({
matrix: totalMatrix,
sourceRect: precomposeRect,
tempCanvas: drawable,
rectAfterTransforms,
internalState,
scale
});
if (t) {
drawable = t;
}
}
const previousTransform = context.getTransform();
context.setTransform(new DOMMatrix);
context.drawImage(drawable, 0, drawable.height - rectAfterTransforms.height, rectAfterTransforms.width, rectAfterTransforms.height, rectAfterTransforms.left - parentRect.x, rectAfterTransforms.top - parentRect.y, rectAfterTransforms.width, rectAfterTransforms.height);
context.setTransform(previousTransform);
Internals6.Log.trace({
logLevel,
tag: "@remotion/web-renderer"
}, `Transforming element in 3D - canvas size: ${precomposeRect.width}x${precomposeRect.height} - compose: ${Date.now() - start}ms - helper canvas: ${drawable.width}x${drawable.height}`);
internalState.addPrecompose({
canvasWidth: precomposeRect.width,
canvasHeight: precomposeRect.height
});
return { type: "skip-children" };
}
const { cleanupAfterChildren } = await drawElement({
rect,
computedStyle,
context,
draw,
opacity,
totalMatrix,
parentRect,
logLevel,
element,
internalState,
scale
});
return { type: "continue", cleanupAfterChildren };
} catch (_catch) {
var _err = _catch, _hasErr = 1;
} finally {
__callDispose(__stack, _err, _hasErr);
}
};
// src/drawing/text/draw-text.ts
import { Internals as Internals7 } from "remotion";
// src/drawing/text/apply-text-transform.ts
var applyTextTransform = (text, transform) => {
if (transform === "uppercase") {
return text.toUpperCase();
}
if (transform === "lowercase") {
return text.toLowerCase();
}
if (transform === "capitalize") {
return text.replace(/\b\w/g, (char) => char.toUpperCase());
}
return text;
};
// src/drawing/text/find-line-breaks.text.ts
var findWords = (span) => {
const originalText = span.textContent;
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const segments = segmenter.segment(span.textContent);
const words = Array.from(segments).map((s) => s.segment);
const tokens = [];
for (let i = 0;i < words.length; i++) {
const wordsBefore = words.slice(0, i);
const wordsAfter = words.slice(i + 1);
const word = words[i];
const wordsBeforeText = wordsBefore.join("");
const wordsAfterText = wordsAfter.join("");
const beforeNode = document.createTextNode(wordsBeforeText);
const afterNode = document.createTextNode(wordsAfterText);
const interstitialNode = document.createElement("span");
interstitialNode.textContent = word;
span.textContent = "";
span.appendChild(beforeNode);
span.appendChild(interstitialNode);
span.appendChild(afterNode);
const rect = interstitialNode.getBoundingClientRect();
span.textContent = originalText;
tokens.push({ text: word, rect });
}
return tokens;
};
// src/drawing/text/draw-text.ts
var drawText = ({
span,
logLevel,
onlyBackgroundClipText,
parentRect
}) => {
const drawFn = ({ computedStyle, contextToDraw }) => {
const {
fontFamily,
fontSize,
fontWeight,
direction,
writingMode,
letterSpacing,
textTransform,
webkitTextFillColor
} = computedStyle;
const isVertical = writingMode !== "horizontal-tb";
if (isVertical) {
Internals7.Log.warn({
logLevel,
tag: "@remotion/web-renderer"
}, 'Detected "writing-mode" CSS property. Vertical text is not yet supported in @remotion/web-renderer');
return;
}
contextToDraw.save();
const fontSizePx = parseFloat(fontSize);
contextToDraw.font = `${fontWeight} ${fontSizePx}px ${fontFamily}`;
contextToDraw.fillStyle = onlyBackgroundClipText ? "black" : webkitTextFillColor;
contextToDraw.letterSpacing = letterSpacing;
const isRTL = direction === "rtl";
contextToDraw.textAlign = isRTL ? "right" : "left";
contextToDraw.textBaseline = "alphabetic";
const originalText = span.textContent;
const transformedText = applyTextTransform(originalText, textTransform);
span.textContent = transformedText;
const tokens = findWords(span);
for (const token of tokens) {
const measurements = contextToDraw.measureText(originalText);
const { fontBoundingBoxDescent, fontBoundingBoxAscent } = measurements;
const fontHeight = fontBoundingBoxAscent + fontBoundingBoxDescent;
const leading = token.rect.height - fontHeight;
const halfLeading = leading / 2;
contextToDraw.fillText(token.text, (isRTL ? token.rect.right : token.rect.left) - parentRect.x, token.rect.top + fontBoundingBoxAscent + halfLeading - parentRect.y);
}
span.textContent = originalText;
contextToDraw.restore();
};
return drawFn;
};
// src/drawing/text/handle-text-node.ts
var handleTextNode = async ({
node,
context,
logLevel,
parentRect,
internalState,
rootElement,
onlyBackgroundClipText,
scale
}) => {
const span = document.createElement("span");
const parent = node.parentNode;
if (!parent) {
throw new Error("Text node has no parent");
}
parent.insertBefore(span, node);
span.appendChild(node);
const value = await processNode({
context,
element: span,
draw: drawText({ span, logLevel, onlyBackgroundClipText, parentRect }),
logLevel,
parentRect,
internalState,
rootElement,
scale
});
parent.insertBefore(node, span);
parent.removeChild(span);
return value;
};
// src/walk-over-node.ts
var walkOverNode = ({
node,
context,
logLevel,
parentRect,
internalState,
rootElement,
onlyBackgroundClipText,
scale
}) => {
if (node instanceof HTMLElement || node instanceof SVGElement) {
return processNode({
element: node,
context,
draw: drawDomElement(node),
logLevel,
parentRect,
internalState,
rootElement,
scale
});
}
if (node instanceof Text) {
return handleTextNode({
node,
context,
logLevel,
parentRect,
internalState,
rootElement,
onlyBackgroundClipText,
scale
});
}
throw new Error("Unknown node type");
};
// src/compose.ts
var getFilterFunction = (node) => {
if (!(node instanceof Element)) {
return NodeFilter.FILTER_ACCEPT;
}
if (node.parentElement instanceof SVGSVGElement) {
return NodeFilter.FILTER_REJECT;
}
const computedStyle = getComputedStyle(node);
if (computedStyle.display === "none") {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
};
var compose = async ({
element,
context,
logLevel,
parentRect,
internalState,
onlyBackgroundClipText,
scale
}) => {
let __stack = [];
try {
const treeWalker = document.createTreeWalker(element, onlyBackgroundClipText ? NodeFilter.SHOW_TEXT : NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, getFilterFunction);
if (onlyBackgroundClipText) {
treeWalker.nextNode();
if (!treeWalker.currentNode) {
return;
}
}
const treeWalkerClean = __using(__stack, createTreeWalkerCleanupAfterChildren(treeWalker), 0);
const { checkCleanUpAtBeginningOfIteration, addCleanup } = treeWalkerClean;
while (true) {
checkCleanUpAtBeginningOfIteration();
const val = await walkOverNode({
node: treeWalker.currentNode,
context,
logLevel,
parentRect,
internalState,
rootElement: element,
onlyBackgroundClipText,
scale
});
if (val.type === "skip-children") {
if (!skipToNextNonDescendant(treeWalker)) {
break;
}
} else {
if (val.cleanupAfterChildren) {
addCleanup(treeWalker.currentNode, val.cleanupAfterChildren);
}
if (!treeWalker.nextNode()) {
break;
}
}
}
} catch (_catch) {
var _err = _catch, _hasErr = 1;
} finally {
__callDispose(__stack, _err, _hasErr);
}
};
// src/take-screenshot.ts
var createLayer = async ({
element,
scale,
logLevel,
internalState,
onlyBackgroundClipText,
cutout
}) => {
const scaledWidth = Math.ceil(cutout.width * scale);
const scaledHeight = Math.ceil(cutout.height * scale);
const canvas = new OffscreenCanvas(scaledWidth, scaledHeight);
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Could not get context");
}
await compose({
element,
context,
logLevel,
parentRect: cutout,
internalState,
onlyBackgroundClipText,
scale
});
return context;
};
// src/throttle-progress.ts
var DEFAULT_THROTTLE_MS = 250;
var createThrottledProgressCallback = (callback, throttleMs = DEFAULT_THROTTLE_MS) => {
if (!callback) {
return null;
}
let lastCallTime = 0;
let pendingUpdate = null;
let timeoutId = null;
const throttled = (progress) => {
const now = Date.now();
const timeSinceLastCall = now - lastCallTime;
pendingUpdate = progress;
if (timeSinceLastCall >= throttleMs) {
lastCallTime = now;
callback(progress);
pendingUpdate = null;
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
} else if (timeoutId === null) {
const remainingTime = throttleMs - timeSinceLastCall;
timeoutId = setTimeout(() => {
if (pendingUpdate !== null) {
lastCallTime = Date.now();
callback(pendingUpdate);
pendingUpdate = null;
}
timeoutId = null;
}, remainingTime);
}
};
const cleanup = () => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
pendingUpdate = null;
};
return { throttled, [Symbol.dispose]: cleanup };
};
// src/validate-scale.ts
var validateScale = (scale) => {
if (typeof scale === "undefined") {
return;
}
if (typeof scale !== "number") {
throw new Error('Scale should be a number or undefined, but is "' + JSON.stringify(scale) + '"');
}
if (Number.isNaN(scale)) {
throw new Error("`scale` should not be NaN, but is NaN");
}
if (!Number.isFinite(scale)) {
throw new Error(`"scale" must be finite, but is ${scale}`);
}
if (scale <= 0) {
throw new Error(`"scale" must be bigger than 0, but is ${scale}`);
}
if (scale > 16) {
throw new Error(`"scale" must be smaller or equal than 16, but is ${scale}`);
}
};
// src/validate-video-frame.ts
var validateVideoFrame = ({
originalFrame,
returnedFrame,
expectedWidth,
expectedHeight,
expectedTimestamp
}) => {
if (!(returnedFrame instanceof VideoFrame)) {
originalFrame.close();
throw new Error("onFrame callback must return a VideoFrame or void");
}
if (returnedFrame === originalFrame) {
return returnedFrame;
}
if (returnedFrame.displayWidth !== expectedWidth || returnedFrame.displayHeight !== expectedHeight) {
originalFrame.close();
returnedFrame.close();
throw new Error(`VideoFrame dimensions mismatch: expected ${expectedWidth}x${expectedHeight}, got ${returnedFrame.displayWidth}x${returnedFrame.displayHeight}`);
}
if (returnedFrame.timestamp !== expectedTimestamp) {
originalFrame.close();
returnedFrame.close();
throw new Error(`VideoFrame timestamp mismatch: expected ${expectedTimestamp}, got ${returnedFrame.timestamp}`);
}
originalFrame.close();
return returnedFrame;
};
// src/with-resolvers.ts
var withResolvers = function() {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
// src/wait-for-ready.ts
var waitForReady = ({
timeoutInMilliseconds,
scope,
signal,
apiName,
internalState,
keepalive
}) => {
const start = performance.now();
const { promise, resolve, reject } = withResolvers();
let cancelled = false;
const check = () => {
if (cancelled) {
return;
}
if (signal?.aborted) {
cancelled = true;
internalState?.addWaitForReadyTime(performance.now() - start);
reject(new Error(`${apiName}() was cancelled`));
return;
}
if (scope.remotion_renderReady === true) {
internalState?.addWaitForReadyTime(performance.now() - start);
resolve();
return;
}
if (scope.remotion_cancelledError !== undefined) {
cancelled = true;
internalState?.addWaitForReadyTime(performance.now() - start);
const stack = scope.remotion_cancelledError;
const message = stack.split(`
`)[0].replace(/^Error: /, "");
const error = new Error(message);
error.stack = stack;
reject(error);
return;
}
if (performance.now() - start > timeoutInMilliseconds + 3000) {
cancelled = true;
internalState?.addWaitForReadyTime(performance.now() - start);
reject(new Error(Object.values(scope.remotion_delayRenderTimeouts).map((d) => d.label).join(", ")));
return;
}
scheduleNextCheck();
};
const scheduleNextCheck = () => {
const rafTick = new Promise((res) => {
requestAnimationFrame(() => res());
});
const backgroundSafeTick = keepalive ? Promise.race([rafTick, keepalive.waitForTick()]) : rafTick;
backgroundSafeTick.then(check);
};
check();
return promise;
};
// src/web-fs-target.ts
var sessionId = null;
var getPrefix = () => {
if (!sessionId) {
sessionId = crypto.randomUUID();
}
return `__remotion_render:${sessionId}:`;
};
var cleanupStaleOpfsFiles = async () => {
try {
const root = await navigator.storage.getDirectory();
for await (const [name] of root.entries()) {
if (name.startsWith("__remotion_render:") && !name.startsWith(getPrefix())) {
await root.removeEntry(name);
}
}
} catch {}
};
var createWebFsTarget = async () => {
const directoryHandle = await navigator.storage.getDirectory();
const filename = `${getPrefix()}${crypto.randomUUID()}`;
const fileHandle = await directoryHandle.getFileHandle(filename, {
create: true
});
const writable = await fileHandle.createWritable();
const stream = new WritableStream({
async write(chunk) {
await writable.seek(chunk.position);
await writable.write(chunk);
}
});
const getBlob = async () => {
const handle = await directoryHandle.getFileHandle(filename);
return handle.getFile();
};
const close = () => writable.close();
return { stream, getBlob, close };
};
// src/render-media-on-web.tsx
var internalRenderMediaOnWeb = async ({
composition,
inputProps,
delayRenderTimeoutInMilliseconds,
logLevel,
mediaCacheSizeInBytes,
schema,
videoCodec: codec,
audioCodec: unresolvedAudioCodec,
audioBitrate,
container,
signal,
onProgress,
hardwareAcceleration,
keyframeIntervalInSeconds,
videoBitrate,
frameRange,
transparent,
onArtifact,
onFrame,
outputTarget: userDesiredOutputTarget,
licenseKey,
muted,
scale,
isProduction
}) => {
let __stack2 = [];
try {
validateScale(scale);
const outputTarget = userDesiredOutputTarget === null ? await canUseWebFsWriter() ? "web-fs" : "arraybuffer" : userDesiredOutputTarget;
if (outputTarget === "web-fs") {
await cleanupStaleOpfsFiles();
}
const format = containerToMediabunnyContainer(container);
if (codec && !format.getSupportedCodecs().includes(codecToMediabunnyCodec(codec))) {
return Promise.reject(new Error(`Codec ${codec} is not supported for container ${container}`));
}
const resolvedAudioBitrate = typeof audioBitrate === "number" ? audioBitrate : getQualityForWebRendererQuality(audioBitrate);
let finalAudioCodec = null;
if (!muted) {
const audioResult = await resolveAudioCodec({
container,
requestedCodec: unresolvedAudioCodec,
userSpecifiedAudioCodec: unresolvedAudioCodec !== undefined && unresolvedAudioCodec !== null,
bitrate: resolvedAudioBitrate
});
for (const issue of audioResult.issues) {
if (issue.severity === "error") {
return Promise.reject(new Error(issue.message));
}
Internals8.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, issue.message);
}
finalAudioCodec = audioResult.codec;
}
const resolved = await Internals8.resolveVideoConfig({
calculateMetadata: composition.calculateMetadata ?? null,
signal: signal ?? new AbortController().signal,
defaultProps: composition.defaultProps ?? {},
inputProps: inputProps ?? {},
compositionId: composition.id,
compositionDurationInFrames: composition.durationInFrames ?? null,
compositionFps: composition.fps ?? null,
compositionHeight: composition.height ?? null,
compositionWidth: composition.width ?? null
});
const realFrameRange = getRealFrameRange(resolved.durationInFrames, frameRange);
if (signal?.aborted) {
return Promise.reject(new Error("renderMediaOnWeb() was cancelled"));
}
const scaffold = __using(__stack2, createScaffold({
width: resolved.width,
height: resolved.height,
fps: resolved.fps,
durationInFrames: resolved.durationInFrames,
Component: composition.component,
resolvedProps: resolved.props,
id: resolved.id,
delayRenderTimeoutInMilliseconds,
logLevel,
mediaCacheSizeInBytes,
schema: schema ?? null,
audioEnabled: !muted,
videoEnabled: true,
initialFrame: 0,
defaultCodec: resolved.defaultCodec,
defaultOutName: resolved.defaultOutName
}), 0);
const { delayRenderScope, div, timeUpdater, collectAssets, errorHolder } = scaffold;
const internalState = __using(__stack2, makeInternalState(), 0);
const keepalive = __using(__stack2, createBackgroundKeepalive({
fps: resolved.fps,
logLevel
}), 0);
const artifactsHandler = handleArtifacts();
const webFsTarget = outputTarget === "web-fs" ? await createWebFsTarget() : null;
const target = webFsTarget ? new StreamTarget(webFsTarget.stream) : new BufferTarget;
const outputWithCleanup = __using(__stack2, makeOutputWithCleanup({
format,
target
}), 0);
const throttledProgress = __using(__stack2, createThrottledProgressCallback(onProgress), 0);
const throttledOnProgress = throttledProgress?.throttled ?? null;
try {
let __stack = [];
try {
if (signal?.aborted) {
throw new Error("renderMediaOnWeb() was cancelled");
}
await waitForReady({
timeoutInMilliseconds: delayRenderTimeoutInMilliseconds,
scope: delayRenderScope,
signal,
apiName: "renderMediaOnWeb",
internalState,
keepalive
});
checkForError(errorHolder);
if (signal?.aborted) {
throw new Error("renderMediaOnWeb() was cancelled");
}
const videoSampleSource = __using(__stack, makeVideoSampleSourceCleanup({
codec: codecToMediabunnyCodec(codec),
bitrate: typeof videoBitrate === "number" ? videoBitrate : getQualityForWebRendererQuality(videoBitrate),
sizeChangeBehavior: "deny",
hardwareAcceleration,
latencyMode: "quality",
keyFrameInterval: keyframeIntervalInSeconds,
alpha: transparent ? "keep" : "discard"
}), 0);
outputWithCleanup.output.addVideoTrack(videoSampleSource.videoSampleSource);
const audioSampleSource = __using(__stack, createAudioSampleSource({
muted,
codec: finalAudioCodec ? audioCodecToMediabunnyAudioCodec(finalAudioCodec) : null,
bitrate: resolvedAudioBitrate
}), 0);
if (audioSampleSource) {
outputWithCleanup.output.addAudioTrack(audioSampleSource.audioSampleSource);
}
await outputWithCleanup.output.start();
if (signal?.aborted) {
throw new Error("renderMediaOnWeb() was cancelled");
}
const progress = {
renderedFrames: 0,
encodedFrames: 0
};
for (let frame = realFrameRange[0];frame <= realFrameRange[1]; frame++) {
if (signal?.aborted) {
throw new Error("renderMediaOnWeb() was cancelled");
}
timeUpdater.current?.update(frame);
await waitForReady({
timeoutInMilliseconds: delayRenderTimeoutInMilliseconds,
scope: delayRenderScope,
signal,
apiName: "renderMediaOnWeb",
keepalive,
internalState
});
checkForError(errorHolder);
if (signal?.aborted) {
throw new Error("renderMediaOnWeb() was cancelled");
}
const createFrameStart = performance.now();
const layer = await createLayer({
element: div,
scale,
logLevel,
internalState,
onlyBackgroundClipText: false,
cutout: new DOMRect(0, 0, resolved.width, resolved.height)
});
internalState.addCreateFrameTime(performance.now() - createFrameStart);
if (signal?.aborted) {
throw new Error("renderMediaOnWeb() was cancelled");
}
const timestamp = Math.round((frame - realFrameRange[0]) / resolved.fps * 1e6);
const videoFrame = new VideoFrame(layer.canvas, {
timestamp
});
progress.renderedFrames++;
throttledOnProgress?.({ ...progress });
let frameToEncode = videoFrame;
if (onFrame) {
const returnedFrame = await onFrame(videoFrame);
if (signal?.aborted) {
throw new Error("renderMediaOnWeb() was cancelled");
}
frameToEncode = validateVideoFrame({
originalFrame: videoFrame,
returnedFrame,
expectedWidth: Math.round(resolved.width * scale),
expectedHeight: Math.round(resolved.height * scale),
expectedTimestamp: timestamp
});
}
const audioCombineStart = performance.now();
const assets = collectAssets.current.collectAssets();
if (onArtifact) {
await artifactsHandler.handle({
imageData: layer.canvas,
frame,
assets,
onArtifact
});
}
if (signal?.aborted) {
throw new Error("renderMediaOnWeb() was cancelled");
}
const audio = muted ? null : onlyInlineAudio({ assets, fps: resolved.fps, timestamp });
internalState.addAudioMixingTime(performance.now() - audioCombineStart);
const addSampleStart = performance.now();
await Promise.all([
addVideoSampleAndCloseFrame(frameToEncode, videoSampleSource.videoSampleSource),
audio && audioSampleSource ? addAudioSample(audio, audioSampleSource.audioSampleSource) : Promise.resolve()
]);
internalState.addAddSampleTime(performance.now() - addSampleStart);
progress.encodedFrames++;
throttledOnProgress?.({ ...progress });
if (signal?.aborted) {
throw new Error("renderMediaOnWeb() was cancelled");
}
}
onProgress?.({ ...progress });
videoSampleSource.videoSampleSource.close();
audioSampleSource?.audioSampleSource.close();
await outputWithCleanup.output.finalize();
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`);
if (webFsTarget) {
sendUsageEvent({
licenseKey: licenseKey ?? null,
succeeded: true,
apiName: "renderMediaOnWeb",
isStill: false,
isProduction: isProduction ?? true
});
await webFsTarget.close();
return {
getBlob: () => {
return webFsTarget.getBlob();
},
internalState
};
}
if (!(target instanceof BufferTarget)) {
throw new Error("Expected target to be a BufferTarget");
}
sendUsageEvent({
licenseKey: licenseKey ?? null,
succeeded: true,
apiName: "renderMediaOnWeb",
isStill: false,
isProduction: isProduction ?? true
});
return {
getBlob: () => {
if (!target.buffer) {
throw new Error("The resulting buffer is empty");
}
return Promise.resolve(new Blob([target.buffer], { type: getMimeType(container) }));
},
internalState
};
} catch (_catch) {
var _err = _catch, _hasErr = 1;
} finally {
__callDispose(__stack, _err, _hasErr);
}
} catch (err) {
if (!signal?.aborted) {
sendUsageEvent({
succeeded: false,
licenseKey: licenseKey ?? null,
apiName: "renderMediaOnWeb",
isStill: false,
isProduction: isProduction ?? true
}).catch((err2) => {
Internals8.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
});
}
throw err;
}
} catch (_catch2) {
var _err2 = _catch2, _hasErr2 = 1;
} finally {
__callDispose(__stack2, _err2, _hasErr2);
}
};
var renderMediaOnWeb = (options) => {
const container = options.container ?? "mp4";
const codec = options.videoCodec ?? getDefaultVideoCodecForContainer(container);
onlyOneRenderAtATimeQueue.ref = onlyOneRenderAtATimeQueue.ref.catch(() => Promise.resolve()).then(() => internalRenderMediaOnWeb({
...options,
delayRenderTimeoutInMilliseconds: options.delayRenderTimeoutInMilliseconds ?? 30000,
logLevel: options.logLevel ?? window.remotion_logLevel ?? "info",
schema: options.schema ?? undefined,
mediaCacheSizeInBytes: options.mediaCacheSizeInBytes ?? null,
videoCodec: codec,
audioCodec: options.audioCodec ?? null,
audioBitrate: options.audioBitrate ?? "medium",
container,
signal: options.signal ?? null,
onProgress: options.onProgress ?? null,
hardwareAcceleration: options.hardwareAcceleration ?? "no-preference",
keyframeIntervalInSeconds: options.keyframeIntervalInSeconds ?? 5,
videoBitrate: options.videoBitrate ?? "medium",
frameRange: options.frameRange ?? null,
transparent: options.transparent ?? false,
onArtifact: options.onArtifact ?? null,
onFrame: options.onFrame ?? null,
outputTarget: options.outputTarget ?? null,
licenseKey: options.licenseKey ?? null,
muted: options.muted ?? false,
scale: options.scale ?? 1,
isProduction: options.isProduction ?? true
}));
return onlyOneRenderAtATimeQueue.ref;
};
// src/render-still-on-web.tsx
import {
Internals as Internals9
} from "remotion";
async function internalRenderStillOnWeb({
frame,
delayRenderTimeoutInMilliseconds,
logLevel,
inputProps,
schema,
imageFormat,
mediaCacheSizeInBytes,
composition,
signal,
onArtifact,
licenseKey,
scale,
isProduction
}) {
let __stack = [];
try {
validateScale(scale);
const resolved = await Internals9.resolveVideoConfig({
calculateMetadata: composition.calculateMetadata ?? null,
signal: signal ?? new AbortController().signal,
defaultProps: composition.defaultProps ?? {},
inputProps: inputProps ?? {},
compositionId: composition.id,
compositionDurationInFrames: composition.durationInFrames ?? null,
compositionFps: composition.fps ?? null,
compositionHeight: composition.height ?? null,
compositionWidth: composition.width ?? null
});
if (signal?.aborted) {
return Promise.reject(new Error("renderStillOnWeb() was cancelled"));
}
const internalState = __using(__stack, makeInternalState(), 0);
const scaffold = __using(__stack, createScaffold({
width: resolved.width,
height: resolved.height,
delayRenderTimeoutInMilliseconds,
logLevel,
resolvedProps: resolved.props,
id: resolved.id,
mediaCacheSizeInBytes,
audioEnabled: false,
Component: composition.component,
videoEnabled: true,
durationInFrames: resolved.durationInFrames,
fps: resolved.fps,
schema: schema ?? null,
initialFrame: frame,
defaultCodec: resolved.defaultCodec,
defaultOutName: resolved.defaultOutName
}), 0);
const { delayRenderScope, div, collectAssets, errorHolder } = scaffold;
const artifactsHandler = handleArtifacts();
try {
if (signal?.aborted) {
throw new Error("renderStillOnWeb() was cancelled");
}
await waitForReady({
timeoutInMilliseconds: delayRenderTimeoutInMilliseconds,
scope: delayRenderScope,
signal,
apiName: "renderStillOnWeb",
internalState: null,
keepalive: null
});
checkForError(errorHolder);
if (signal?.aborted) {
throw new Error("renderStillOnWeb() was cancelled");
}
const capturedFrame = await createLayer({
element: div,
scale,
logLevel,
internalState,
onlyBackgroundClipText: false,
cutout: new DOMRect(0, 0, resolved.width, resolved.height)
});
const imageData = await capturedFrame.canvas.convertToBlob({
type: `image/${imageFormat}`
});
const assets = collectAssets.current.collectAssets();
if (onArtifact) {
await artifactsHandler.handle({ imageData, frame, assets, onArtifact });
}
sendUsageEvent({
licenseKey: licenseKey ?? null,
succeeded: true,
apiName: "renderStillOnWeb",
isStill: true,
isProduction
});
return { blob: imageData, internalState };
} catch (err) {
if (!signal?.aborted) {
sendUsageEvent({
succeeded: false,
licenseKey: licenseKey ?? null,
apiName: "renderStillOnWeb",
isStill: true,
isProduction
}).catch((err2) => {
Internals9.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
});
}
throw err;
}
} catch (_catch) {
var _err = _catch, _hasErr = 1;
} finally {
__callDispose(__stack, _err, _hasErr);
}
}
var renderStillOnWeb = (options) => {
onlyOneRenderAtATimeQueue.ref = onlyOneRenderAtATimeQueue.ref.catch(() => Promise.resolve()).then(() => internalRenderStillOnWeb({
...options,
delayRenderTimeoutInMilliseconds: options.delayRenderTimeoutInMilliseconds ?? 30000,
logLevel: options.logLevel ?? window.remotion_logLevel ?? "info",
schema: options.schema ?? undefined,
mediaCacheSizeInBytes: options.mediaCacheSizeInBytes ?? null,
signal: options.signal ?? null,
onArtifact: options.onArtifact ?? null,
licenseKey: options.licenseKey ?? null,
scale: options.scale ?? 1,
isProduction: options.isProduction ?? true
}));
return onlyOneRenderAtATimeQueue.ref;
};
export {
renderStillOnWeb,
renderMediaOnWeb,
getSupportedVideoCodecsForContainer,
getSupportedAudioCodecsForContainer,
getEncodableVideoCodecs,
getEncodableAudioCodecs,
getDefaultVideoCodecForContainer,
getDefaultAudioCodecForContainer,
canRenderMediaOnWeb
};