remotion
Version:
Make videos programmatically
253 lines (252 loc) • 13.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.HtmlInCanvas = exports.HTML_IN_CANVAS_UNSUPPORTED_MESSAGE = exports.isHtmlInCanvasSupported = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const delay_render_js_1 = require("./delay-render.js");
const run_effect_chain_js_1 = require("./effects/run-effect-chain.js");
const use_effect_chain_state_js_1 = require("./effects/use-effect-chain-state.js");
const use_memoized_effects_js_1 = require("./effects/use-memoized-effects.js");
const enable_sequence_stack_traces_js_1 = require("./enable-sequence-stack-traces.js");
const sequence_field_schema_js_1 = require("./sequence-field-schema.js");
const Sequence_js_1 = require("./Sequence.js");
const use_delay_render_js_1 = require("./use-delay-render.js");
const use_video_config_js_1 = require("./use-video-config.js");
const wrap_in_schema_js_1 = require("./wrap-in-schema.js");
// Memoize the support check across the session — neither the platform
// capability nor the chrome://flags toggle can change between calls.
// SSR results are not cached so the check runs again once `document` exists.
let cachedSupport = null;
const isHtmlInCanvasSupported = () => {
if (cachedSupport !== null) {
return cachedSupport;
}
if (typeof document === 'undefined') {
return false;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
cachedSupport =
typeof (ctx === null || ctx === void 0 ? void 0 : ctx.drawElementImage) === 'function' &&
typeof canvas.requestPaint === 'function' &&
typeof canvas.captureElementImage === 'function' &&
'transferControlToOffscreen' in HTMLCanvasElement.prototype;
return cachedSupport;
};
exports.isHtmlInCanvasSupported = isHtmlInCanvasSupported;
/** Shown when {@link isHtmlInCanvasSupported} is false: APIs are absent (old Chrome and/or flag off). */
exports.HTML_IN_CANVAS_UNSUPPORTED_MESSAGE = 'HTML in Canvas is not supported. Two common causes: Chrome is older than version 148 (update Chrome), or the HTML-in-Canvas flag is disabled at chrome://flags/#canvas-draw-element (enable it and restart Chrome).';
function assertHtmlInCanvasDimensions(width, height) {
if (typeof width !== 'number' || typeof height !== 'number') {
throw new Error(`HtmlInCanvas: \`width\` and \`height\` must be numbers. Received width=${String(width)}, height=${String(height)}.`);
}
if (!Number.isInteger(width) || width <= 0) {
throw new Error(`HtmlInCanvas: \`width\` must be a positive integer. Received: ${String(width)}.`);
}
if (!Number.isInteger(height) || height <= 0) {
throw new Error(`HtmlInCanvas: \`height\` must be a positive integer. Received: ${String(height)}.`);
}
}
const defaultOnPaint = ({ canvas, element, elementImage, }) => {
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to acquire 2D context for <HtmlInCanvas> canvas');
}
ctx.reset();
const transform = ctx.drawElementImage(elementImage, 0, 0);
element.style.transform = transform.toString();
};
/* eslint-enable react/require-default-props */
const HtmlInCanvasAncestorContext = (0, react_1.createContext)(false);
const HtmlInCanvasContent = (0, react_1.forwardRef)(({ width, height, effects, children, onPaint, onInit, controls, style }, ref) => {
var _a;
const isInsideAncestorHtmlInCanvas = (0, react_1.useContext)(HtmlInCanvasAncestorContext);
assertHtmlInCanvasDimensions(width, height);
const { continueRender, cancelRender } = (0, use_delay_render_js_1.useDelayRender)();
if (!(0, exports.isHtmlInCanvasSupported)()) {
cancelRender(new Error(exports.HTML_IN_CANVAS_UNSUPPORTED_MESSAGE));
}
const canvas2dRef = (0, react_1.useRef)(null);
const offscreenRef = (0, react_1.useRef)(null);
const divRef = (0, react_1.useRef)(null);
const canvasSizeKey = `${width}x${height}`;
const setLayoutCanvasRef = (0, react_1.useCallback)((node) => {
canvas2dRef.current = node;
if (typeof ref === 'function') {
ref(node);
}
else if (ref) {
ref.current =
node;
}
}, [ref]);
const chainState = (0, use_effect_chain_state_js_1.useEffectChainState)();
const memoizedEffects = (0, use_memoized_effects_js_1.useMemoizedEffects)({
effects,
overrideId: (_a = controls === null || controls === void 0 ? void 0 : controls.overrideId) !== null && _a !== void 0 ? _a : null,
});
// Refs so the paint handler always reads fresh values.
const effectsRef = (0, react_1.useRef)(memoizedEffects);
effectsRef.current = memoizedEffects;
const onPaintRef = (0, react_1.useRef)(onPaint);
onPaintRef.current = onPaint;
const onInitRef = (0, react_1.useRef)(onInit);
onInitRef.current = onInit;
const initializedRef = (0, react_1.useRef)(false);
const onInitCleanupRef = (0, react_1.useRef)(null);
const unmountedRef = (0, react_1.useRef)(false);
const onPaintCb = (0, react_1.useCallback)(async () => {
var _a;
const element = divRef.current;
if (!element) {
throw new Error('Canvas or scene element not found');
}
const offscreen = offscreenRef.current;
if (!offscreen) {
throw new Error('HtmlInCanvas: offscreen canvas not ready (transferControlToOffscreen failed or canvas is remounting)');
}
offscreen.width = width;
offscreen.height = height;
try {
const placeholderCanvas = canvas2dRef.current;
if (!placeholderCanvas) {
throw new Error('Canvas not found');
}
// `GPUQueue.copyElementImageToTexture` / related paths validate the
// linked offscreen surface has a rendering context. Acquire `2d` here
// before any capture or handler (must not call getContext on placeholder).
const offscreen2d = offscreen.getContext('2d');
if (!offscreen2d) {
throw new Error('Failed to acquire 2D context for <HtmlInCanvas> offscreen canvas');
}
const handle = (0, delay_render_js_1.delayRender)('onPaint');
if (!initializedRef.current) {
initializedRef.current = true;
// `onInit` may be async (e.g. WebGPU `requestAdapter`/`requestDevice`).
// Capture an `ElementImage` here only for `onInit` consumers — do NOT
// reuse it for the paint handler below, because awaiting `onInit`
// can invalidate the capture's paint context, leaving subsequent
// uploads (e.g. `copyElementImageToTexture`) failing with
// "No context found for ElementImage" on the very first paint.
const initImage = placeholderCanvas.captureElementImage(element);
const currentOnInit = onInitRef.current;
if (currentOnInit) {
const cleanup = await currentOnInit({
canvas: offscreen,
element,
elementImage: initImage,
});
if (typeof cleanup !== 'function') {
throw new Error('HtmlInCanvas: when `onInit` is provided, it must return a cleanup function, or a Promise that resolves to one.');
}
if (unmountedRef.current) {
cleanup();
}
else {
onInitCleanupRef.current = cleanup;
}
}
}
const handler = (_a = onPaintRef.current) !== null && _a !== void 0 ? _a : defaultOnPaint;
const elImage = placeholderCanvas.captureElementImage(element);
await handler({
canvas: offscreen,
element,
elementImage: elImage,
});
await (0, run_effect_chain_js_1.runEffectChain)({
state: chainState.get(width, height),
source: offscreen,
effects: effectsRef.current,
output: offscreen,
width,
height,
});
continueRender(handle);
}
catch (error) {
cancelRender(error);
}
}, [chainState, continueRender, cancelRender, width, height]);
// Transfer control once per layout canvas instance, then listen for paint on
// the placeholder (capture) while drawing on the linked offscreen surface.
(0, react_1.useLayoutEffect)(() => {
const placeholder = canvas2dRef.current;
if (!placeholder) {
throw new Error('Canvas not found');
}
placeholder.layoutSubtree = true;
const offscreen = placeholder.transferControlToOffscreen();
offscreenRef.current = offscreen;
offscreen.width = width;
offscreen.height = height;
initializedRef.current = false;
unmountedRef.current = false;
placeholder.addEventListener('paint', onPaintCb);
return () => {
var _a;
placeholder.removeEventListener('paint', onPaintCb);
offscreenRef.current = null;
initializedRef.current = false;
unmountedRef.current = true;
(_a = onInitCleanupRef.current) === null || _a === void 0 ? void 0 : _a.call(onInitCleanupRef);
onInitCleanupRef.current = null;
};
}, [onPaintCb, cancelRender, width, height]);
const onPaintChangedRef = (0, react_1.useRef)(false);
(0, react_1.useLayoutEffect)(() => {
var _a;
if (!onPaintChangedRef.current) {
onPaintChangedRef.current = true;
return;
}
const canvas = canvas2dRef.current;
if (!canvas) {
return;
}
(_a = canvas.requestPaint) === null || _a === void 0 ? void 0 : _a.call(canvas);
}, [onPaint, memoizedEffects]);
(0, react_1.useLayoutEffect)(() => {
const canvas = canvas2dRef.current;
if (!canvas) {
return;
}
const handle = (0, delay_render_js_1.delayRender)('waiting for first paint after canvas resize');
canvas.addEventListener('paint', () => {
continueRender(handle);
}, { once: true });
return () => {
continueRender(handle);
};
}, [width, height, continueRender, canvasSizeKey]);
const innerStyle = (0, react_1.useMemo)(() => {
return {
width,
height,
};
}, [width, height]);
if (isInsideAncestorHtmlInCanvas) {
throw new Error('<HtmlInCanvas> effects cannot be nested together. Chrome will only display the outer effect. Consider merging the effects into one if you can.');
}
return ((0, jsx_runtime_1.jsx)(HtmlInCanvasAncestorContext.Provider, { value: true, children: (0, jsx_runtime_1.jsx)("canvas", { ref: setLayoutCanvasRef, width: width, height: height, style: style, children: (0, jsx_runtime_1.jsx)("div", { ref: divRef, style: innerStyle, children: children }) }, canvasSizeKey) }));
});
HtmlInCanvasContent.displayName = 'HtmlInCanvasContent';
const HtmlInCanvasInner = (0, react_1.forwardRef)(({ width, height, effects = [], children, onPaint, onInit, _experimentalControls: controls, style, durationInFrames, name, ...sequenceProps }, ref) => {
const { durationInFrames: videoDuration } = (0, use_video_config_js_1.useVideoConfig)();
const resolvedDuration = durationInFrames !== null && durationInFrames !== void 0 ? durationInFrames : videoDuration;
const memoizedEffectDefinitions = (0, use_memoized_effects_js_1.useMemoizedEffectDefinitions)(effects);
return ((0, jsx_runtime_1.jsx)(Sequence_js_1.Sequence, { durationInFrames: resolvedDuration, name: name !== null && name !== void 0 ? name : '<HtmlInCanvas>', _remotionInternalDocumentationLink: name === undefined
? 'https://www.remotion.dev/docs/remotion/html-in-canvas'
: undefined, _experimentalControls: controls, _remotionInternalEffects: memoizedEffectDefinitions, layout: "none", ...sequenceProps, children: (0, jsx_runtime_1.jsx)(HtmlInCanvasContent, { ref: ref, width: width, height: height, effects: effects, onPaint: onPaint, onInit: onInit, controls: controls, style: style, children: children }) }));
});
HtmlInCanvasInner.displayName = 'HtmlInCanvas';
const htmlInCanvasSchema = {
...sequence_field_schema_js_1.sequenceVisualStyleSchema,
hidden: sequence_field_schema_js_1.hiddenField,
};
const HtmlInCanvasWrapped = (0, wrap_in_schema_js_1.wrapInSchema)(HtmlInCanvasInner, htmlInCanvasSchema);
exports.HtmlInCanvas = Object.assign(HtmlInCanvasWrapped, {
isSupported: exports.isHtmlInCanvasSupported,
});
exports.HtmlInCanvas.displayName = 'HtmlInCanvas';
(0, enable_sequence_stack_traces_js_1.addSequenceStackTraces)(exports.HtmlInCanvas);