remotion
Version:
Make videos programmatically
248 lines (247 loc) • 11.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CanvasImage = 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 Img_js_1 = require("../Img.js");
const prefetch_js_1 = require("../prefetch.js");
const sequence_field_schema_js_1 = require("../sequence-field-schema.js");
const Sequence_js_1 = require("../Sequence.js");
const SequenceContext_js_1 = require("../SequenceContext.js");
const use_buffer_state_js_1 = require("../use-buffer-state.js");
const use_delay_render_js_1 = require("../use-delay-render.js");
const wrap_in_schema_js_1 = require("../wrap-in-schema.js");
const canvasImageSchema = {
fit: {
type: 'enum',
default: 'fill',
description: 'Fit',
variants: {
fill: {},
contain: {},
cover: {},
},
},
...sequence_field_schema_js_1.sequenceVisualStyleSchema,
hidden: sequence_field_schema_js_1.hiddenField,
};
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, Img_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, Img_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 CanvasImageContent = (0, react_1.forwardRef)(({ src, width, height, fit = 'fill', effects, controls, onError, className, style, id, pauseWhenLoading, maxRetries = 2, delayRenderRetries, delayRenderTimeoutInMilliseconds, }, 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 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 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 (typeof ref === 'function') {
ref(canvas);
}
else if (ref) {
ref.current = canvas;
}
}, [ref]);
(0, react_1.useEffect)(() => {
if (!outputCanvas || !sourceCanvas) {
return;
}
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, Img_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 continued = false;
let errorCount = 0;
let timeoutId = null;
const continueRenderOnce = () => {
if (continued) {
return;
}
continued = true;
unblock();
continueRender(handle);
};
const attemptLoad = () => {
loadImage({ src: actualSrc, signal: controller.signal })
.then((image) => {
if (cancelled) {
return;
}
const canvasWidth = width !== null && width !== void 0 ? width : image.width;
const canvasHeight = height !== null && height !== void 0 ? height : image.height;
const sourceContext = sourceCanvas.getContext('2d', {
colorSpace: 'srgb',
});
if (!sourceContext) {
throw new Error('Could not get 2D context for <CanvasImage> source canvas');
}
sourceCanvas.width = canvasWidth;
sourceCanvas.height = canvasHeight;
outputCanvas.width = canvasWidth;
outputCanvas.height = canvasHeight;
sourceContext.clearRect(0, 0, canvasWidth, canvasHeight);
sourceContext.drawImage(image.element, ...(0, calculate_image_fit_js_1.calculateImageFit)(fit, { width: image.width, height: image.height }, { width: canvasWidth, height: canvasHeight }));
return (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) {
continueRenderOnce();
}
})
.catch((err) => {
if (err.name === 'AbortError') {
continueRenderOnce();
return;
}
errorCount++;
if (errorCount <= maxRetries) {
const backoff = exponentialBackoff(errorCount);
// eslint-disable-next-line no-console
console.warn(`Could not load <CanvasImage> with src="${(0, Img_js_1.truncateSrcForLabel)(actualSrc)}", retrying in ${backoff}ms`);
timeoutId = setTimeout(() => {
if (!cancelled) {
attemptLoad();
}
}, backoff);
}
else if (onError) {
onError(err);
continueRenderOnce();
}
else {
cancelRender(err);
}
});
};
attemptLoad();
return () => {
cancelled = true;
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
controller.abort();
continueRenderOnce();
};
}, [
actualSrc,
cancelRender,
chainState,
continueRender,
delayPlayback,
delayRender,
delayRenderRetries,
delayRenderTimeoutInMilliseconds,
fit,
height,
maxRetries,
memoizedEffects,
onError,
outputCanvas,
pauseWhenLoading,
sequenceContext === null || sequenceContext === void 0 ? void 0 : sequenceContext.postmounting,
sequenceContext === null || sequenceContext === void 0 ? void 0 : sequenceContext.premounting,
sourceCanvas,
width,
]);
return ((0, jsx_runtime_1.jsx)("canvas", { 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, hidden, name, showInTimeline, stack, _experimentalControls: controls, }, ref) => {
if (!src) {
throw new Error('No "src" prop was passed to <CanvasImage>.');
}
const memoizedEffectDefinitions = (0, use_memoized_effects_js_1.useMemoizedEffectDefinitions)(effects);
return ((0, jsx_runtime_1.jsx)(Sequence_js_1.Sequence, { layout: "none", from: from !== null && from !== void 0 ? from : 0, durationInFrames: durationInFrames !== null && durationInFrames !== void 0 ? durationInFrames : Infinity, hidden: hidden, showInTimeline: showInTimeline !== null && showInTimeline !== void 0 ? showInTimeline : true, name: name !== null && name !== void 0 ? name : '<CanvasImage>', _experimentalControls: controls, _remotionInternalEffects: memoizedEffectDefinitions, _remotionInternalIsMedia: { type: 'image', src }, _remotionInternalStack: stack, children: (0, jsx_runtime_1.jsx)(CanvasImageContent, { ref: ref, 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 }) }));
});
/*
* @description Renders a static image to a `<canvas>` and applies Remotion effects.
* @see [Documentation](https://www.remotion.dev/docs/canvasimage)
*/
exports.CanvasImage = (0, wrap_in_schema_js_1.wrapInSchema)(CanvasImageInner, canvasImageSchema);
exports.CanvasImage.displayName = 'CanvasImage';
(0, enable_sequence_stack_traces_js_1.addSequenceStackTraces)(exports.CanvasImage);