UNPKG

@remotion/renderer

Version:

Render Remotion videos using Node.js or Bun

313 lines (312 loc) • 13.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.attachDownloadListenerToEmitter = exports.downloadAndMapAssetsToFileUrl = exports.getSanitizedFilenameForAssetUrl = exports.markAllAssetsAsDownloaded = exports.downloadAsset = void 0; const node_fs_1 = __importDefault(require("node:fs")); const node_path_1 = __importStar(require("node:path")); const no_react_1 = require("remotion/no-react"); const compress_assets_1 = require("../compress-assets"); const ensure_output_directory_1 = require("../ensure-output-directory"); const mime_types_1 = require("../mime-types"); const download_file_1 = require("./download-file"); const get_audio_channels_1 = require("./get-audio-channels"); const sanitize_filepath_1 = require("./sanitize-filepath"); const waitForAssetToBeDownloaded = ({ src, downloadDir, downloadMap, }) => { var _a, _b; if ((_a = downloadMap.hasBeenDownloadedMap[src]) === null || _a === void 0 ? void 0 : _a[downloadDir]) { return Promise.resolve((_b = downloadMap.hasBeenDownloadedMap[src]) === null || _b === void 0 ? void 0 : _b[downloadDir]); } if (!downloadMap.listeners[src]) { downloadMap.listeners[src] = {}; } if (!downloadMap.listeners[src][downloadDir]) { downloadMap.listeners[src][downloadDir] = []; } return new Promise((resolve) => { downloadMap.listeners[src][downloadDir].push(() => { const srcMap = downloadMap.hasBeenDownloadedMap[src]; if (!(srcMap === null || srcMap === void 0 ? void 0 : srcMap[downloadDir])) { throw new Error('Expected file for ' + src + 'to be available in ' + downloadDir); } resolve(srcMap[downloadDir]); }); }); }; const notifyAssetIsDownloaded = ({ src, downloadDir, to, downloadMap, }) => { if (!downloadMap.listeners[src]) { downloadMap.listeners[src] = {}; } if (!downloadMap.listeners[src][downloadDir]) { downloadMap.listeners[src][downloadDir] = []; } if (!downloadMap.isDownloadingMap[src]) { downloadMap.isDownloadingMap[src] = {}; } downloadMap.isDownloadingMap[src][downloadDir] = false; if (!downloadMap.hasBeenDownloadedMap[src]) { downloadMap.hasBeenDownloadedMap[src] = {}; } downloadMap.hasBeenDownloadedMap[src][downloadDir] = to; downloadMap.listeners[src][downloadDir].forEach((fn) => fn()); }; const validateMimeType = (mimeType, src) => { if (!mimeType.includes('/')) { const errMessage = [ 'A data URL was passed but did not have the correct format so that Remotion could convert it for the video to be rendered.', 'The format of the data URL must be `data:[mime-type];[encoding],[data]`.', 'The `mime-type` parameter must be a valid mime type.', 'The data that was received is (truncated to 100 characters):', src.substr(0, 100), ].join(' '); throw new TypeError(errMessage); } }; function validateBufferEncoding(potentialEncoding, dataUrl) { const asserted = potentialEncoding; const validEncodings = [ 'ascii', 'base64', 'base64url', 'binary', 'hex', 'latin1', 'ucs-2', 'ucs2', 'utf-8', 'utf16le', 'utf8', ]; if (!validEncodings.find((en) => asserted === en)) { const errMessage = [ 'A data URL was passed but did not have the correct format so that Remotion could convert it for the video to be rendered.', 'The format of the data URL must be `data:[mime-type];[encoding],[data]`.', 'The `encoding` parameter must be one of the following:', `${validEncodings.join(' ')}.`, 'The data that was received is (truncated to 100 characters):', dataUrl.substr(0, 100), ].join(' '); throw new TypeError(errMessage); } } const downloadAsset = async ({ src, downloadMap, indent, logLevel, shouldAnalyzeAudioImmediately, binariesDirectory, cancelSignalForAudioAnalysis, audioStreamIndex, }) => { var _a, _b, _c; if ((0, compress_assets_1.isAssetCompressed)(src)) { return src; } const { downloadDir } = downloadMap; if ((_a = downloadMap.hasBeenDownloadedMap[src]) === null || _a === void 0 ? void 0 : _a[downloadDir]) { const claimedDownloadLocation = (_b = downloadMap.hasBeenDownloadedMap[src]) === null || _b === void 0 ? void 0 : _b[downloadDir]; // The OS might have deleted the file since even though we marked it as downloaded. In that case we reset the state and download it again if (node_fs_1.default.existsSync(claimedDownloadLocation)) { return claimedDownloadLocation; } downloadMap.hasBeenDownloadedMap[src][downloadDir] = null; if (!downloadMap.isDownloadingMap[src]) { downloadMap.isDownloadingMap[src] = {}; } downloadMap.isDownloadingMap[src][downloadDir] = false; } if ((_c = downloadMap.isDownloadingMap[src]) === null || _c === void 0 ? void 0 : _c[downloadDir]) { return waitForAssetToBeDownloaded({ downloadMap, src, downloadDir }); } if (!downloadMap.isDownloadingMap[src]) { downloadMap.isDownloadingMap[src] = {}; } downloadMap.isDownloadingMap[src][downloadDir] = true; downloadMap.emitter.dispatchDownload(src); if (src.startsWith('data:')) { const [assetDetails, assetData] = src.substring('data:'.length).split(','); if (!assetDetails.includes(';')) { const errMessage = [ 'A data URL was passed but did not have the correct format so that Remotion could convert it for the video to be rendered.', 'The format of the data URL must be `data:[mime-type];[encoding],[data]`.', 'The data that was received is (truncated to 100 characters):', src.substring(0, 100), ].join(' '); throw new TypeError(errMessage); } const [mimeType, encoding] = assetDetails.split(';'); validateMimeType(mimeType, src); validateBufferEncoding(encoding, src); const output = (0, exports.getSanitizedFilenameForAssetUrl)({ contentDisposition: null, downloadDir, src, contentType: mimeType, }); (0, ensure_output_directory_1.ensureOutputDirectory)(output); const buff = Buffer.from(assetData, encoding); await node_fs_1.default.promises.writeFile(output, buff); notifyAssetIsDownloaded({ src, downloadMap, downloadDir, to: output }); return output; } const { to } = await (0, download_file_1.downloadFile)({ url: src, onProgress: (progress) => { downloadMap.emitter.dispatchDownloadProgress(src, progress.percent, progress.downloaded, progress.totalSize); }, to: (contentDisposition, contentType) => (0, exports.getSanitizedFilenameForAssetUrl)({ contentDisposition, downloadDir, src, contentType, }), indent, logLevel, }); notifyAssetIsDownloaded({ src, downloadMap, downloadDir, to }); if (shouldAnalyzeAudioImmediately) { await (0, get_audio_channels_1.getAudioChannelsAndDuration)({ binariesDirectory, downloadMap, src: to, indent, logLevel, cancelSignal: cancelSignalForAudioAnalysis, audioStreamIndex, }); } return to; }; exports.downloadAsset = downloadAsset; const markAllAssetsAsDownloaded = (downloadMap) => { Object.keys(downloadMap.hasBeenDownloadedMap).forEach((key) => { delete downloadMap.hasBeenDownloadedMap[key]; }); Object.keys(downloadMap.isDownloadingMap).forEach((key) => { delete downloadMap.isDownloadingMap[key]; }); }; exports.markAllAssetsAsDownloaded = markAllAssetsAsDownloaded; const getFilename = ({ contentDisposition, src, contentType, }) => { const filenameProbe = 'filename='; if (contentDisposition === null || contentDisposition === void 0 ? void 0 : contentDisposition.includes(filenameProbe)) { const start = contentDisposition.indexOf(filenameProbe); const onlyFromFileName = contentDisposition.substring(start + filenameProbe.length); const hasSemi = onlyFromFileName.indexOf(';'); if (hasSemi === -1) { return { pathname: onlyFromFileName.trim(), search: '' }; } return { search: '', pathname: onlyFromFileName.substring(0, hasSemi).trim(), }; } const { pathname, search } = new URL(src); const ext = (0, node_path_1.extname)(pathname); // Has no file extension, check if we can derive it from contentType if (!ext && contentType) { const matchedExt = (0, mime_types_1.getExt)(contentType); return { pathname: `${pathname}.${matchedExt}`, search, }; } return { pathname, search }; }; const getSanitizedFilenameForAssetUrl = ({ src, downloadDir, contentDisposition, contentType, }) => { if ((0, compress_assets_1.isAssetCompressed)(src)) { return src; } const { pathname, search } = getFilename({ contentDisposition, contentType, src, }); const split = pathname.split('.'); const fileExtension = split.length > 1 && split[split.length - 1] ? `.${split[split.length - 1]}` : ''; const hashedFileName = String((0, no_react_1.random)(`${src}${pathname}${search}`)).replace('0.', ''); const filename = hashedFileName + fileExtension; return node_path_1.default.join(downloadDir, (0, sanitize_filepath_1.sanitizeFilePath)(filename)); }; exports.getSanitizedFilenameForAssetUrl = getSanitizedFilenameForAssetUrl; const downloadAndMapAssetsToFileUrl = async ({ renderAsset, onDownload, downloadMap, logLevel, indent, binariesDirectory, cancelSignalForAudioAnalysis, shouldAnalyzeAudioImmediately, }) => { const cleanup = (0, exports.attachDownloadListenerToEmitter)(downloadMap, onDownload); const newSrc = await (0, exports.downloadAsset)({ src: renderAsset.src, downloadMap, indent, logLevel, shouldAnalyzeAudioImmediately, binariesDirectory, cancelSignalForAudioAnalysis, audioStreamIndex: renderAsset.audioStreamIndex, }); cleanup(); return { ...renderAsset, src: newSrc, }; }; exports.downloadAndMapAssetsToFileUrl = downloadAndMapAssetsToFileUrl; const attachDownloadListenerToEmitter = (downloadMap, onDownload) => { const cleanup = []; if (!onDownload) { return () => undefined; } if (downloadMap.downloadListeners.includes(onDownload)) { return () => undefined; } downloadMap.downloadListeners.push(onDownload); cleanup.push(() => { downloadMap.downloadListeners = downloadMap.downloadListeners.filter((l) => l !== onDownload); return Promise.resolve(); }); const cleanupDownloadListener = downloadMap.emitter.addEventListener('download', ({ detail: { src: initialSrc } }) => { const progress = onDownload(initialSrc); const cleanupProgressListener = downloadMap.emitter.addEventListener('progress', ({ detail: { downloaded, percent, src: progressSrc, totalSize } }) => { if (initialSrc === progressSrc) { progress === null || progress === void 0 ? void 0 : progress({ downloaded, percent, totalSize }); } }); cleanup.push(() => { cleanupProgressListener(); return Promise.resolve(); }); }); cleanup.push(() => { cleanupDownloadListener(); return Promise.resolve(); }); return () => { cleanup.forEach((c) => c()); }; }; exports.attachDownloadListenerToEmitter = attachDownloadListenerToEmitter;