UNPKG

remotion

Version:

Make videos programmatically

354 lines (353 loc) 16.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HtmlInCanvas = exports.htmlInCanvasSchema = 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 interactivity_schema_js_1 = require("./interactivity-schema.js"); const Sequence_js_1 = require("./Sequence.js"); const use_delay_render_js_1 = require("./use-delay-render.js"); const use_remotion_environment_js_1 = require("./use-remotion-environment.js"); const use_video_config_js_1 = require("./use-video-config.js"); const with_interactivity_schema_js_1 = require("./with-interactivity-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)}.`); } } function resolveHtmlInCanvasPixelDensity(pixelDensity) { if (pixelDensity === undefined) { return 1; } if (typeof pixelDensity !== 'number' || !Number.isFinite(pixelDensity) || pixelDensity <= 0) { throw new Error(`HtmlInCanvas: \`pixelDensity\` must be a positive finite number. Received: ${String(pixelDensity)}.`); } return pixelDensity; } const isMissingPaintRecordError = (error) => { return error instanceof DOMException && error.name === 'InvalidStateError'; }; const missingPaintRecordMessage = 'HtmlInCanvas: Expected the element to be inside the viewport during rendering, but Chrome had no cached paint record for it.'; const resizeOffscreenCanvas = ({ offscreen, width, height, }) => { if (offscreen.width !== width) { offscreen.width = width; } if (offscreen.height !== height) { offscreen.height = 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, pixelDensity, controls, style, }, ref) => { var _a; const isInsideAncestorHtmlInCanvas = (0, react_1.useContext)(HtmlInCanvasAncestorContext); assertHtmlInCanvasDimensions(width, height); const resolvedPixelDensity = resolveHtmlInCanvasPixelDensity(pixelDensity); const canvasWidth = Math.ceil(width * resolvedPixelDensity); const canvasHeight = Math.ceil(height * resolvedPixelDensity); const { continueRender, cancelRender } = (0, use_delay_render_js_1.useDelayRender)(); const { isRendering } = (0, use_remotion_environment_js_1.useRemotionEnvironment)(); 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}@${resolvedPixelDensity}`; 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)'); } resizeOffscreenCanvas({ offscreen, width: canvasWidth, height: canvasHeight, }); try { const placeholderCanvas = canvas2dRef.current; if (!placeholderCanvas) { throw new Error('Canvas not found'); } const handle = (0, delay_render_js_1.delayRender)('onPaint'); if (!initializedRef.current) { // `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. let initImage = null; try { initImage = placeholderCanvas.captureElementImage(element); } catch (error) { if (isMissingPaintRecordError(error) && !isRendering) { // Element may be outside viewport (no cached paint record). // Skip init — will retry on the next paint cycle. } else if (isMissingPaintRecordError(error)) { throw new Error(missingPaintRecordMessage); } else { throw error; } } if (initImage) { initializedRef.current = true; const currentOnInit = onInitRef.current; if (currentOnInit) { const cleanup = await currentOnInit({ canvas: offscreen, element, elementImage: initImage, pixelDensity: resolvedPixelDensity, }); 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; let elImage; try { elImage = placeholderCanvas.captureElementImage(element); } catch (error) { // `captureElementImage` throws `InvalidStateError` when the // element is outside the viewport (no cached paint record). // Skip this paint cycle — the canvas retains its last state. if (isMissingPaintRecordError(error) && !isRendering) { continueRender(handle); return; } if (isMissingPaintRecordError(error)) { throw new Error(missingPaintRecordMessage); } throw error; } await handler({ canvas: offscreen, element, elementImage: elImage, pixelDensity: resolvedPixelDensity, }); await (0, run_effect_chain_js_1.runEffectChain)({ state: chainState.get(canvasWidth, canvasHeight), source: offscreen, effects: effectsRef.current, output: offscreen, width: canvasWidth, height: canvasHeight, }); continueRender(handle); } catch (error) { cancelRender(error); } }, [ canvasHeight, canvasWidth, chainState, continueRender, cancelRender, resolvedPixelDensity, isRendering, ]); // 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; resizeOffscreenCanvas({ offscreen, width: canvasWidth, height: canvasHeight, }); 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, canvasWidth, canvasHeight]); 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]); const canvasStyle = (0, react_1.useMemo)(() => { return { width, height, ...(style !== null && style !== void 0 ? style : {}), }; }, [height, style, width]); 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 (jsx_runtime_1.jsx(HtmlInCanvasAncestorContext.Provider, { value: true, children: jsx_runtime_1.jsx("canvas", { ref: setLayoutCanvasRef, width: canvasWidth, height: canvasHeight, style: canvasStyle, children: 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, pixelDensity, 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); const actualRef = (0, react_1.useRef)(null); const setCanvasRef = (0, react_1.useCallback)((node) => { actualRef.current = node; if (typeof ref === 'function') { ref(node); } else if (ref) { ref.current = node; } }, [ref]); return (jsx_runtime_1.jsx(Sequence_js_1.Sequence, { durationInFrames: resolvedDuration, name: name !== null && name !== void 0 ? name : '<HtmlInCanvas>', _remotionInternalDocumentationLink: "https://www.remotion.dev/docs/remotion/html-in-canvas", controls: controls, _remotionInternalEffects: memoizedEffectDefinitions, outlineRef: actualRef, layout: "none", ...sequenceProps, children: jsx_runtime_1.jsx(HtmlInCanvasContent, { ref: setCanvasRef, width: width, height: height, effects: effects, onPaint: onPaint, onInit: onInit, pixelDensity: pixelDensity, controls: controls, style: style, children: children }) })); }); HtmlInCanvasInner.displayName = 'HtmlInCanvas'; exports.htmlInCanvasSchema = { ...interactivity_schema_js_1.baseSchema, pixelDensity: { type: 'number', min: 1, max: 3, step: 0.1, default: 1, description: 'Pixel density', hiddenFromList: false, }, ...interactivity_schema_js_1.transformSchema, }; const HtmlInCanvasWrapped = (0, with_interactivity_schema_js_1.withInteractivitySchema)({ Component: HtmlInCanvasInner, componentName: '<HtmlInCanvas>', componentIdentity: 'dev.remotion.remotion.HtmlInCanvas', schema: exports.htmlInCanvasSchema, supportsEffects: true, }); exports.HtmlInCanvas = Object.assign(HtmlInCanvasWrapped, { isSupported: exports.isHtmlInCanvasSupported, }); exports.HtmlInCanvas.displayName = 'HtmlInCanvas'; (0, enable_sequence_stack_traces_js_1.addSequenceStackTraces)(exports.HtmlInCanvas);