Add .gitignore to exclude all node packages and lock files

This commit is contained in:
Adolfo Reyna
2026-02-23 21:56:04 -05:00
parent faae96c9ed
commit dcc5c6c044
9747 changed files with 1555105 additions and 2 deletions
@@ -0,0 +1,10 @@
import type { CancelSignal } from '../make-cancel-signal';
export declare const applyToneFrequencyUsingFfmpeg: ({ input, output, toneFrequency, indent, logLevel, binariesDirectory, cancelSignal, }: {
input: string;
output: string;
toneFrequency: number;
indent: boolean;
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
binariesDirectory: string | null;
cancelSignal: CancelSignal | undefined;
}) => Promise<void>;
@@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyToneFrequencyUsingFfmpeg = void 0;
const call_ffmpeg_1 = require("../call-ffmpeg");
const logger_1 = require("../logger");
const sample_rate_1 = require("../sample-rate");
const applyToneFrequencyUsingFfmpeg = async ({ input, output, toneFrequency, indent, logLevel, binariesDirectory, cancelSignal, }) => {
const filter = `asetrate=${sample_rate_1.DEFAULT_SAMPLE_RATE}*${toneFrequency},aresample=${sample_rate_1.DEFAULT_SAMPLE_RATE},atempo=1/${toneFrequency}`;
const args = [
'-hide_banner',
'-i',
input,
['-ac', '2'],
'-filter:a',
filter,
['-c:a', 'pcm_s16le'],
['-ar', String(sample_rate_1.DEFAULT_SAMPLE_RATE)],
'-y',
output,
].flat(2);
logger_1.Log.verbose({ indent, logLevel }, 'Changing tone frequency using FFmpeg:', JSON.stringify(args.join(' ')), 'Filter:', filter);
const startTimestamp = Date.now();
const task = (0, call_ffmpeg_1.callFf)({
bin: 'ffmpeg',
args,
indent,
logLevel,
binariesDirectory,
cancelSignal,
});
await task;
logger_1.Log.verbose({ indent, logLevel }, 'Changed tone frequency using FFmpeg', `${Date.now() - startTimestamp}ms`);
};
exports.applyToneFrequencyUsingFfmpeg = applyToneFrequencyUsingFfmpeg;
@@ -0,0 +1,3 @@
import type { AudioOrVideoAsset } from 'remotion/no-react';
import type { Assets } from './types';
export declare const calculateAssetPositions: (frames: AudioOrVideoAsset[][]) => Assets;
@@ -0,0 +1,72 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateAssetPositions = void 0;
const filter_asset_types_1 = require("../filter-asset-types");
const resolve_asset_src_1 = require("../resolve-asset-src");
const flatten_volume_array_1 = require("./flatten-volume-array");
const types_1 = require("./types");
const areEqual = (a, b) => {
return a.id === b.id;
};
const findFrom = (target, renderAsset) => {
const index = target.findIndex((a) => areEqual(a, renderAsset));
if (index === -1) {
return false;
}
target.splice(index, 1);
return true;
};
const copyAndDeduplicateAssets = (renderAssets) => {
const onlyAudioAndVideo = (0, filter_asset_types_1.onlyAudioAndVideoAssets)(renderAssets);
const deduplicated = [];
for (const renderAsset of onlyAudioAndVideo) {
if (!deduplicated.find((d) => d.id === renderAsset.id)) {
deduplicated.push(renderAsset);
}
}
return deduplicated;
};
const calculateAssetPositions = (frames) => {
var _a, _b;
const assets = [];
const flattened = frames.flat(1);
for (let frame = 0; frame < frames.length; frame++) {
const prev = copyAndDeduplicateAssets((_a = frames[frame - 1]) !== null && _a !== void 0 ? _a : []);
const current = copyAndDeduplicateAssets(frames[frame]);
const next = copyAndDeduplicateAssets((_b = frames[frame + 1]) !== null && _b !== void 0 ? _b : []);
for (const asset of current) {
if (!findFrom(prev, asset)) {
assets.push({
src: (0, resolve_asset_src_1.resolveAssetSrc)((0, types_1.uncompressMediaAsset)(flattened, asset).src),
type: asset.type,
duration: null,
id: asset.id,
startInVideo: frame,
trimLeft: asset.mediaFrame,
volume: [],
playbackRate: asset.playbackRate,
toneFrequency: asset.toneFrequency,
audioStartFrame: asset.audioStartFrame,
audioStreamIndex: asset.audioStreamIndex,
});
}
const found = assets.find((a) => a.duration === null && areEqual(a, asset));
if (!found)
throw new Error('something wrong');
if (!findFrom(next, asset)) {
// Duration calculation:
// start 0, range 0-59:
// 59 - 0 + 1 ==> 60 frames duration
found.duration = frame - found.startInVideo + 1;
}
found.volume = [...found.volume, asset.volume];
}
}
for (const asset of assets) {
if (asset.duration === null) {
throw new Error('duration is unexpectedly null');
}
}
return assets.map((a) => (0, flatten_volume_array_1.convertAssetToFlattenedVolume)(a));
};
exports.calculateAssetPositions = calculateAssetPositions;
@@ -0,0 +1 @@
export declare const calculateATempo: (playbackRate: number) => string | null;
@@ -0,0 +1,16 @@
"use strict";
// Solving this problem of FFMPEG:
// https://trac.ffmpeg.org/wiki/How%20to%20speed%20up%20/%20slow%20down%20a%20video
// "The atempo filter is limited to using values between 0.5 and 2.0 (so it can slow it down to no less than half the original speed, and speed up to no more than double the input). If you need to, you can get around this limitation by stringing multiple atempo filters together. The following with quadruple the audio speed:"
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateATempo = void 0;
const calculateATempo = (playbackRate) => {
if (playbackRate === 1) {
return null;
}
if (playbackRate >= 0.5 && playbackRate <= 2) {
return `atempo=${playbackRate.toFixed(5)}`;
}
return `${(0, exports.calculateATempo)(Math.sqrt(playbackRate))},${(0, exports.calculateATempo)(Math.sqrt(playbackRate))}`;
};
exports.calculateATempo = calculateATempo;
@@ -0,0 +1,12 @@
import type { AudioOrVideoAsset } from 'remotion/no-react';
import type { FrameAndAssets } from '../render-frames';
import type { RenderMediaOnDownload } from './download-and-map-assets-to-file';
import type { DownloadMap } from './download-map';
export declare const convertAssetsToFileUrls: ({ assets, onDownload, downloadMap, indent, logLevel, binariesDirectory, }: {
assets: FrameAndAssets[];
onDownload: RenderMediaOnDownload;
downloadMap: DownloadMap;
indent: boolean;
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
binariesDirectory: string | null;
}) => Promise<AudioOrVideoAsset[][]>;
@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertAssetsToFileUrls = void 0;
const download_and_map_assets_to_file_1 = require("./download-and-map-assets-to-file");
const chunk = (input, size) => {
return input.reduce((arr, item, idx) => {
return idx % size === 0
? [...arr, [item]]
: [...arr.slice(0, -1), [...arr.slice(-1)[0], item]];
}, []);
};
const convertAssetsToFileUrls = async ({ assets, onDownload, downloadMap, indent, logLevel, binariesDirectory, }) => {
const chunks = chunk(assets, 1000);
const results = [];
for (const ch of chunks) {
const assetPromises = ch.map((frame) => {
const frameAssetPromises = frame.audioAndVideoAssets.map((a) => {
return (0, download_and_map_assets_to_file_1.downloadAndMapAssetsToFileUrl)({
renderAsset: a,
onDownload,
downloadMap,
indent,
logLevel,
binariesDirectory,
cancelSignalForAudioAnalysis: undefined,
shouldAnalyzeAudioImmediately: true,
});
});
return Promise.all(frameAssetPromises);
});
const result = await Promise.all(assetPromises);
results.push(result);
}
return results.flat(1);
};
exports.convertAssetsToFileUrls = convertAssetsToFileUrls;
@@ -0,0 +1,36 @@
import type { AudioOrVideoAsset } from 'remotion/no-react';
import type { CancelSignal } from '../make-cancel-signal';
import type { DownloadMap } from './download-map';
export type RenderMediaOnDownload = (src: string) => ((progress: {
percent: number | null;
downloaded: number;
totalSize: number | null;
}) => void) | undefined | void;
export declare const downloadAsset: ({ src, downloadMap, indent, logLevel, shouldAnalyzeAudioImmediately, binariesDirectory, cancelSignalForAudioAnalysis, audioStreamIndex, }: {
src: string;
downloadMap: DownloadMap;
indent: boolean;
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
shouldAnalyzeAudioImmediately: boolean;
binariesDirectory: string | null;
cancelSignalForAudioAnalysis: CancelSignal | undefined;
audioStreamIndex: number | undefined;
}) => Promise<string>;
export declare const markAllAssetsAsDownloaded: (downloadMap: DownloadMap) => void;
export declare const getSanitizedFilenameForAssetUrl: ({ src, downloadDir, contentDisposition, contentType, }: {
src: string;
downloadDir: string;
contentDisposition: string | null;
contentType: string | null;
}) => string;
export declare const downloadAndMapAssetsToFileUrl: ({ renderAsset, onDownload, downloadMap, logLevel, indent, binariesDirectory, cancelSignalForAudioAnalysis, shouldAnalyzeAudioImmediately, }: {
renderAsset: AudioOrVideoAsset;
onDownload: RenderMediaOnDownload | null;
downloadMap: DownloadMap;
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
indent: boolean;
shouldAnalyzeAudioImmediately: boolean;
binariesDirectory: string | null;
cancelSignalForAudioAnalysis: CancelSignal | undefined;
}) => Promise<AudioOrVideoAsset>;
export declare const attachDownloadListenerToEmitter: (downloadMap: DownloadMap, onDownload: RenderMediaOnDownload | null) => () => void;
@@ -0,0 +1,313 @@
"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,
abortSignal: downloadMap.cleanupController.signal,
});
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;
@@ -0,0 +1,19 @@
import type { LogLevel } from '../log-level';
type Response = {
sizeInBytes: number;
to: string;
};
type Options = {
url: string;
to: (contentDisposition: string | null, contentType: string | null) => string;
onProgress: ((progress: {
percent: number | null;
downloaded: number;
totalSize: number | null;
}) => void) | undefined;
logLevel: LogLevel;
indent: boolean;
abortSignal: AbortSignal;
};
export declare const downloadFile: (options: Options, retries?: number, attempt?: number) => Promise<Response>;
export {};
@@ -0,0 +1,150 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.downloadFile = void 0;
const node_fs_1 = require("node:fs");
const ensure_output_directory_1 = require("../ensure-output-directory");
const logger_1 = require("../logger");
const read_file_1 = require("./read-file");
const CANCELLED_ERROR = 'cancelled';
const incorrectContentLengthToken = 'Download finished with';
const downloadFileWithoutRetries = ({ onProgress, url, to: toFn, abortSignal, }) => {
return new Promise((resolve, reject) => {
let rejected = false;
let resolved = false;
let timeout;
const resolveAndFlag = (val) => {
abortSignal.removeEventListener('abort', onAbort);
resolved = true;
resolve(val);
if (timeout) {
clearTimeout(timeout);
}
};
const rejectAndFlag = (err) => {
abortSignal.removeEventListener('abort', onAbort);
if (timeout) {
clearTimeout(timeout);
}
reject(err);
rejected = true;
};
const refreshTimeout = () => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
if (resolved) {
return;
}
rejectAndFlag(new Error(`Tried to download file ${url}, but the server sent no data for 20 seconds`));
}, 20000);
};
refreshTimeout();
let finishEventSent = false;
let closeConnection = () => undefined;
const onAbort = () => {
rejectAndFlag(new Error(CANCELLED_ERROR));
closeConnection();
};
abortSignal.addEventListener('abort', onAbort);
(0, read_file_1.readFile)(url)
.then(({ response, request }) => {
var _a, _b;
closeConnection = () => {
request.destroy();
response.destroy();
};
if (abortSignal.aborted) {
onAbort();
return;
}
const contentDisposition = (_a = response.headers['content-disposition']) !== null && _a !== void 0 ? _a : null;
const contentType = (_b = response.headers['content-type']) !== null && _b !== void 0 ? _b : null;
const to = toFn(contentDisposition, contentType);
(0, ensure_output_directory_1.ensureOutputDirectory)(to);
const sizeHeader = response.headers['content-length'];
const totalSize = typeof sizeHeader === 'undefined' ? null : Number(sizeHeader);
const writeStream = (0, node_fs_1.createWriteStream)(to);
let downloaded = 0;
// Listen to 'close' event instead of more
// concise method to avoid this problem
// https://github.com/remotion-dev/remotion/issues/384#issuecomment-844398183
writeStream.on('close', () => {
if (rejected) {
return;
}
if (!finishEventSent) {
onProgress === null || onProgress === void 0 ? void 0 : onProgress({
downloaded,
percent: 1,
totalSize: downloaded,
});
}
refreshTimeout();
return resolveAndFlag({ sizeInBytes: downloaded, to });
});
writeStream.on('error', (err) => rejectAndFlag(err));
response.on('error', (err) => {
closeConnection();
rejectAndFlag(err);
});
response.pipe(writeStream).on('error', (err) => rejectAndFlag(err));
response.on('data', (d) => {
refreshTimeout();
downloaded += d.length;
refreshTimeout();
const percent = totalSize === null ? null : downloaded / totalSize;
onProgress === null || onProgress === void 0 ? void 0 : onProgress({
downloaded,
percent,
totalSize,
});
if (percent === 1) {
finishEventSent = true;
}
});
response.on('close', () => {
if (totalSize !== null && downloaded !== totalSize) {
rejectAndFlag(new Error(`${incorrectContentLengthToken} ${downloaded} bytes, but expected ${totalSize} bytes from 'Content-Length'.`));
}
writeStream.close();
closeConnection();
});
})
.catch((err) => {
rejectAndFlag(err);
});
});
};
const downloadFile = async (options, retries = 2, attempt = 1) => {
try {
const res = await downloadFileWithoutRetries(options);
return res;
}
catch (err) {
const { message } = err;
if (message === CANCELLED_ERROR) {
throw err;
}
if (message === 'aborted' ||
message.includes('ECONNRESET') ||
message.includes(incorrectContentLengthToken) ||
// Try again if hitting internal errors
message.includes('503') ||
message.includes('502') ||
message.includes('504') ||
message.includes('500')) {
if (retries === 0) {
throw err;
}
logger_1.Log.warn({ indent: options.indent, logLevel: options.logLevel }, `Downloading ${options.url} failed (will retry): ${message}`);
const backoffInSeconds = (attempt + 1) ** 2;
await new Promise((resolve) => {
setTimeout(() => resolve(), backoffInSeconds * 1000);
});
return (0, exports.downloadFile)(options, retries - 1, attempt + 1);
}
throw err;
}
};
exports.downloadFile = downloadFile;
@@ -0,0 +1,56 @@
import { OffthreadVideoServerEmitter } from '../offthread-video-server';
import type { FrameAndAssets } from '../render-frames';
import type { RenderMediaOnDownload } from './download-and-map-assets-to-file';
import { type InlineAudioMixing } from './inline-audio-mixing';
export type AudioChannelsAndDurationResultCache = {
channels: number;
duration: number | null;
startTime: number | null;
};
export type DownloadMap = {
id: string;
emitter: OffthreadVideoServerEmitter;
downloadListeners: RenderMediaOnDownload[];
isDownloadingMap: {
[src: string]: {
[downloadDir: string]: boolean;
} | undefined;
};
hasBeenDownloadedMap: {
[src: string]: {
[downloadDir: string]: string | null;
} | undefined;
};
listeners: {
[key: string]: {
[downloadDir: string]: (() => void)[];
};
};
durationOfAssetCache: Record<string, AudioChannelsAndDurationResultCache>;
downloadDir: string;
preEncode: string;
audioMixing: string;
complexFilter: string;
audioPreprocessing: string;
stitchFrames: string;
assetDir: string;
compositingDir: string;
preventCleanup: () => void;
allowCleanup: () => void;
isPreventedFromCleanup: () => boolean;
inlineAudioMixing: InlineAudioMixing;
cleanupController: AbortController;
};
export type RenderAssetInfo = {
assets: FrameAndAssets[];
imageSequenceName: string;
firstFrameIndex: number;
downloadMap: DownloadMap;
chunkLengthInSeconds: number;
trimLeftOffset: number;
trimRightOffset: number;
forSeamlessAacConcatenation: boolean;
};
export declare const makeAndReturn: (dir: string, name: string) => string;
export declare const makeDownloadMap: () => DownloadMap;
export declare const cleanDownloadMap: (downloadMap: DownloadMap) => void;
@@ -0,0 +1,65 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.cleanDownloadMap = exports.makeDownloadMap = exports.makeAndReturn = void 0;
const node_fs_1 = require("node:fs");
const node_path_1 = __importDefault(require("node:path"));
const version_1 = require("remotion/version");
const delete_directory_1 = require("../delete-directory");
const offthread_video_server_1 = require("../offthread-video-server");
const tmp_dir_1 = require("../tmp-dir");
const inline_audio_mixing_1 = require("./inline-audio-mixing");
const makeAndReturn = (dir, name) => {
const p = node_path_1.default.join(dir, name);
(0, node_fs_1.mkdirSync)(p);
return p;
};
exports.makeAndReturn = makeAndReturn;
const makeDownloadMap = () => {
const dir = (0, tmp_dir_1.tmpDir)(`remotion-v${version_1.VERSION}-assets`);
let prevented = false;
return {
isDownloadingMap: {},
hasBeenDownloadedMap: {},
listeners: {},
durationOfAssetCache: {},
id: String(Math.random()),
assetDir: dir,
downloadListeners: [],
downloadDir: (0, exports.makeAndReturn)(dir, 'remotion-assets-dir'),
complexFilter: (0, exports.makeAndReturn)(dir, 'remotion-complex-filter'),
preEncode: (0, exports.makeAndReturn)(dir, 'pre-encode'),
audioMixing: (0, exports.makeAndReturn)(dir, 'remotion-audio-mixing'),
audioPreprocessing: (0, exports.makeAndReturn)(dir, 'remotion-audio-preprocessing'),
stitchFrames: (0, exports.makeAndReturn)(dir, 'remotion-stitch-temp-dir'),
compositingDir: (0, exports.makeAndReturn)(dir, 'remotion-compositing-temp-dir'),
emitter: new offthread_video_server_1.OffthreadVideoServerEmitter(),
preventCleanup: () => {
prevented = true;
},
allowCleanup: () => {
prevented = false;
},
isPreventedFromCleanup: () => {
return prevented;
},
inlineAudioMixing: (0, inline_audio_mixing_1.makeInlineAudioMixing)(dir),
cleanupController: new AbortController(),
};
};
exports.makeDownloadMap = makeDownloadMap;
const cleanDownloadMap = (downloadMap) => {
if (downloadMap.isPreventedFromCleanup()) {
return;
}
downloadMap.cleanupController.abort();
(0, delete_directory_1.deleteDirectory)(downloadMap.downloadDir);
(0, delete_directory_1.deleteDirectory)(downloadMap.complexFilter);
(0, delete_directory_1.deleteDirectory)(downloadMap.compositingDir);
downloadMap.inlineAudioMixing.cleanup();
// Assets dir must be last since the others are contained
(0, delete_directory_1.deleteDirectory)(downloadMap.assetDir);
};
exports.cleanDownloadMap = cleanDownloadMap;
@@ -0,0 +1,12 @@
import type { AssetVolume } from './types';
type FfmpegEval = 'once' | 'frame';
type FfmpegVolumeExpression = {
eval: FfmpegEval;
value: string;
};
export declare const ffmpegVolumeExpression: ({ volume, fps, trimLeft, }: {
volume: AssetVolume;
trimLeft: number;
fps: number;
}) => FfmpegVolumeExpression;
export {};
@@ -0,0 +1,96 @@
"use strict";
// Example is here https://ffmpeg.org/ffmpeg-filters.html#volume
// Allowed syntax is here: https://ffmpeg.org/ffmpeg-utils.html#Expression-Evaluation
Object.defineProperty(exports, "__esModule", { value: true });
exports.ffmpegVolumeExpression = void 0;
// If once, ffmpeg evaluates volume expression once.
// If frame, it evaluates it for each frame
const round_volume_to_avoid_stack_overflow_1 = require("./round-volume-to-avoid-stack-overflow");
// In FFMPEG expressions, the current time is represented by 't'
// the `n` for the timestamp variable is buggy
const FFMPEG_TIME_VARIABLE = 't';
const ffmpegIfOrElse = (condition, then, elseDo) => {
return `if(${condition},${then},${elseDo})`;
};
const ffmpegIsOneOfFrames = ({ frames, trimLeft, fps, }) => {
const consecutiveArrays = [];
for (let i = 0; i < frames.length; i++) {
const previousFrame = frames[i - 1];
const frame = frames[i];
if (previousFrame === undefined || frame !== previousFrame + 1) {
consecutiveArrays.push([]);
}
consecutiveArrays[consecutiveArrays.length - 1].push(frame);
}
return consecutiveArrays
.map((f) => {
const firstFrame = f[0];
const lastFrame = f[f.length - 1];
const before = (firstFrame - 0.5) / fps;
const after = (lastFrame + 0.5) / fps;
return `between(${FFMPEG_TIME_VARIABLE},${(before + trimLeft).toFixed(4)},${(after + trimLeft).toFixed(4)})`;
})
.join('+');
};
const ffmpegBuildVolumeExpression = ({ arr, delay, fps, }) => {
if (arr.length === 0) {
throw new Error('Volume array expression should never have length 0');
}
if (arr.length === 1) {
return String(arr[0][0]);
}
const [first, ...rest] = arr;
const [volume, frames] = first;
return ffmpegIfOrElse(ffmpegIsOneOfFrames({ frames, trimLeft: delay, fps }), String(volume), ffmpegBuildVolumeExpression({ arr: rest, delay, fps }));
};
const ffmpegVolumeExpression = ({ volume, fps, trimLeft, }) => {
// If it's a static volume, we return it and tell
// FFMPEG it only has to evaluate it once
if (typeof volume === 'number') {
return {
eval: 'once',
value: String(volume),
};
}
if ([...new Set(volume)].length === 1) {
return (0, exports.ffmpegVolumeExpression)({
volume: volume[0],
fps,
trimLeft,
});
}
// A 1 sec video with frames 0-29 would mean that
// frame 29 corresponds to timestamp 0.966666...
// but the audio is actually 1 sec long. For that reason we pad the last
// timestamp.
const paddedVolume = [...volume, volume[volume.length - 1]];
// Otherwise, we construct an FFMPEG expression. First step:
// Make a map of all possible volumes
// {possibleVolume1} => [frame1, frame2]
// {possibleVolume2} => [frame3, frame4]
const volumeMap = {};
paddedVolume.forEach((baseVolume, frame) => {
// Adjust volume based on how many other tracks have not yet finished
const actualVolume = (0, round_volume_to_avoid_stack_overflow_1.roundVolumeToAvoidStackOverflow)(baseVolume);
if (!volumeMap[actualVolume]) {
volumeMap[actualVolume] = [];
}
volumeMap[actualVolume].push(frame);
});
// Sort the map so that the most common volume is last
// this is going to be the else statement so the expression is short
const volumeArray = Object.keys(volumeMap)
.map((key) => [Number(key), volumeMap[key]])
.sort((a, b) => a[1].length - b[1].length);
// Construct and tell FFMPEG it has to evaluate expression on each frame
const expression = ffmpegBuildVolumeExpression({
arr: volumeArray,
delay: trimLeft,
fps,
});
return {
eval: 'frame',
value: `'${expression}'`,
};
};
exports.ffmpegVolumeExpression = ffmpegVolumeExpression;
@@ -0,0 +1,3 @@
import type { AssetVolume, MediaAsset } from './types';
export declare const flattenVolumeArray: (volume: AssetVolume) => AssetVolume;
export declare const convertAssetToFlattenedVolume: (asset: MediaAsset) => MediaAsset;
@@ -0,0 +1,26 @@
"use strict";
// An input is an array with volume levels for each frame, like
// [1, 1, 1, 1, 1, ...].
// If all elements are the same, we flatten it to `1`, otherwise we leave it as an array.
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertAssetToFlattenedVolume = exports.flattenVolumeArray = void 0;
const flattenVolumeArray = (volume) => {
if (typeof volume === 'number') {
return volume;
}
if (volume.length === 0) {
throw new TypeError('Volume array must have at least 1 number');
}
if (new Set(volume).size === 1) {
return volume[0];
}
return volume;
};
exports.flattenVolumeArray = flattenVolumeArray;
const convertAssetToFlattenedVolume = (asset) => {
return {
...asset,
volume: (0, exports.flattenVolumeArray)(asset.volume),
};
};
exports.convertAssetToFlattenedVolume = convertAssetToFlattenedVolume;
@@ -0,0 +1,19 @@
import type { CancelSignal } from '../make-cancel-signal';
import type { AudioChannelsAndDurationResultCache, DownloadMap } from './download-map';
export declare const getAudioChannelsAndDurationWithoutCache: ({ src, indent, logLevel, binariesDirectory, cancelSignal, audioStreamIndex, }: {
src: string;
indent: boolean;
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
binariesDirectory: string | null;
cancelSignal: CancelSignal | undefined;
audioStreamIndex: number | undefined;
}) => Promise<AudioChannelsAndDurationResultCache>;
export declare const getAudioChannelsAndDuration: ({ downloadMap, src, indent, logLevel, binariesDirectory, cancelSignal, audioStreamIndex, }: {
downloadMap: DownloadMap;
src: string;
indent: boolean;
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
binariesDirectory: string | null;
cancelSignal: CancelSignal | undefined;
audioStreamIndex: number | undefined;
}) => Promise<AudioChannelsAndDurationResultCache>;
@@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAudioChannelsAndDuration = exports.getAudioChannelsAndDurationWithoutCache = void 0;
const call_ffmpeg_1 = require("../call-ffmpeg");
const p_limit_1 = require("../p-limit");
const limit = (0, p_limit_1.pLimit)(1);
const getAudioChannelsAndDurationWithoutCache = async ({ src, indent, logLevel, binariesDirectory, cancelSignal, audioStreamIndex, }) => {
const args = [
['-v', 'error'],
['-select_streams', audioStreamIndex ? `a:${audioStreamIndex}` : 'a:0'],
[
'-show_entries',
'stream=channels:stream=start_time:format=duration:format=format_name',
],
['-of', 'default=nw=1'],
[src],
]
.reduce((acc, val) => acc.concat(val), [])
.filter(Boolean);
try {
const task = await (0, call_ffmpeg_1.callFf)({
bin: 'ffprobe',
args,
indent,
logLevel,
binariesDirectory,
cancelSignal,
});
const channels = task.stdout.match(/channels=([0-9]+)/);
const duration = task.stdout.match(/duration=([0-9.]+)/);
const startTime = task.stdout.match(/start_time=([0-9.]+)/);
const container = task.stdout.match(/format_name=([a-zA-Z0-9.]+)/);
const isMP3 = container ? container[1] === 'mp3' : false;
const result = {
channels: channels ? parseInt(channels[1], 10) : 0,
duration: duration ? parseFloat(duration[1]) : null,
// We ignore the start time for MP3 because that is an inherent encoder thing
// not in the sense that we want
startTime: startTime ? (isMP3 ? 0 : parseFloat(startTime[1])) : null,
};
return result;
}
catch (err) {
if (err.message.includes('This file cannot be read by `ffprobe`. Is it a valid multimedia file?')) {
throw new Error('This file cannot be read by `ffprobe`. Is it a valid multimedia file?');
}
throw err;
}
};
exports.getAudioChannelsAndDurationWithoutCache = getAudioChannelsAndDurationWithoutCache;
async function getAudioChannelsAndDurationUnlimited({ downloadMap, src, indent, logLevel, binariesDirectory, cancelSignal, audioStreamIndex, }) {
const cacheKey = audioStreamIndex ? `${src}-${audioStreamIndex}` : src;
if (downloadMap.durationOfAssetCache[cacheKey]) {
return downloadMap.durationOfAssetCache[cacheKey];
}
const result = await (0, exports.getAudioChannelsAndDurationWithoutCache)({
src,
indent,
logLevel,
binariesDirectory,
cancelSignal,
audioStreamIndex,
});
downloadMap.durationOfAssetCache[cacheKey] = result;
return result;
}
const getAudioChannelsAndDuration = ({ downloadMap, src, indent, logLevel, binariesDirectory, cancelSignal, audioStreamIndex, }) => {
return limit(() => getAudioChannelsAndDurationUnlimited({
downloadMap,
src,
indent,
logLevel,
binariesDirectory,
cancelSignal,
audioStreamIndex,
}));
};
exports.getAudioChannelsAndDuration = getAudioChannelsAndDuration;
@@ -0,0 +1,21 @@
import type { InlineAudioAsset } from 'remotion/no-react';
import type { CancelSignal } from '../make-cancel-signal';
export declare const makeInlineAudioMixing: (dir: string) => {
cleanup: () => void;
addAsset: ({ asset, fps, totalNumberOfFrames, firstFrame, trimLeftOffset, trimRightOffset, }: {
asset: InlineAudioAsset;
fps: number;
totalNumberOfFrames: number;
firstFrame: number;
trimLeftOffset: number;
trimRightOffset: number;
}) => void;
getListOfAssets: () => string[];
finish: ({ binariesDirectory, indent, logLevel, cancelSignal, }: {
indent: boolean;
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
binariesDirectory: string | null;
cancelSignal: CancelSignal | undefined;
}) => Promise<void>;
};
export type InlineAudioMixing = ReturnType<typeof makeInlineAudioMixing>;
@@ -0,0 +1,200 @@
"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.makeInlineAudioMixing = void 0;
const node_fs_1 = __importStar(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const delete_directory_1 = require("../delete-directory");
const sample_rate_1 = require("../sample-rate");
const apply_tone_frequency_1 = require("./apply-tone-frequency");
const download_map_1 = require("./download-map");
const numberTo32BiIntLittleEndian = (num) => {
return new Uint8Array([
num & 0xff,
(num >> 8) & 0xff,
(num >> 16) & 0xff,
(num >> 24) & 0xff,
]);
};
const numberTo16BitLittleEndian = (num) => {
return new Uint8Array([num & 0xff, (num >> 8) & 0xff]);
};
/**
* When multiplying seconds by sample rate, floating point errors can cause
* results like 244799.99999999997 instead of 244800 (5.1 * 48000).
* This snaps to the nearest integer when within tolerance,
* preventing Math.floor/Math.ceil from rounding incorrectly.
*/
const correctFloatingPointError = (value) => {
const rounded = Math.round(value);
if (Math.abs(value - rounded) < 0.00001) {
return rounded;
}
return value;
};
const BIT_DEPTH = 16;
const BYTES_PER_SAMPLE = BIT_DEPTH / 8;
const NUMBER_OF_CHANNELS = 2;
const makeInlineAudioMixing = (dir) => {
const folderToAdd = (0, download_map_1.makeAndReturn)(dir, 'remotion-inline-audio-mixing');
// asset id -> file descriptor
const openFiles = {};
const writtenHeaders = {};
const toneFrequencies = {};
const cleanup = () => {
for (const fd of Object.values(openFiles)) {
try {
node_fs_1.default.closeSync(fd);
}
catch (_a) { }
}
(0, delete_directory_1.deleteDirectory)(folderToAdd);
};
const getListOfAssets = () => {
return Object.keys(openFiles);
};
const getFilePath = (asset) => {
return node_path_1.default.join(folderToAdd, `${asset.id}.wav`);
};
const ensureAsset = ({ asset, fps, totalNumberOfFrames, trimLeftOffset, trimRightOffset, }) => {
const filePath = getFilePath(asset);
if (!openFiles[filePath]) {
openFiles[filePath] = node_fs_1.default.openSync(filePath, 'w');
}
if (writtenHeaders[filePath]) {
return;
}
writtenHeaders[filePath] = true;
const expectedDataSize = Math.round((totalNumberOfFrames / fps - trimLeftOffset + trimRightOffset) *
NUMBER_OF_CHANNELS *
sample_rate_1.DEFAULT_SAMPLE_RATE *
BYTES_PER_SAMPLE);
const expectedSize = 40 + expectedDataSize;
const fd = openFiles[filePath];
(0, node_fs_1.writeSync)(fd, new Uint8Array([0x52, 0x49, 0x46, 0x46]), 0, 4, 0); // "RIFF"
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo32BiIntLittleEndian(expectedSize)), 0, 4, 4); // Remaining size
(0, node_fs_1.writeSync)(fd, new Uint8Array([0x57, 0x41, 0x56, 0x45]), 0, 4, 8); // "WAVE"
(0, node_fs_1.writeSync)(fd, new Uint8Array([0x66, 0x6d, 0x74, 0x20]), 0, 4, 12); // "fmt "
(0, node_fs_1.writeSync)(fd, new Uint8Array([BIT_DEPTH, 0x00, 0x00, 0x00]), 0, 4, 16); // fmt chunk size = 16
(0, node_fs_1.writeSync)(fd, new Uint8Array([0x01, 0x00]), 0, 2, 20); // Audio format (PCM) = 1, set 3 if float32 would be true
(0, node_fs_1.writeSync)(fd, new Uint8Array([NUMBER_OF_CHANNELS, 0x00]), 0, 2, 22); // Number of channels
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo32BiIntLittleEndian(sample_rate_1.DEFAULT_SAMPLE_RATE)), 0, 4, 24); // Sample rate
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo32BiIntLittleEndian(sample_rate_1.DEFAULT_SAMPLE_RATE * NUMBER_OF_CHANNELS * BYTES_PER_SAMPLE)), 0, 4, 28); // Byte rate
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo16BitLittleEndian(NUMBER_OF_CHANNELS * BYTES_PER_SAMPLE)), 0, 2, 32); // Block align
(0, node_fs_1.writeSync)(fd, numberTo16BitLittleEndian(BIT_DEPTH), 0, 2, 34); // Bits per sample
(0, node_fs_1.writeSync)(fd, new Uint8Array([0x64, 0x61, 0x74, 0x61]), 0, 4, 36); // "data"
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo32BiIntLittleEndian(expectedDataSize)), 0, 4, 40); // Remaining size
};
const finish = async ({ binariesDirectory, indent, logLevel, cancelSignal, }) => {
for (const fd of Object.keys(openFiles)) {
const frequency = toneFrequencies[fd];
if (frequency !== 1) {
const tmpFile = fd.replace(/.wav$/, '-tmp.wav');
await (0, apply_tone_frequency_1.applyToneFrequencyUsingFfmpeg)({
input: fd,
output: tmpFile,
toneFrequency: frequency,
indent,
logLevel,
binariesDirectory,
cancelSignal,
});
node_fs_1.default.renameSync(tmpFile, fd);
}
}
};
const addAsset = ({ asset, fps, totalNumberOfFrames, firstFrame, trimLeftOffset, trimRightOffset, }) => {
ensureAsset({
asset,
fps,
totalNumberOfFrames,
trimLeftOffset,
trimRightOffset,
});
const filePath = getFilePath(asset);
if (toneFrequencies[filePath] !== undefined &&
toneFrequencies[filePath] !== asset.toneFrequency) {
throw new Error(`toneFrequency must be the same across the entire audio, got ${asset.toneFrequency}, but before it was ${toneFrequencies[filePath]}`);
}
const fileDescriptor = openFiles[filePath];
toneFrequencies[filePath] = asset.toneFrequency;
let arr = new Int16Array(asset.audio);
const isFirst = asset.frame === firstFrame;
const isLast = asset.frame === totalNumberOfFrames + firstFrame - 1;
const samplesToShaveFromStart = trimLeftOffset * sample_rate_1.DEFAULT_SAMPLE_RATE;
const samplesToShaveFromEnd = trimRightOffset * sample_rate_1.DEFAULT_SAMPLE_RATE;
// Higher tolerance is needed for floating point videos
// Rendering https://github.com/remotion-dev/remotion/pull/5920 in native frame rate
// could hit this case
if (isFirst) {
arr = arr.slice(Math.floor(correctFloatingPointError(samplesToShaveFromStart)) *
NUMBER_OF_CHANNELS);
}
if (isLast) {
arr = arr.slice(0, arr.length +
Math.ceil(correctFloatingPointError(samplesToShaveFromEnd)) *
NUMBER_OF_CHANNELS);
}
const positionInSeconds = (asset.frame - firstFrame) / fps - (isFirst ? 0 : trimLeftOffset);
// Always rounding down to ensure there are no gaps when the samples don't align
// In @remotion/media, we also round down the sample start timestamp and round up the end timestamp
// This might lead to overlapping, hopefully aligning perfectly!
// Test case: https://github.com/remotion-dev/remotion/issues/5758
const position = Math.floor(correctFloatingPointError(positionInSeconds * sample_rate_1.DEFAULT_SAMPLE_RATE)) *
NUMBER_OF_CHANNELS *
BYTES_PER_SAMPLE;
(0, node_fs_1.writeSync)(
// fs
fileDescriptor,
// data
arr,
// offset of data
0,
// length
arr.byteLength,
// position
44 + position);
};
return {
cleanup,
addAsset,
getListOfAssets,
finish,
};
};
exports.makeInlineAudioMixing = makeInlineAudioMixing;
@@ -0,0 +1,7 @@
import http from 'node:http';
type NodeRequestAndResponse = {
request: http.ClientRequest;
response: http.IncomingMessage;
};
export declare const readFile: (url: string, redirectsSoFar?: number) => Promise<NodeRequestAndResponse>;
export {};
@@ -0,0 +1,97 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.readFile = void 0;
const https_1 = __importDefault(require("https"));
const node_http_1 = __importDefault(require("node:http"));
const redirect_status_codes_1 = require("../redirect-status-codes");
const truthy_1 = require("../truthy");
const getClient = (url) => {
if (url.startsWith('https://')) {
return https_1.default.get;
}
if (url.startsWith('http://')) {
return node_http_1.default.get;
}
throw new Error(`Can only download URLs starting with http:// or https://, got "${url}"`);
};
const readFileWithoutRedirect = (url) => {
return new Promise((resolve, reject) => {
const client = getClient(url);
const req = client(url,
// Bun 1.1.16 does not support the `headers` option
typeof Bun === 'undefined'
? {
headers: {
'user-agent': 'Mozilla/5.0 (@remotion/renderer - https://remotion.dev)',
},
}
: {}, (res) => {
resolve({ request: req, response: res });
});
req.on('error', (err) => {
req.destroy();
return reject(err);
});
});
};
const readFile = async (url, redirectsSoFar = 0) => {
if (redirectsSoFar > 10) {
throw new Error(`Too many redirects while downloading ${url}`);
}
const { request, response } = await readFileWithoutRedirect(url);
if (redirect_status_codes_1.redirectStatusCodes.includes(response.statusCode)) {
if (!response.headers.location) {
throw new Error(`Received a status code ${response.statusCode} but no "Location" header while calling ${response.headers.location}`);
}
const { origin } = new URL(url);
const redirectUrl = new URL(response.headers.location, origin).toString();
request.destroy();
response.destroy();
return (0, exports.readFile)(redirectUrl, redirectsSoFar + 1);
}
if (response.statusCode >= 400) {
const body = await tryToObtainBody(response);
request.destroy();
response.destroy();
throw new Error([
`Received a status code of ${response.statusCode} while downloading file ${url}.`,
body ? `The response body was:` : null,
body ? `---` : null,
body ? body : null,
body ? `---` : null,
]
.filter(truthy_1.truthy)
.join('\n'));
}
return { request, response };
};
exports.readFile = readFile;
const tryToObtainBody = async (file) => {
const success = new Promise((resolve) => {
let data = '';
file.on('data', (chunk) => {
data += chunk;
});
file.on('end', () => {
resolve(data);
});
// OK even when getting an error, this is just a best effort
file.on('error', () => resolve(data));
});
let timeout = null;
const body = await Promise.race([
success,
new Promise((resolve) => {
timeout = setTimeout(() => {
resolve(null);
}, 5000);
}),
]);
if (timeout) {
clearTimeout(timeout);
}
return body;
};
@@ -0,0 +1 @@
export declare const roundVolumeToAvoidStackOverflow: (volume: number) => number;
@@ -0,0 +1,17 @@
"use strict";
// FFMPEG allows expressions to have a maximum depth of 100.
// As defined here: https://github.com/FFmpeg/FFmpeg/blob/2014b0135293c41d261757bfa1aaba51653bab8e/libavutil/eval.c#L706
// With one depth added by the if/else statement,
// this means we can have a maximum of 99 different volumes.
// Therefore we round the volumes (which can only be between 0 and 1)
// so that there are only 99 possible values.
// We then subtract 1 again because FFMPEG is not precise and queries out of range
// values, for which we have to provide a default
Object.defineProperty(exports, "__esModule", { value: true });
exports.roundVolumeToAvoidStackOverflow = void 0;
const MAX_FFMPEG_STACK_DEPTH = 98;
const roundVolumeToAvoidStackOverflow = (volume) => {
return Number((Math.round(volume * (MAX_FFMPEG_STACK_DEPTH - 1)) /
(MAX_FFMPEG_STACK_DEPTH - 1)).toFixed(3));
};
exports.roundVolumeToAvoidStackOverflow = roundVolumeToAvoidStackOverflow;
@@ -0,0 +1 @@
export declare const sanitizeFilename: (input: string) => string;
@@ -0,0 +1,58 @@
"use strict";
// Inlined from https://github.com/parshap/node-sanitize-filename/blob/master/index.js
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeFilename = void 0;
const truncate_utf8_bytes_1 = require("./truncate-utf8-bytes");
/**
* Replaces characters in strings that are illegal/unsafe for filenames.
* Unsafe characters are either removed or replaced by a substitute set
* in the optional `options` object.
*
* Illegal Characters on Various Operating Systems
* / ? < > \ : * | "
* https://kb.acronis.com/content/39790
*
* Unicode Control codes
* C0 0x00-0x1f & C1 (0x80-0x9f)
* http://en.wikipedia.org/wiki/C0_and_C1_control_codes
*
* Reserved filenames on Unix-based systems (".", "..")
* Reserved filenames in Windows ("CON", "PRN", "AUX", "NUL", "COM1",
* "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
* "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", and
* "LPT9") case-insesitively and with or without filename extensions.
*
* Capped at 255 characters in length.
* http://unix.stackexchange.com/questions/32795/what-is-the-maximum-allowed-filename-and-folder-size-with-ecryptfs
*
* @param {String} input Original filename
* @param {Object} options {replacement: String | Function }
* @return {String} Sanitized filename
*/
const illegalRe = /[/?<>\\:*|"]/g;
// eslint-disable-next-line no-control-regex
const controlRe = /[\x00-\x1f\x80-\x9f]/g;
const reservedRe = /^\.+$/;
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
const windowsTrailingRe = /[. ]+$/;
function sanitize(input, replacement) {
if (typeof input !== 'string') {
throw new Error('Input must be string');
}
const sanitized = input
.replace(illegalRe, replacement)
.replace(controlRe, replacement)
.replace(reservedRe, replacement)
.replace(windowsReservedRe, replacement)
.replace(windowsTrailingRe, replacement);
return (0, truncate_utf8_bytes_1.truncateUtf8Bytes)(sanitized, 255);
}
const sanitizeFilename = (input) => {
const replacement = '';
const output = sanitize(input, replacement);
if (replacement === '') {
return output;
}
return sanitize(output, '');
};
exports.sanitizeFilename = sanitizeFilename;
@@ -0,0 +1 @@
export declare const sanitizeFilePath: (pathToSanitize: string) => string;
@@ -0,0 +1,16 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeFilePath = void 0;
const node_path_1 = __importDefault(require("node:path"));
const sanitize_filename_1 = require("./sanitize-filename");
const pathSeparators = /[/\\]/;
const sanitizeFilePath = (pathToSanitize) => {
return pathToSanitize
.split(pathSeparators)
.map((s) => (0, sanitize_filename_1.sanitizeFilename)(s))
.join(node_path_1.default.sep);
};
exports.sanitizeFilePath = sanitizeFilePath;
@@ -0,0 +1 @@
export declare function truncateUtf8Bytes(string: string, byteLength: number): string;
@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.truncateUtf8Bytes = truncateUtf8Bytes;
function isHighSurrogate(codePoint) {
return codePoint >= 0xd800 && codePoint <= 0xdbff;
}
function isLowSurrogate(codePoint) {
return codePoint >= 0xdc00 && codePoint <= 0xdfff;
}
const getLength = Buffer.byteLength.bind(Buffer);
function truncateUtf8Bytes(string, byteLength) {
if (typeof string !== 'string') {
throw new Error('Input must be string');
}
const charLength = string.length;
let curByteLength = 0;
let codePoint;
let segment;
for (let i = 0; i < charLength; i += 1) {
codePoint = string.charCodeAt(i);
segment = string[i];
if (isHighSurrogate(codePoint) &&
isLowSurrogate(string.charCodeAt(i + 1))) {
i += 1;
segment += string[i];
}
curByteLength += getLength(segment);
if (curByteLength === byteLength) {
return string.slice(0, i + 1);
}
if (curByteLength > byteLength) {
return string.slice(0, i - segment.length + 1);
}
}
return string;
}
@@ -0,0 +1,18 @@
import type { AudioOrVideoAsset } from 'remotion/no-react';
export type UnsafeAsset = Omit<AudioOrVideoAsset, 'frame' | 'id' | 'volume' | 'mediaFrame' | 'audioStartFrom'> & {
startInVideo: number;
duration: number | null;
trimLeft: number;
volume: number[];
id: string;
playbackRate: number;
toneFrequency: number;
audioStreamIndex: number;
};
export type AssetVolume = number | number[];
export type MediaAsset = Omit<UnsafeAsset, 'duration' | 'volume'> & {
duration: number;
volume: AssetVolume;
};
export declare const uncompressMediaAsset: (allRenderAssets: AudioOrVideoAsset[], assetToUncompress: AudioOrVideoAsset) => AudioOrVideoAsset;
export type Assets = MediaAsset[];
@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.uncompressMediaAsset = void 0;
const uncompressMediaAsset = (allRenderAssets, assetToUncompress) => {
const isCompressed = assetToUncompress.src.match(/same-as-(.*)-([0-9.]+)$/);
if (!isCompressed) {
return assetToUncompress;
}
const [, id, frame] = isCompressed;
const assetToFill = allRenderAssets.find((a) => a.id === id && String(a.frame) === frame);
if (!assetToFill) {
console.log('List of assets:');
console.log(allRenderAssets);
throw new TypeError(`Cannot uncompress asset, asset list seems corrupt. Please file a bug in the Remotion repo with the debug information above.`);
}
return {
...assetToUncompress,
src: assetToFill.src,
};
};
exports.uncompressMediaAsset = uncompressMediaAsset;