remotion
Version:
Make videos programmatically
330 lines (329 loc) • 15 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CanvasImage = exports.canvasImageSchema = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const calculate_image_fit_js_1 = require("../calculate-image-fit.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 prefetch_js_1 = require("../prefetch.js");
const Sequence_js_1 = require("../Sequence.js");
const SequenceContext_js_1 = require("../SequenceContext.js");
const truncate_src_for_label_js_1 = require("../truncate-src-for-label.js");
const use_buffer_state_js_1 = require("../use-buffer-state.js");
const use_delay_render_js_1 = require("../use-delay-render.js");
const with_interactivity_schema_js_1 = require("../with-interactivity-schema.js");
exports.canvasImageSchema = {
...interactivity_schema_js_1.baseSchema,
fit: {
type: 'enum',
default: 'fill',
description: 'Fit',
variants: {
fill: {},
contain: {},
cover: {},
},
},
...interactivity_schema_js_1.transformSchema,
};
const makeAbortError = () => {
if (typeof DOMException !== 'undefined') {
return new DOMException('Image loading was aborted', 'AbortError');
}
const error = new Error('Image loading was aborted');
error.name = 'AbortError';
return error;
};
const loadImage = ({ src, signal, }) => {
return new Promise((resolve, reject) => {
const image = new Image();
let settled = false;
function cleanup() {
image.onload = null;
image.onerror = null;
}
function settle(callback) {
if (settled) {
return;
}
settled = true;
cleanup();
callback();
}
function onAbort() {
settle(() => reject(makeAbortError()));
}
image.onload = () => {
var _a;
Promise.resolve((_a = image.decode) === null || _a === void 0 ? void 0 : _a.call(image))
.catch(() => undefined)
.then(() => {
const imageWidth = image.naturalWidth || image.width;
const imageHeight = image.naturalHeight || image.height;
if (imageWidth <= 0 || imageHeight <= 0) {
settle(() => reject(new Error(`Could not determine dimensions for <CanvasImage> with src="${(0, truncate_src_for_label_js_1.truncateSrcForLabel)(src)}"`)));
return;
}
settle(() => resolve({ element: image, width: imageWidth, height: imageHeight }));
});
};
image.onerror = () => {
settle(() => reject(new Error(`Could not load <CanvasImage> with src="${(0, truncate_src_for_label_js_1.truncateSrcForLabel)(src)}"`)));
};
signal.addEventListener('abort', onAbort, { once: true });
if (signal.aborted) {
onAbort();
return;
}
image.crossOrigin = 'anonymous';
image.src = src;
});
};
function exponentialBackoff(errorCount) {
return 1000 * 2 ** (errorCount - 1);
}
const waitForNextFrame = ({ onFrame, }) => {
if (typeof requestAnimationFrame === 'undefined') {
onFrame();
return () => undefined;
}
const frame = requestAnimationFrame(onFrame);
return () => cancelAnimationFrame(frame);
};
const CanvasImageContent = (0, react_1.forwardRef)(({ src, width, height, fit = 'fill', effects, controls, onError, className, style, id, pauseWhenLoading, maxRetries = 2, delayRenderRetries, delayRenderTimeoutInMilliseconds, refForOutline, ...canvasProps }, ref) => {
var _a;
const { delayRender, continueRender, cancelRender } = (0, use_delay_render_js_1.useDelayRender)();
const { delayPlayback } = (0, use_buffer_state_js_1.useBufferState)();
const [outputCanvas, setOutputCanvas] = (0, react_1.useState)(null);
const [loadedImage, setLoadedImage] = (0, react_1.useState)(null);
const actualSrc = (0, prefetch_js_1.usePreload)(src);
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,
});
const sequenceContext = (0, react_1.useContext)(SequenceContext_js_1.SequenceContext);
const pendingLoadDelayRef = (0, react_1.useRef)(null);
const continuePendingLoadDelay = (0, react_1.useCallback)(() => {
const pending = pendingLoadDelayRef.current;
if (!pending || pending.continued) {
return;
}
pending.continued = true;
pending.unblock();
continueRender(pending.handle);
pendingLoadDelayRef.current = null;
}, [continueRender]);
const sourceCanvas = (0, react_1.useMemo)(() => {
if (typeof document === 'undefined') {
return null;
}
return document.createElement('canvas');
}, []);
const canvasRef = (0, react_1.useCallback)((canvas) => {
setOutputCanvas(canvas);
if (refForOutline) {
refForOutline.current = canvas;
}
if (typeof ref === 'function') {
ref(canvas);
}
else if (ref) {
ref.current = canvas;
}
}, [ref, refForOutline]);
(0, react_1.useLayoutEffect)(() => {
const isPremounting = Boolean(sequenceContext === null || sequenceContext === void 0 ? void 0 : sequenceContext.premounting);
const isPostmounting = Boolean(sequenceContext === null || sequenceContext === void 0 ? void 0 : sequenceContext.postmounting);
const handle = delayRender(`Rendering <CanvasImage> with src="${(0, truncate_src_for_label_js_1.truncateSrcForLabel)(actualSrc)}"`, {
retries: delayRenderRetries !== null && delayRenderRetries !== void 0 ? delayRenderRetries : undefined,
timeoutInMilliseconds: delayRenderTimeoutInMilliseconds !== null && delayRenderTimeoutInMilliseconds !== void 0 ? delayRenderTimeoutInMilliseconds : undefined,
});
const unblock = pauseWhenLoading && !isPremounting && !isPostmounting
? delayPlayback().unblock
: () => undefined;
const controller = new AbortController();
let cancelled = false;
let errorCount = 0;
let timeoutId = null;
setLoadedImage(null);
pendingLoadDelayRef.current = {
handle,
unblock,
continued: false,
};
const attemptLoad = () => {
loadImage({ src: actualSrc, signal: controller.signal })
.then((image) => {
if (cancelled) {
return;
}
setLoadedImage(image);
})
.catch((err) => {
if (err.name === 'AbortError') {
continuePendingLoadDelay();
return;
}
errorCount++;
if (errorCount <= maxRetries) {
const backoff = exponentialBackoff(errorCount);
// eslint-disable-next-line no-console
console.warn(`Could not load <CanvasImage> with src="${(0, truncate_src_for_label_js_1.truncateSrcForLabel)(actualSrc)}", retrying in ${backoff}ms`);
timeoutId = setTimeout(() => {
if (!cancelled) {
attemptLoad();
}
}, backoff);
}
else if (onError) {
onError(err);
continuePendingLoadDelay();
}
else {
cancelRender(err);
}
});
};
attemptLoad();
return () => {
cancelled = true;
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
controller.abort();
continuePendingLoadDelay();
};
}, [
actualSrc,
cancelRender,
continuePendingLoadDelay,
delayPlayback,
delayRender,
delayRenderRetries,
delayRenderTimeoutInMilliseconds,
maxRetries,
onError,
pauseWhenLoading,
sequenceContext === null || sequenceContext === void 0 ? void 0 : sequenceContext.postmounting,
sequenceContext === null || sequenceContext === void 0 ? void 0 : sequenceContext.premounting,
]);
(0, react_1.useLayoutEffect)(() => {
if (!loadedImage || !outputCanvas || !sourceCanvas) {
return;
}
const handle = delayRender(`Applying effects to <CanvasImage> with src="${(0, truncate_src_for_label_js_1.truncateSrcForLabel)(actualSrc)}"`);
let cancelled = false;
let continued = false;
let cancelWaitForNextFrame = () => undefined;
const continueRenderOnce = () => {
if (continued) {
return;
}
continued = true;
continueRender(handle);
};
const canvasWidth = width !== null && width !== void 0 ? width : loadedImage.width;
const canvasHeight = height !== null && height !== void 0 ? height : loadedImage.height;
const sourceContext = sourceCanvas.getContext('2d', {
colorSpace: 'srgb',
});
if (!sourceContext) {
cancelRender(new Error('Could not get 2D context for <CanvasImage> source canvas'));
continueRenderOnce();
return () => {
continueRenderOnce();
};
}
sourceCanvas.width = canvasWidth;
sourceCanvas.height = canvasHeight;
outputCanvas.width = canvasWidth;
outputCanvas.height = canvasHeight;
sourceContext.clearRect(0, 0, canvasWidth, canvasHeight);
sourceContext.drawImage(loadedImage.element, ...(0, calculate_image_fit_js_1.calculateImageFit)(fit, { width: loadedImage.width, height: loadedImage.height }, { width: canvasWidth, height: canvasHeight }));
(0, run_effect_chain_js_1.runEffectChain)({
state: chainState.get(canvasWidth, canvasHeight),
source: sourceCanvas,
effects: memoizedEffects,
output: outputCanvas,
width: canvasWidth,
height: canvasHeight,
})
.then((completed) => {
if (completed && !cancelled) {
cancelWaitForNextFrame = waitForNextFrame({
onFrame: () => {
if (cancelled) {
return;
}
continueRenderOnce();
continuePendingLoadDelay();
},
});
}
})
.catch((err) => {
if (cancelled) {
return;
}
if (onError) {
onError(err);
continueRenderOnce();
continuePendingLoadDelay();
}
else {
cancelRender(err);
}
});
return () => {
cancelled = true;
cancelWaitForNextFrame();
continueRenderOnce();
};
}, [
actualSrc,
cancelRender,
chainState,
continueRender,
continuePendingLoadDelay,
delayRender,
fit,
height,
loadedImage,
memoizedEffects,
onError,
outputCanvas,
sourceCanvas,
width,
]);
return (jsx_runtime_1.jsx("canvas", { ...canvasProps, ref: canvasRef, width: width, height: height, className: className, style: style, id: id }));
});
CanvasImageContent.displayName = 'CanvasImageContent';
const CanvasImageInner = (0, react_1.forwardRef)(({ src, width, height, fit, effects = [], className, style, id, onError, pauseWhenLoading, maxRetries, delayRenderRetries, delayRenderTimeoutInMilliseconds, durationInFrames, from, trimBefore, freeze, hidden, name, showInTimeline, stack, controls, _remotionInternalDocumentationLink, outlineRef, ...canvasProps }, ref) => {
if (!src) {
throw new Error('No "src" prop was passed to <CanvasImage>.');
}
const memoizedEffectDefinitions = (0, use_memoized_effects_js_1.useMemoizedEffectDefinitions)(effects);
const actualRef = (0, react_1.useRef)(null);
(0, react_1.useImperativeHandle)(ref, () => {
return actualRef.current;
}, []);
return (jsx_runtime_1.jsx(Sequence_js_1.Sequence, { layout: "none", from: from !== null && from !== void 0 ? from : 0, trimBefore: trimBefore, durationInFrames: durationInFrames !== null && durationInFrames !== void 0 ? durationInFrames : Infinity, freeze: freeze, hidden: hidden, showInTimeline: showInTimeline !== null && showInTimeline !== void 0 ? showInTimeline : true, name: name !== null && name !== void 0 ? name : '<CanvasImage>', _remotionInternalDocumentationLink: _remotionInternalDocumentationLink !== null && _remotionInternalDocumentationLink !== void 0 ? _remotionInternalDocumentationLink : 'https://www.remotion.dev/docs/canvasimage', controls: controls, _remotionInternalEffects: memoizedEffectDefinitions, _remotionInternalIsMedia: { type: 'image', src }, _remotionInternalStack: stack, outlineRef: outlineRef !== null && outlineRef !== void 0 ? outlineRef : actualRef, children: jsx_runtime_1.jsx(CanvasImageContent, { ref: actualRef, src: src, width: width, height: height, fit: fit, effects: effects, controls: controls, className: className, style: style, id: id, onError: onError, pauseWhenLoading: pauseWhenLoading, maxRetries: maxRetries, delayRenderRetries: delayRenderRetries, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds, refForOutline: outlineRef !== null && outlineRef !== void 0 ? outlineRef : null, ...canvasProps }) }));
});
/*
* @description Renders a static image to a `<canvas>` and applies Remotion effects.
* @see [Documentation](https://www.remotion.dev/docs/canvasimage)
*/
exports.CanvasImage = (0, with_interactivity_schema_js_1.withInteractivitySchema)({
Component: CanvasImageInner,
componentName: '<CanvasImage>',
componentIdentity: 'dev.remotion.remotion.CanvasImage',
schema: exports.canvasImageSchema,
supportsEffects: true,
});
exports.CanvasImage.displayName = 'CanvasImage';
(0, enable_sequence_stack_traces_js_1.addSequenceStackTraces)(exports.CanvasImage);