remotion
Version:
Make videos programmatically
269 lines (268 loc) • 14.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Img = exports.imgSchema = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const index_js_1 = require("./canvas-image/index.js");
const enable_sequence_stack_traces_js_1 = require("./enable-sequence-stack-traces.js");
const get_cross_origin_value_js_1 = require("./get-cross-origin-value.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 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 use_remotion_environment_js_1 = require("./use-remotion-environment.js");
const wrap_in_schema_js_1 = require("./wrap-in-schema.js");
function exponentialBackoff(errorCount) {
return 1000 * 2 ** (errorCount - 1);
}
const ImgContent = ({ onError, maxRetries = 2, src, pauseWhenLoading, delayRenderRetries, delayRenderTimeoutInMilliseconds, onImageFrame, crossOrigin, decoding, ref, refForOutline, ...props }) => {
const imageRef = (0, react_1.useRef)(null);
const errors = (0, react_1.useRef)({});
const { delayPlayback } = (0, use_buffer_state_js_1.useBufferState)();
const sequenceContext = (0, react_1.useContext)(SequenceContext_js_1.SequenceContext);
const _propsValid = true;
if (!_propsValid) {
throw new Error('typecheck error');
}
const imageCallbackRef = (0, react_1.useCallback)((img) => {
imageRef.current = img;
refForOutline.current = img;
if (typeof ref === 'function') {
ref(img);
}
else if (ref) {
ref.current = img;
}
}, [ref, refForOutline]);
const actualSrc = (0, prefetch_js_1.usePreload)(src);
const retryIn = (0, react_1.useCallback)((timeout) => {
if (!imageRef.current) {
return;
}
const currentSrc = imageRef.current.src;
setTimeout(() => {
var _a;
if (!imageRef.current) {
// Component has been unmounted, do not retry
return;
}
const newSrc = (_a = imageRef.current) === null || _a === void 0 ? void 0 : _a.src;
if (newSrc !== currentSrc) {
// src has changed, do not retry
return;
}
imageRef.current.removeAttribute('src');
imageRef.current.setAttribute('src', newSrc);
}, timeout);
}, []);
const { delayRender, continueRender, cancelRender } = (0, use_delay_render_js_1.useDelayRender)();
const didGetError = (0, react_1.useCallback)((e) => {
var _a, _b, _c, _d, _e, _f, _g;
var _h, _j, _k, _l;
if (!errors.current) {
return;
}
errors.current[(_a = imageRef.current) === null || _a === void 0 ? void 0 : _a.src] =
((_h = errors.current[(_b = imageRef.current) === null || _b === void 0 ? void 0 : _b.src]) !== null && _h !== void 0 ? _h : 0) + 1;
if (onError &&
((_j = errors.current[(_c = imageRef.current) === null || _c === void 0 ? void 0 : _c.src]) !== null && _j !== void 0 ? _j : 0) > maxRetries) {
onError(e);
return;
}
if (((_k = errors.current[(_d = imageRef.current) === null || _d === void 0 ? void 0 : _d.src]) !== null && _k !== void 0 ? _k : 0) <= maxRetries) {
const backoff = exponentialBackoff((_l = errors.current[(_e = imageRef.current) === null || _e === void 0 ? void 0 : _e.src]) !== null && _l !== void 0 ? _l : 0);
// eslint-disable-next-line no-console
console.warn(`Could not load image with source ${(0, truncate_src_for_label_js_1.truncateSrcForLabel)((_f = imageRef.current) === null || _f === void 0 ? void 0 : _f.src)}, retrying again in ${backoff}ms`);
retryIn(backoff);
return;
}
try {
cancelRender('Error loading image with src: ' +
(0, truncate_src_for_label_js_1.truncateSrcForLabel)((_g = imageRef.current) === null || _g === void 0 ? void 0 : _g.src));
}
catch (_m) {
// cancelRender() intentionally throws after storing the error in scope.
// In async image callbacks, we rely on the stored error for renderer propagation.
}
}, [cancelRender, maxRetries, onError, retryIn]);
if (typeof window !== 'undefined') {
const isPremounting = Boolean(sequenceContext === null || sequenceContext === void 0 ? void 0 : sequenceContext.premounting);
const isPostmounting = Boolean(sequenceContext === null || sequenceContext === void 0 ? void 0 : sequenceContext.postmounting);
// eslint-disable-next-line react-hooks/rules-of-hooks
(0, react_1.useLayoutEffect)(() => {
var _a, _b;
if (((_b = (_a = window.process) === null || _a === void 0 ? void 0 : _a.env) === null || _b === void 0 ? void 0 : _b.NODE_ENV) === 'test') {
if (imageRef.current) {
imageRef.current.src = actualSrc;
}
return;
}
const { current } = imageRef;
if (!current) {
return;
}
const newHandle = delayRender('Loading <Img> 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;
let unmounted = false;
const onComplete = () => {
var _a, _b, _c;
var _d;
// the decode() promise isn't cancelable -- it may still resolve after unmounting
if (unmounted) {
continueRender(newHandle);
return;
}
if (((_d = errors.current[(_a = imageRef.current) === null || _a === void 0 ? void 0 : _a.src]) !== null && _d !== void 0 ? _d : 0) > 0) {
delete errors.current[(_b = imageRef.current) === null || _b === void 0 ? void 0 : _b.src];
// eslint-disable-next-line no-console
console.info(`Retry successful - ${(0, truncate_src_for_label_js_1.truncateSrcForLabel)((_c = imageRef.current) === null || _c === void 0 ? void 0 : _c.src)} is now loaded`);
}
if (current) {
onImageFrame === null || onImageFrame === void 0 ? void 0 : onImageFrame(current);
}
unblock();
continueRender(newHandle);
};
if (!imageRef.current) {
onComplete();
return;
}
current.src = actualSrc;
current
.decode()
.then(onComplete)
.catch((err) => {
// fall back to onload event if decode() fails
// eslint-disable-next-line no-console
console.warn(err);
// HTMLImageElement.complete is also true for broken images (e.g. 404),
// so only treat it as loaded if intrinsic dimensions are available.
if (current.complete &&
current.naturalWidth > 0 &&
current.naturalHeight > 0) {
onComplete();
}
else {
current.addEventListener('load', onComplete);
}
});
// If tag gets unmounted, clear pending handles because image is not going to load
return () => {
unmounted = true;
current.removeEventListener('load', onComplete);
unblock();
continueRender(newHandle);
};
}, [
actualSrc,
delayPlayback,
delayRenderRetries,
delayRenderTimeoutInMilliseconds,
pauseWhenLoading,
isPremounting,
isPostmounting,
onImageFrame,
continueRender,
delayRender,
]);
}
const { isClientSideRendering, isRendering } = (0, use_remotion_environment_js_1.useRemotionEnvironment)();
const crossOriginValue = (0, get_cross_origin_value_js_1.getCrossOriginValue)({
crossOrigin,
requestsVideoFrame: false,
isClientSideRendering,
});
// src gets set once we've loaded and decoded the image.
return (jsx_runtime_1.jsx("img", { ...props, ref: imageCallbackRef, crossOrigin: crossOriginValue, onError: didGetError, decoding: isRendering ? 'sync' : decoding }));
};
const NativeImgInner = ({ hidden, name, stack, showInTimeline, src, from, durationInFrames, _experimentalControls: controls, _remotionInternalRefForOutline: refForOutline, ...props }) => {
if (!src) {
throw new Error('No "src" prop was passed to <Img>.');
}
return (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, _remotionInternalStack: stack, _remotionInternalDocumentationLink: name === undefined ? 'https://www.remotion.dev/docs/img' : undefined, _remotionInternalIsMedia: { type: 'image', src }, name: name !== null && name !== void 0 ? name : '<Img>', _experimentalControls: controls, showInTimeline: showInTimeline !== null && showInTimeline !== void 0 ? showInTimeline : true, hidden: hidden, _remotionInternalRefForOutline: refForOutline, children: jsx_runtime_1.jsx(ImgContent, { src: src, refForOutline: refForOutline, ...props }) }));
};
const CanvasImageWithPrivateProps = index_js_1.CanvasImage;
exports.imgSchema = {
durationInFrames: sequence_field_schema_js_1.durationInFramesField,
from: sequence_field_schema_js_1.fromField,
...sequence_field_schema_js_1.sequenceVisualStyleSchema,
hidden: sequence_field_schema_js_1.hiddenField,
};
const imgCanvasFallbackIncompatibleProps = new Set([
'alt',
'crossOrigin',
'decoding',
'fetchPriority',
'loading',
'onError',
'onImageFrame',
'onLoad',
'sizes',
'srcSet',
'useMap',
]);
const getIncompatiblePropNames = (props) => Object.keys(props).filter((key) => props[key] !== undefined && imgCanvasFallbackIncompatibleProps.has(key));
const formatPropList = (props) => {
return props.map((prop) => `"${prop}"`).join(', ');
};
const validateCanvasImageFallbackProps = ({ props, ref, width, height, }) => {
if (typeof width === 'string' || typeof height === 'string') {
throw new Error('The "width" and "height" props must be numbers on <Img> when effects are passed, because <Img> renders a <CanvasImage>. Use numeric props or CSS dimensions in "style".');
}
const conflictingProps = getIncompatiblePropNames(props);
if (ref !== null && ref !== undefined) {
conflictingProps.unshift('ref');
}
if (conflictingProps.length === 0) {
return;
}
throw new Error(`The ${formatPropList(conflictingProps)} prop${conflictingProps.length === 1 ? '' : 's'} cannot be used on <Img> when effects are passed, because <Img> renders a <canvas> instead of a native <img>. Remove ${conflictingProps.length === 1 ? 'this prop' : 'these props'}.`);
};
const getFitFromObjectFit = (style) => {
const objectFit = style === null || style === void 0 ? void 0 : style.objectFit;
if (objectFit === 'fill' ||
objectFit === 'contain' ||
objectFit === 'cover') {
return objectFit;
}
return undefined;
};
const ImgInner = ({ effects = [], ref, hidden, name, stack, showInTimeline, src, from, durationInFrames, _experimentalControls: controls, width, height, className, style, id, pauseWhenLoading, maxRetries, delayRenderRetries, delayRenderTimeoutInMilliseconds, ...props }) => {
var _a;
const refForOutline = (0, react_1.useRef)(null);
if (effects.length === 0) {
return (jsx_runtime_1.jsx(NativeImgInner, { ...props, ref: ref, hidden: hidden, name: name, stack: stack, showInTimeline: showInTimeline, src: src, from: from, durationInFrames: durationInFrames, _experimentalControls: controls, width: width, height: height, className: className, style: style, id: id, pauseWhenLoading: pauseWhenLoading, maxRetries: maxRetries, delayRenderRetries: delayRenderRetries, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds, _remotionInternalRefForOutline: refForOutline }));
}
if (!src) {
throw new Error('No "src" prop was passed to <Img>.');
}
validateCanvasImageFallbackProps({
props,
ref,
width,
height,
});
const canvasWidth = typeof width === 'number' ? width : undefined;
const canvasHeight = typeof height === 'number' ? height : undefined;
const canvasProps = props;
const canvasFit = (_a = getFitFromObjectFit(style)) !== null && _a !== void 0 ? _a : 'fill';
return (jsx_runtime_1.jsx(CanvasImageWithPrivateProps, { src: src, width: canvasWidth, height: canvasHeight, fit: canvasFit, effects: effects, className: className, style: style, id: id, pauseWhenLoading: pauseWhenLoading, maxRetries: maxRetries, delayRenderRetries: delayRenderRetries, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds, from: from, durationInFrames: durationInFrames, hidden: hidden, name: name !== null && name !== void 0 ? name : '<Img>', showInTimeline: showInTimeline, stack: stack, _remotionInternalDocumentationLink: name === undefined ? 'https://www.remotion.dev/docs/img' : undefined, _experimentalControls: controls, _remotionInternalRefForOutline: refForOutline, ...canvasProps }));
};
/*
* @description Works just like a regular HTML img tag. When you use the <Img> tag, Remotion will ensure that the image is loaded before rendering the frame.
* @see [Documentation](https://remotion.dev/docs/img)
*/
exports.Img = (0, wrap_in_schema_js_1.wrapInSchema)({
Component: ImgInner,
schema: exports.imgSchema,
supportsEffects: true,
});
(0, enable_sequence_stack_traces_js_1.addSequenceStackTraces)(exports.Img);