UNPKG

remotion

Version:

Make videos programmatically

253 lines (252 loc) 13.1 kB
"use strict"; 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);