UNPKG

@remotion/studio

Version:

APIs for interacting with the Remotion Studio

437 lines (436 loc) 25.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebRenderModalWithLoader = void 0; const jsx_runtime_1 = require("react/jsx-runtime"); const studio_shared_1 = require("@remotion/studio-shared"); const web_renderer_1 = require("@remotion/web-renderer"); const react_1 = require("react"); const ShortcutHint_1 = require("../../error-overlay/remotion-overlay/ShortcutHint"); const audio_1 = require("../../icons/audio"); const certificate_1 = require("../../icons/certificate"); const data_1 = require("../../icons/data"); const file_1 = require("../../icons/file"); const frame_1 = require("../../icons/frame"); const gear_1 = require("../../icons/gear"); const modals_1 = require("../../state/modals"); const sidebar_1 = require("../../state/sidebar"); const Button_1 = require("../Button"); const is_menu_item_1 = require("../Menu/is-menu-item"); const ModalHeader_1 = require("../ModalHeader"); const DismissableModal_1 = require("../NewComposition/DismissableModal"); const OptionsPanel_1 = require("../OptionsPanel"); const context_1 = require("../RenderQueue/context"); const SegmentedControl_1 = require("../SegmentedControl"); const vertical_1 = require("../Tabs/vertical"); const DataEditor_1 = require("./DataEditor"); const get_string_before_suffix_1 = require("./get-string-before-suffix"); const render_modals_1 = require("./render-modals"); const ResolveCompositionBeforeModal_1 = require("./ResolveCompositionBeforeModal"); const use_encodable_audio_codecs_1 = require("./use-encodable-audio-codecs"); const use_encodable_video_codecs_1 = require("./use-encodable-video-codecs"); const WebRendererExperimentalBadge_1 = require("./WebRendererExperimentalBadge"); const WebRenderModalAdvanced_1 = require("./WebRenderModalAdvanced"); const WebRenderModalAudio_1 = require("./WebRenderModalAudio"); const WebRenderModalBasic_1 = require("./WebRenderModalBasic"); const WebRenderModalLicense_1 = require("./WebRenderModalLicense"); const WebRenderModalPicture_1 = require("./WebRenderModalPicture"); const invalidCharacters = ['?', '*', '+', ':', '%']; const isValidStillExtension = (extension, stillImageFormat) => { if (stillImageFormat === 'jpeg' && extension === 'jpg') { return true; } return extension === stillImageFormat; }; const validateOutnameForStill = ({ outName, stillImageFormat, }) => { try { const extension = outName.substring(outName.lastIndexOf('.') + 1); const prefix = outName.substring(0, outName.lastIndexOf('.')); const hasDotAfterSlash = () => { const substrings = prefix.split('/'); for (const str of substrings) { if (str[0] === '.') { return true; } } return false; }; const hasInvalidChar = () => { return prefix.split('').some((char) => invalidCharacters.includes(char)); }; if (prefix.length < 1) { throw new Error('The prefix must be at least 1 character long'); } if (prefix[0] === '.' || hasDotAfterSlash()) { throw new Error('The output name must not start with a dot'); } if (hasInvalidChar()) { throw new Error("Filename can't contain the following characters: ?, *, +, %, :"); } if (!isValidStillExtension(extension, stillImageFormat)) { throw new Error(`The extension ${extension} is not supported for still image format ${stillImageFormat}`); } return { valid: true }; } catch (err) { return { valid: false, error: err }; } }; // TODO: Switch to server-side rendering // TODO: Filter out codecs that are not supported for the container // TODO: Shortcut: Shift + R // TODO: Apply defaultCodec // TODO: Apply defaultOutName const WebRenderModal = ({ initialFrame, defaultProps, inFrameMark, outFrameMark, initialLogLevel, initialLicenseKey, initialStillImageFormat, initialDefaultOutName, initialScale, initialDelayRenderTimeout, initialMediaCacheSizeInBytes, initialContainer, initialVideoCodec, initialAudioCodec, initialAudioBitrate, initialVideoBitrate, initialHardwareAcceleration, initialKeyframeIntervalInSeconds, initialTransparent, initialMuted, }) => { var _a; const context = (0, react_1.useContext)(ResolveCompositionBeforeModal_1.ResolvedCompositionContext); const { setSelectedModal } = (0, react_1.useContext)(modals_1.ModalsContext); const { setSidebarCollapsedState } = (0, react_1.useContext)(sidebar_1.SidebarContext); const { addClientStillJob, addClientVideoJob } = (0, react_1.useContext)(context_1.RenderQueueContext); if (!context) { throw new Error('Should not be able to render without resolving comp first'); } const { resolved: { result: resolvedComposition }, unresolved: unresolvedComposition, } = context; const [isVideo] = (0, react_1.useState)(() => { return typeof resolvedComposition.durationInFrames === 'undefined' ? true : resolvedComposition.durationInFrames > 1; }); const [renderMode, setRenderMode] = (0, react_1.useState)(initialContainer && (0, web_renderer_1.isAudioOnlyContainer)(initialContainer) ? 'audio' : isVideo ? 'video' : 'still'); const [tab, setTab] = (0, react_1.useState)('general'); const [imageFormat, setImageFormat] = (0, react_1.useState)(() => initialStillImageFormat !== null && initialStillImageFormat !== void 0 ? initialStillImageFormat : 'png'); const [frame, setFrame] = (0, react_1.useState)(() => initialFrame); const [logLevel, setLogLevel] = (0, react_1.useState)(() => initialLogLevel); const [inputProps, _setInputProps] = (0, react_1.useState)(() => defaultProps); const setInputProps = (0, react_1.useCallback)((updater) => { _setInputProps(updater); }, []); const [delayRenderTimeout, setDelayRenderTimeout] = (0, react_1.useState)(initialDelayRenderTimeout !== null && initialDelayRenderTimeout !== void 0 ? initialDelayRenderTimeout : 30000); const [mediaCacheSizeInBytes, setMediaCacheSizeInBytes] = (0, react_1.useState)(initialMediaCacheSizeInBytes); // Video-specific state const [codec, setCodec] = (0, react_1.useState)(initialVideoCodec !== null && initialVideoCodec !== void 0 ? initialVideoCodec : 'h264'); const [container, setContainer] = (0, react_1.useState)(initialContainer !== null && initialContainer !== void 0 ? initialContainer : 'mp4'); const [audioCodec, setAudioCodec] = (0, react_1.useState)(initialAudioCodec !== null && initialAudioCodec !== void 0 ? initialAudioCodec : 'aac'); const [audioBitrate, setAudioBitrate] = (0, react_1.useState)(initialAudioBitrate !== null && initialAudioBitrate !== void 0 ? initialAudioBitrate : 'medium'); const [videoBitrate, setVideoBitrate] = (0, react_1.useState)(initialVideoBitrate !== null && initialVideoBitrate !== void 0 ? initialVideoBitrate : 'high'); const [hardwareAcceleration, setHardwareAcceleration] = (0, react_1.useState)((_a = initialHardwareAcceleration) !== null && _a !== void 0 ? _a : 'no-preference'); const [keyframeIntervalInSeconds, setKeyframeIntervalInSeconds] = (0, react_1.useState)(initialKeyframeIntervalInSeconds !== null && initialKeyframeIntervalInSeconds !== void 0 ? initialKeyframeIntervalInSeconds : 5); const [startFrame, setStartFrame] = (0, react_1.useState)(() => inFrameMark); const [endFrame, setEndFrame] = (0, react_1.useState)(() => outFrameMark); const [transparent, setTransparent] = (0, react_1.useState)(initialTransparent !== null && initialTransparent !== void 0 ? initialTransparent : false); const [muted, setMuted] = (0, react_1.useState)(initialMuted !== null && initialMuted !== void 0 ? initialMuted : false); const [scale, setScale] = (0, react_1.useState)(initialScale !== null && initialScale !== void 0 ? initialScale : 1); const [licenseKey, setLicenseKey] = (0, react_1.useState)(initialLicenseKey); const encodableAudioCodecs = (0, use_encodable_audio_codecs_1.useEncodableAudioCodecs)(container); const encodableVideoCodecs = (0, use_encodable_video_codecs_1.useEncodableVideoCodecs)(container); const effectiveAudioCodec = (0, react_1.useMemo)(() => { var _a; if (encodableAudioCodecs.includes(audioCodec)) { return audioCodec; } return (_a = encodableAudioCodecs[0]) !== null && _a !== void 0 ? _a : audioCodec; }, [audioCodec, encodableAudioCodecs]); const effectiveVideoCodec = (0, react_1.useMemo)(() => { var _a; if (encodableVideoCodecs.includes(codec)) { return codec; } return (_a = encodableVideoCodecs[0]) !== null && _a !== void 0 ? _a : codec; }, [codec, encodableVideoCodecs]); const finalEndFrame = (0, react_1.useMemo)(() => { if (endFrame === null) { return resolvedComposition.durationInFrames - 1; } return Math.max(0, Math.min(resolvedComposition.durationInFrames - 1, endFrame)); }, [endFrame, resolvedComposition.durationInFrames]); const finalStartFrame = (0, react_1.useMemo)(() => { if (startFrame === null) { return 0; } return Math.max(0, Math.min(finalEndFrame, startFrame)); }, [finalEndFrame, startFrame]); const [initialOutNameState] = (0, react_1.useState)(() => { var _a; var _b; if (initialDefaultOutName) { return initialDefaultOutName; } const defaultOut = (0, studio_shared_1.getDefaultOutLocation)({ compositionName: resolvedComposition.id, defaultExtension: renderMode === 'still' ? imageFormat : isVideo ? container : imageFormat, type: 'asset', compositionDefaultOutName: resolvedComposition.defaultOutName, outputLocation: (_b = (_a = window.remotion_renderDefaults) === null || _a === void 0 ? void 0 : _a.outputLocation) !== null && _b !== void 0 ? _b : null, }); if (window.remotion_isReadOnlyStudio) { return defaultOut.replace(/^out\//, ''); } return defaultOut; }); const [outName, setOutName] = (0, react_1.useState)(() => initialOutNameState); const updateOutNameExtension = (0, react_1.useCallback)((extension) => { setOutName((prev) => (0, get_string_before_suffix_1.getStringBeforeSuffix)(prev) + '.' + extension); }, []); const setStillFormat = (0, react_1.useCallback)((format) => { setImageFormat(format); updateOutNameExtension(format); }, [updateOutNameExtension]); const setContainerFormat = (0, react_1.useCallback)((newContainer) => { setContainer(newContainer); const defaultVideoCodec = (0, web_renderer_1.getDefaultVideoCodecForContainer)(newContainer); if (defaultVideoCodec) { setCodec(defaultVideoCodec); } setAudioCodec((0, web_renderer_1.getDefaultAudioCodecForContainer)(newContainer)); updateOutNameExtension(newContainer); }, [updateOutNameExtension]); const setCodecWithContainer = (0, react_1.useCallback)((newCodec) => { setCodec(newCodec); const newContainer = (0, web_renderer_1.getDefaultContainerForCodec)(newCodec); setContainer(newContainer); setAudioCodec((0, web_renderer_1.getDefaultAudioCodecForContainer)(newContainer)); updateOutNameExtension(newContainer); }, [updateOutNameExtension]); const onRenderModeChange = (0, react_1.useCallback)((newMode) => { setRenderMode(newMode); if (newMode === 'video') { const newContainer = (0, web_renderer_1.isAudioOnlyContainer)(container) ? 'mp4' : container; setContainer(newContainer); setAudioCodec((0, web_renderer_1.getDefaultAudioCodecForContainer)(newContainer)); updateOutNameExtension(newContainer); } else if (newMode === 'audio') { const newContainer = 'wav'; setContainer(newContainer); setMuted(false); setAudioCodec((0, web_renderer_1.getDefaultAudioCodecForContainer)(newContainer)); updateOutNameExtension(newContainer); setTab((prev) => (prev === 'picture' ? 'general' : prev)); } else if (newMode === 'still') { updateOutNameExtension(imageFormat); setTab((prev) => (prev === 'audio' ? 'general' : prev)); } }, [container, imageFormat, updateOutNameExtension]); const renderTabOptions = (0, react_1.useMemo)(() => { const options = [ { label: 'Still', onClick: () => { onRenderModeChange('still'); }, key: 'still', selected: renderMode === 'still', }, ]; // Only show video/audio options if composition has more than 1 frame if (resolvedComposition.durationInFrames > 1) { options.push({ label: 'Video', onClick: () => { onRenderModeChange('video'); }, key: 'video', selected: renderMode === 'video', }); options.push({ label: 'Audio', onClick: () => { onRenderModeChange('audio'); }, key: 'audio', selected: renderMode === 'audio', }); } return options; }, [renderMode, resolvedComposition.durationInFrames, onRenderModeChange]); const onFrameSetDirectly = (0, react_1.useCallback)((newFrame) => { setFrame(newFrame); }, [setFrame]); const onFrameChanged = (0, react_1.useCallback)((e) => { setFrame((q) => { const newFrame = parseFloat(e); if (Number.isNaN(newFrame)) { return q; } return newFrame; }); }, [setFrame]); const onOutNameChange = (0, react_1.useCallback)((e) => { setOutName(e.target.value); }, []); const outnameValidation = (0, react_1.useMemo)(() => { if (renderMode === 'still') { return validateOutnameForStill({ outName, stillImageFormat: imageFormat, }); } // Validate for video try { const extension = outName.substring(outName.lastIndexOf('.') + 1); const prefix = outName.substring(0, outName.lastIndexOf('.')); const hasDotAfterSlash = () => { const substrings = prefix.split('/'); for (const str of substrings) { if (str[0] === '.') { return true; } } return false; }; const hasInvalidChar = () => { return prefix .split('') .some((char) => invalidCharacters.includes(char)); }; if (prefix.length < 1) { throw new Error('The prefix must be at least 1 character long'); } if (prefix[0] === '.' || hasDotAfterSlash()) { throw new Error('The output name must not start with a dot'); } if (hasInvalidChar()) { throw new Error("Filename can't contain the following characters: ?, *, +, %, :"); } if (extension !== container) { throw new Error(`The extension ${extension} is not supported for container format ${container}`); } return { valid: true }; } catch (err) { return { valid: false, error: err }; } }, [outName, imageFormat, renderMode, container]); const onAddToQueue = (0, react_1.useCallback)(() => { var _a; var _b; const compositionRef = { component: unresolvedComposition.component, calculateMetadata: (_b = unresolvedComposition.calculateMetadata) !== null && _b !== void 0 ? _b : null, width: resolvedComposition.width, height: resolvedComposition.height, fps: resolvedComposition.fps, durationInFrames: resolvedComposition.durationInFrames, defaultProps: resolvedComposition.defaultProps, }; if (renderMode === 'still') { addClientStillJob({ type: 'client-still', compositionId: resolvedComposition.id, outName, imageFormat, frame, inputProps, delayRenderTimeout, mediaCacheSizeInBytes, logLevel, licenseKey, scale, }, compositionRef); } else { addClientVideoJob({ type: 'client-video', compositionId: resolvedComposition.id, outName, container, videoCodec: (0, web_renderer_1.isAudioOnlyContainer)(container) ? null : effectiveVideoCodec, audioCodec: effectiveAudioCodec, startFrame: finalStartFrame, endFrame: finalEndFrame, audioBitrate, videoBitrate, hardwareAcceleration, keyframeIntervalInSeconds, transparent, muted, inputProps, delayRenderTimeout, mediaCacheSizeInBytes, logLevel, licenseKey, scale, }, compositionRef); } setSidebarCollapsedState({ left: null, right: 'expanded' }); (0, OptionsPanel_1.persistSelectedOptionsSidebarPanel)('renders'); (_a = OptionsPanel_1.optionsSidebarTabs.current) === null || _a === void 0 ? void 0 : _a.selectRendersPanel(); setSelectedModal(null); }, [ renderMode, unresolvedComposition.component, unresolvedComposition.calculateMetadata, resolvedComposition.width, resolvedComposition.height, resolvedComposition.fps, resolvedComposition.durationInFrames, resolvedComposition.defaultProps, resolvedComposition.id, setSidebarCollapsedState, outName, imageFormat, frame, inputProps, delayRenderTimeout, mediaCacheSizeInBytes, logLevel, licenseKey, container, effectiveVideoCodec, effectiveAudioCodec, finalStartFrame, finalEndFrame, audioBitrate, videoBitrate, hardwareAcceleration, keyframeIntervalInSeconds, transparent, muted, setSelectedModal, addClientStillJob, addClientVideoJob, scale, ]); return (jsx_runtime_1.jsxs("div", { style: render_modals_1.outerModalStyle, children: [ jsx_runtime_1.jsx(ModalHeader_1.ModalHeader, { title: `Render ${resolvedComposition.id}` }), jsx_runtime_1.jsxs("div", { style: render_modals_1.container, children: [ jsx_runtime_1.jsx(SegmentedControl_1.SegmentedControl, { items: renderTabOptions, needsWrapping: false }), jsx_runtime_1.jsx("div", { style: render_modals_1.flexer }), jsx_runtime_1.jsxs(Button_1.Button, { autoFocus: true, onClick: onAddToQueue, style: render_modals_1.buttonStyle, disabled: !outnameValidation.valid, children: ["Render ", renderMode, jsx_runtime_1.jsx(ShortcutHint_1.ShortcutHint, { keyToPress: "\u21B5", cmdOrCtrl: true }) ] }) ] }), jsx_runtime_1.jsx("div", { style: render_modals_1.container, children: jsx_runtime_1.jsx(WebRendererExperimentalBadge_1.WebRendererExperimentalBadge, {}) }), jsx_runtime_1.jsxs("div", { style: render_modals_1.horizontalLayout, children: [ jsx_runtime_1.jsxs("div", { style: render_modals_1.leftSidebar, children: [ jsx_runtime_1.jsxs(vertical_1.VerticalTab, { style: render_modals_1.horizontalTab, selected: tab === 'general', onClick: () => setTab('general'), children: [ jsx_runtime_1.jsx("div", { style: render_modals_1.iconContainer, children: jsx_runtime_1.jsx(file_1.FileIcon, { style: render_modals_1.icon }) }), "General"] }), jsx_runtime_1.jsxs(vertical_1.VerticalTab, { style: render_modals_1.horizontalTab, selected: tab === 'data', onClick: () => setTab('data'), children: [ jsx_runtime_1.jsx("div", { style: render_modals_1.iconContainer, children: jsx_runtime_1.jsx(data_1.DataIcon, { style: render_modals_1.icon }) }), "Input Props"] }), renderMode !== 'audio' ? (jsx_runtime_1.jsxs(vertical_1.VerticalTab, { style: render_modals_1.horizontalTab, selected: tab === 'picture', onClick: () => setTab('picture'), children: [ jsx_runtime_1.jsx("div", { style: render_modals_1.iconContainer, children: jsx_runtime_1.jsx(frame_1.PicIcon, { style: render_modals_1.icon }) }), "Picture"] })) : null, renderMode === 'video' || renderMode === 'audio' ? (jsx_runtime_1.jsxs(vertical_1.VerticalTab, { style: render_modals_1.horizontalTab, selected: tab === 'audio', onClick: () => setTab('audio'), children: [ jsx_runtime_1.jsx("div", { style: render_modals_1.iconContainer, children: jsx_runtime_1.jsx(audio_1.AudioIcon, { style: render_modals_1.icon }) }), "Audio"] })) : null, jsx_runtime_1.jsxs(vertical_1.VerticalTab, { style: render_modals_1.horizontalTab, selected: tab === 'advanced', onClick: () => setTab('advanced'), children: [ jsx_runtime_1.jsx("div", { style: render_modals_1.iconContainer, children: jsx_runtime_1.jsx(gear_1.GearIcon, { style: render_modals_1.icon }) }), "Other"] }), jsx_runtime_1.jsxs(vertical_1.VerticalTab, { style: render_modals_1.horizontalTab, selected: tab === 'license', onClick: () => setTab('license'), children: [ jsx_runtime_1.jsx("div", { style: render_modals_1.iconContainer, children: jsx_runtime_1.jsx(certificate_1.CertificateIcon, { style: render_modals_1.icon }) }), "License"] }) ] }), jsx_runtime_1.jsx("div", { style: render_modals_1.optionsPanel, className: is_menu_item_1.VERTICAL_SCROLLBAR_CLASSNAME, children: tab === 'general' ? (jsx_runtime_1.jsx(WebRenderModalBasic_1.WebRenderModalBasic, { renderMode: renderMode, resolvedComposition: resolvedComposition, imageFormat: imageFormat, setStillFormat: setStillFormat, frame: frame, onFrameChanged: onFrameChanged, onFrameSetDirectly: onFrameSetDirectly, container: container, setContainerFormat: setContainerFormat, setCodec: setCodecWithContainer, encodableVideoCodecs: encodableVideoCodecs, effectiveVideoCodec: effectiveVideoCodec, startFrame: finalStartFrame, setStartFrame: setStartFrame, endFrame: finalEndFrame, setEndFrame: setEndFrame, outName: outName, onOutNameChange: onOutNameChange, validationMessage: outnameValidation.valid ? null : outnameValidation.error.message, logLevel: logLevel, setLogLevel: setLogLevel })) : tab === 'data' ? (jsx_runtime_1.jsx(DataEditor_1.DataEditor, { defaultProps: inputProps, setDefaultProps: setInputProps, unresolvedComposition: unresolvedComposition, propsEditType: "input-props", canSaveDefaultProps: { canUpdate: false, reason: 'render dialogue', determined: false, } })) : tab === 'picture' ? (jsx_runtime_1.jsx(WebRenderModalPicture_1.WebRenderModalPicture, { renderMode: renderMode, videoBitrate: videoBitrate, setVideoBitrate: setVideoBitrate, keyframeIntervalInSeconds: keyframeIntervalInSeconds, setKeyframeIntervalInSeconds: setKeyframeIntervalInSeconds, transparent: transparent, setTransparent: setTransparent, scale: scale, setScale: setScale, compositionWidth: resolvedComposition.width, compositionHeight: resolvedComposition.height })) : tab === 'audio' ? (jsx_runtime_1.jsx(WebRenderModalAudio_1.WebRenderModalAudio, { renderMode: renderMode, muted: muted, setMuted: setMuted, audioCodec: audioCodec, setAudioCodec: setAudioCodec, audioBitrate: audioBitrate, setAudioBitrate: setAudioBitrate, container: container, encodableCodecs: encodableAudioCodecs, effectiveAudioCodec: effectiveAudioCodec })) : tab === 'advanced' ? (jsx_runtime_1.jsx(WebRenderModalAdvanced_1.WebRenderModalAdvanced, { renderMode: renderMode, delayRenderTimeout: delayRenderTimeout, setDelayRenderTimeout: setDelayRenderTimeout, mediaCacheSizeInBytes: mediaCacheSizeInBytes, setMediaCacheSizeInBytes: setMediaCacheSizeInBytes, hardwareAcceleration: hardwareAcceleration, setHardwareAcceleration: setHardwareAcceleration })) : (jsx_runtime_1.jsx(WebRenderModalLicense_1.WebRenderModalLicense, { licenseKey: licenseKey, setLicenseKey: setLicenseKey, initialPublicLicenseKey: initialLicenseKey })) }) ] }) ] })); }; const WebRenderModalWithLoader = (props) => { return (jsx_runtime_1.jsx(DismissableModal_1.DismissableModal, { children: jsx_runtime_1.jsx(ResolveCompositionBeforeModal_1.ResolveCompositionBeforeModal, { compositionId: props.compositionId, children: jsx_runtime_1.jsx(WebRenderModal, { ...props }) }) })); }; exports.WebRenderModalWithLoader = WebRenderModalWithLoader;