remote-rdr / proxy-renderer.js
shiveshnavin's picture
Update dockerfile
7e81bd7
import { renderFrames, renderStill, stitchFramesToVideo } from '@remotion/renderer';
import { bundle } from '@remotion/bundler';
import path from 'path';
import fs from 'fs';
import axios from 'axios';
import os from 'os'
import { exec, spawn } from 'child_process';
const { RenderUtils } = await import('./src/RenderUtils.cjs');
const { GenerateScript } = await import('./src/GenerateScript.cjs');
const originalManuScriptPath = path.join(process.cwd(), 'public/original_manuscript.json');
let cmd = `npm run preview`;
let childProcess = null;
var ffmpegLocation = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
export function startChildProcess() {
if (childProcess && !childProcess.killed) {
return childProcess;
}
const isWindows = os.platform() === 'win32';
childProcess = spawn(isWindows ? 'npm.cmd' : 'npm', ['run', 'preview'], { detached: true });
childProcess.on('error', (err) => {
console.error('Preview child process error:', err);
});
childProcess.on('close', (code, signal) => {
console.log('Render studio exited with', code, signal, '. Restarting in 2s...');
// clear reference so future callers can recreate
childProcess = null;
setTimeout(() => {
try {
startChildProcess();
} catch (e) {
console.error('Failed to restart preview process', e);
}
}, 2000);
});
return childProcess;
}
// start immediately
// startChildProcess();
export const renderProxy = async (outFile, jobId, options, controller) => {
const ScriptStr = fs.readFileSync(originalManuScriptPath);
const ScriptInput = JSON.parse(ScriptStr);
let {
duration,
Script,
} = GenerateScript(ScriptInput)
const composition = ScriptInput?.meta?.renderComposition;
duration = options?.duration || duration
const framesPerChunk = options?.framesPerChunk ?? 500;
let framesRendered = 0;
const chunkFiles = [];
while (framesRendered < duration) {
const endFrame = Math.min(framesRendered + framesPerChunk, duration);
const chunkOutFile = path.join(process.cwd(), `out/${jobId}-chunk-${framesRendered}-${endFrame}.mp4`);
chunkFiles.push(chunkOutFile)
if (fs.existsSync(chunkOutFile)) {
console.log(`Job ${jobId} chunk ${framesRendered}-${endFrame} already rendered. Skipping.`);
framesRendered = endFrame;
continue;
}
let retryAttemptsLeft = options?.retry ?? 1
while (retryAttemptsLeft >= 0) {
try {
await renderChunk(
{
...options,
startFrame: framesRendered,
endFrame: endFrame - 1,
outName: chunkOutFile,
},
composition,
duration,
chunkOutFile,
controller);
break;
} catch (error) {
console.error(`Render chunk failed. Retrying... (${retryAttemptsLeft - 1} attempts left)`);
if (controller._proxy_stopped) {
retryAttemptsLeft = 0
}
retryAttemptsLeft--;
if (retryAttemptsLeft === 0) {
throw error;
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
framesRendered = endFrame;
}
const ffmpegPath = ffmpegLocation;
const concatListPath = path.join(process.cwd(), 'out/concat_list.txt');
fs.writeFileSync(concatListPath, chunkFiles.map(f => `file '${f}'`).join('\n'));
const concatCmd = `${ffmpegPath} -f concat -safe 0 -i ${concatListPath} -c:v copy -c:a aac -b:a 192k -y ${outFile}`;
await new Promise((resolve, reject) => {
console.log('Stitching chunks...: ' + concatCmd)
const ffmpegProcess = spawn('npx', [
'remotion', 'ffmpeg',
'-f', 'concat',
'-safe', '0',
'-i', concatListPath,
'-c:v', 'copy',
'-c:a', 'aac',
'-b:a', '192k',
'-y',
outFile
], { detached: true });
if (controller) {
controller.stop = () => {
console.log('Stopping proxy render ffmpeg chunk join process');
try {
process.kill(-ffmpegProcess.pid, 'SIGKILL');
} catch (e) {
console.error(`Failed to kill process group ${-ffmpegProcess.pid}`, e);
ffmpegProcess.kill('SIGKILL');
}
}
}
ffmpegProcess.stdout.on('data', (data) => {
console.log(data.toString());
});
ffmpegProcess.stderr.on('data', (data) => {
console.error(data.toString());
});
ffmpegProcess.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`FFmpeg process exited with code ${code}`));
}
});
ffmpegProcess.on('error', (err) => {
reject(err);
});
});
// Clean up chunk files
for (const chunkFile of chunkFiles) {
// fs.unlinkSync(chunkFile);
}
fs.unlinkSync(concatListPath);
return outFile;
}
function renderChunk(
options,
composition,
duration,
finalOutFile,
controller
) {
console.log('Rendering chunk from frame', options.startFrame,
'to', options.endFrame,
'out of total duration', duration,
'to', finalOutFile);
return new Promise((async (resolve, reject) => {
const renderOptions = {
compositionId: composition,
startFrame: options?.startFrame ?? 0,
endFrame: options?.endFrame ?? duration - 1,
logLevel: options?.logLevel ?? "info",
type: options?.type ?? "video",
outName: finalOutFile ?? "out/output.mp4",
imageFormat: options?.imageFormat ?? "jpeg",
jpegQuality: options?.jpegQuality ?? 70,
scale: options?.scale ?? 1,
codec: options?.codec ?? "h264",
concurrency: options?.concurrency ?? 2,
crf: options?.crf ?? 18,
muted: options?.muted ?? false,
enforceAudioTrack: options?.enforceAudioTrack ?? false,
proResProfile: options?.proResProfile ?? null,
x264Preset: options?.x264Preset ?? "medium",
pixelFormat: options?.pixelFormat ?? "yuv420p",
audioBitrate: options?.audioBitrate ?? null,
videoBitrate: options?.videoBitrate ?? null,
everyNthFrame: options?.everyNthFrame ?? 1,
numberOfGifLoops: options?.numberOfGifLoops ?? null,
delayRenderTimeout: options?.delayRenderTimeout ?? 300000,
audioCodec: options?.audioCodec ?? "aac",
disallowParallelEncoding: options?.disallowParallelEncoding ?? false,
chromiumOptions: {
headless: options?.chromiumOptions?.headless ?? true,
disableWebSecurity: options?.chromiumOptions?.disableWebSecurity ?? false,
ignoreCertificateErrors: options?.chromiumOptions?.ignoreCertificateErrors ?? false,
gl: options?.chromiumOptions?.gl ?? null,
userAgent: options?.chromiumOptions?.userAgent ?? null,
enableMultiProcessOnLinux: options?.chromiumOptions?.enableMultiProcessOnLinux ?? true
},
envVariables: options?.envVariables ?? {},
// serializedInputPropsWithCustomSchema: options?.serializedInputPropsWithCustomSchema ?? JSON.stringify(Script),
offthreadVideoCacheSizeInBytes: options?.offthreadVideoCacheSizeInBytes ?? null,
offthreadVideoThreads: options?.offthreadVideoThreads ?? null,
colorSpace: options?.colorSpace ?? "default",
multiProcessOnLinux: options?.multiProcessOnLinux ?? true,
encodingBufferSize: options?.encodingBufferSize ?? null,
encodingMaxRate: options?.encodingMaxRate ?? null,
beepOnFinish: options?.beepOnFinish ?? false,
repro: options?.repro ?? false,
forSeamlessAacConcatenation: options?.forSeamlessAacConcatenation ?? false,
separateAudioTo: options?.separateAudioTo ?? null,
hardwareAcceleration: options?.hardwareAcceleration ?? "disable",
chromeMode: options?.chromeMode ?? "headless-shell"
};
console.log('Invoking studio with', renderOptions)
const proc = startChildProcess();
// sleep fo4 5 sec
await new Promise((resolve) => setTimeout(resolve, 5000));
axios.post('http://localhost:3000/api/render', renderOptions).then(resp => {
console.log('Studio started render', resp.data)
if (controller) {
controller.stop = () => {
controller._proxy_stopped = true
console.log('Stopping proxy render studio process');
try {
process.kill(-proc.pid, 'SIGKILL');
} catch (e) {
console.error(`Failed to kill process group ${-proc.pid}`, e);
proc.kill('SIGKILL');
}
}
}
let settled = false;
const cleanupHandlers = (() => {
try {
if (proc && proc.stdout && stdoutHandler) proc.stdout.removeListener('data', stdoutHandler);
} catch (e) { }
try {
if (proc && proc.stderr && stderrHandler) proc.stderr.removeListener('data', stderrHandler);
} catch (e) { }
}).bind(this);
const stdoutHandler = ((chunk) => {
const data = String(chunk);
console.log(data);
if (data.includes('Cleanup: Closing browser instance')) {
if (settled) return;
settled = true;
cleanupHandlers();
resolve(finalOutFile);
}
}).bind(this);
const stderrHandler = ((chunk) => {
const data = String(chunk);
console.error(data);
if (data.includes('Failed to render')) {
if (settled) return;
settled = true;
cleanupHandlers();
reject(new Error(data));
}
}).bind(this);
if (proc && proc.stdout) proc.stdout.on('data', stdoutHandler);
if (proc && proc.stderr) proc.stderr.on('data', stderrHandler);
// safety: if proc exits before we resolve, clean up and let the caller retry
const onProcClose = (() => {
if (settled) return;
settled = true;
cleanupHandlers();
reject(new Error('Preview process exited before render finished'));
}).bind(this);
proc.once('close', onProcClose);
}).catch(reject)
}).bind(this))
}