@remotion/renderer
Version:
Render Remotion videos using Node.js or Bun
218 lines (217 loc) • 8.75 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.OffthreadVideoServerEmitter = exports.startOffthreadVideoServer = exports.extractUrlAndSourceFromUrl = void 0;
const node_url_1 = require("node:url");
const download_and_map_assets_to_file_1 = require("./assets/download-and-map-assets-to-file");
const compositor_1 = require("./compositor/compositor");
const log_level_1 = require("./log-level");
const logger_1 = require("./logger");
const offthreadvideo_cache_size_1 = require("./options/offthreadvideo-cache-size");
const extractUrlAndSourceFromUrl = (url) => {
const parsed = new URL(url, 'http://localhost');
const query = parsed.search;
if (!query.trim()) {
throw new Error('Expected query from ' + url);
}
const params = new node_url_1.URLSearchParams(query);
const src = params.get('src');
if (!src) {
throw new Error('Did not pass `src` parameter');
}
const time = params.get('time');
if (!time) {
throw new Error('Did not get `time` parameter');
}
const transparent = params.get('transparent');
const toneMapped = params.get('toneMapped');
if (!toneMapped) {
throw new Error('Did not get `toneMapped` parameter');
}
return {
src,
time: parseFloat(time),
transparent: transparent === 'true',
toneMapped: toneMapped === 'true',
};
};
exports.extractUrlAndSourceFromUrl = extractUrlAndSourceFromUrl;
const REQUEST_CLOSED_TOKEN = 'Request closed';
const startOffthreadVideoServer = ({ downloadMap, logLevel, indent, offthreadVideoCacheSizeInBytes, binariesDirectory, offthreadVideoThreads, }) => {
(0, offthreadvideo_cache_size_1.validateOffthreadVideoCacheSizeInBytes)(offthreadVideoCacheSizeInBytes);
const compositor = (0, compositor_1.startCompositor)({
type: 'StartLongRunningProcess',
payload: {
concurrency: offthreadVideoThreads,
maximum_frame_cache_size_in_bytes: offthreadVideoCacheSizeInBytes,
verbose: (0, log_level_1.isEqualOrBelowLogLevel)(logLevel, 'verbose'),
},
logLevel,
indent,
binariesDirectory,
});
return {
close: async () => {
// Note: This is being used as a promise:
// .close().then()
// but if finishCommands() fails, it acts like a sync function,
// therefore we have to catch an error and put a promise rejection
try {
await compositor.finishCommands();
return compositor.waitForDone();
}
catch (err) {
return Promise.reject(err);
}
},
listener: (req, response) => {
if (!req.url) {
throw new Error('Request came in without URL');
}
if (!req.url.startsWith('/proxy')) {
response.writeHead(404);
response.end();
return;
}
const { src, time, transparent, toneMapped } = (0, exports.extractUrlAndSourceFromUrl)(req.url);
response.setHeader('access-control-allow-origin', '*');
// Prevent caching of the response and excessive disk writes
// https://github.com/remotion-dev/remotion/issues/2760
response.setHeader('cache-control', 'no-cache, no-store, must-revalidate');
// Handling this case on Lambda:
// https://support.google.com/chrome/a/answer/7679408?hl=en
// Chrome sends Private Network Access preflights for subresources
if (req.method === 'OPTIONS') {
response.statusCode = 200;
if (req.headers['access-control-request-private-network']) {
response.setHeader('Access-Control-Allow-Private-Network', 'true');
}
response.end();
return;
}
let closed = false;
req.on('close', () => {
closed = true;
});
let extractStart = Date.now();
(0, download_and_map_assets_to_file_1.downloadAsset)({
src,
downloadMap,
indent,
logLevel,
binariesDirectory,
cancelSignalForAudioAnalysis: undefined,
shouldAnalyzeAudioImmediately: true,
audioStreamIndex: undefined,
})
.then((to) => {
return new Promise((resolve, reject) => {
if (closed) {
reject(Error(REQUEST_CLOSED_TOKEN));
return;
}
extractStart = Date.now();
compositor
.executeCommand('ExtractFrame', {
src: to,
original_src: src,
time,
transparent,
tone_mapped: toneMapped,
})
.then(resolve)
.catch(reject);
});
})
.then((readable) => {
return new Promise((resolve, reject) => {
if (closed) {
reject(Error(REQUEST_CLOSED_TOKEN));
return;
}
if (!readable) {
reject(new Error('no readable from compositor'));
return;
}
const extractEnd = Date.now();
const timeToExtract = extractEnd - extractStart;
if (timeToExtract > 1000) {
logger_1.Log.verbose({ indent, logLevel }, `Took ${timeToExtract}ms to extract frame from ${src} at ${time}`);
}
const firstByte = readable.at(0);
const secondByte = readable.at(1);
const thirdByte = readable.at(2);
const isPng = firstByte === 0x89 && secondByte === 0x50 && thirdByte === 0x4e;
const isBmp = firstByte === 0x42 && secondByte === 0x4d;
if (isPng) {
response.setHeader('content-type', `image/png`);
response.setHeader('content-length', readable.byteLength);
}
else if (isBmp) {
response.setHeader('content-type', `image/bmp`);
response.setHeader('content-length', readable.byteLength);
}
else {
reject(new Error(`Unknown file type: ${firstByte} ${secondByte} ${thirdByte}`));
return;
}
response.writeHead(200);
response.write(readable, (err) => {
response.end();
if (err) {
reject(err);
}
else {
resolve();
}
});
});
})
.catch((err) => {
logger_1.Log.error({ indent, logLevel }, 'Could not extract frame from compositor', err);
if (!response.headersSent) {
response.writeHead(500);
response.write(JSON.stringify({ error: err.stack }));
}
response.end();
});
},
compositor,
};
};
exports.startOffthreadVideoServer = startOffthreadVideoServer;
class OffthreadVideoServerEmitter {
constructor() {
this.listeners = {
progress: [],
download: [],
};
}
addEventListener(name, callback) {
this.listeners[name].push(callback);
return () => {
this.removeEventListener(name, callback);
};
}
removeEventListener(name, callback) {
this.listeners[name] = this.listeners[name].filter((l) => l !== callback);
}
dispatchEvent(dispatchName, context) {
this.listeners[dispatchName].forEach((callback) => {
callback({ detail: context });
});
}
dispatchDownloadProgress(src, percent, downloaded, totalSize) {
this.dispatchEvent('progress', {
downloaded,
percent,
totalSize,
src,
});
}
dispatchDownload(src) {
this.dispatchEvent('download', {
src,
});
}
}
exports.OffthreadVideoServerEmitter = OffthreadVideoServerEmitter;