Spaces:
Running
Running
| 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)) | |
| } | |