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)) }