Spaces:
Running
Running
| import { copyFileSync, existsSync, readdirSync, unlinkSync } from 'fs'; | |
| import { join } from 'path'; | |
| import pkg from 'common-utils'; | |
| import { exec, spawn } from 'child_process'; | |
| import { dirname } from 'path'; | |
| import { fileURLToPath } from 'url'; | |
| import { platform } from 'os'; | |
| import { renderSSR } from './ssr.js'; | |
| import path from 'path'; | |
| import { renderProxy } from './proxy-renderer.js'; | |
| import { ProcessKiller } from './utils/ProcessKiller.js'; | |
| const { UnzipFiles, Utils, ZipFiles } = pkg; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = dirname(__filename); | |
| export async function explodeUrl(fileUrl, jobId, dir, zipFile) { | |
| await Utils.downloadFile(fileUrl, zipFile, true); | |
| await UnzipFiles(zipFile, dir); | |
| } | |
| export async function listOutputFiles(jobId) { | |
| let outDir = join(__dirname, 'out'); | |
| let manuFile = join(__dirname, `public/original_manuscript.json`); | |
| copyFileSync(manuFile, join(__dirname, 'out', `original_manuscript.json`)); | |
| let outputFiles = readdirSync(outDir).map((fname) => { | |
| const filePath = join(outDir, fname); | |
| return filePath; | |
| }); | |
| return outputFiles; | |
| } | |
| export async function generateOutputBundle(jobId, outputFiles) { | |
| let outFile = join(__dirname, 'out', `output-${jobId}.zip`); | |
| if (existsSync(outFile)) { | |
| unlinkSync(outFile); | |
| } | |
| await ZipFiles(outputFiles, outFile); | |
| return outFile; | |
| } | |
| export function getNpmScript(mediaType) { | |
| if (mediaType === 'image') { | |
| return 'still'; | |
| } else if (!mediaType || mediaType === 'video') { | |
| return 'render'; | |
| } else { | |
| return mediaType; | |
| } | |
| } | |
| export async function doRender( | |
| jobId, | |
| originalManuscript, | |
| sendToObserver, | |
| target = 'render', | |
| ssrOptions, | |
| proxyOptions, | |
| controller) { | |
| const composition = originalManuscript?.meta?.renderComposition || 'SemibitComposition'; | |
| // Determine file extension based on codec | |
| let defaultBuildParams = ` --audio-codec mp3 --image-format=jpeg --enable-multi-process-on-linux --quality=70 --timeout 60000 --concurrency 1 --gl=angle `; | |
| let tempBuildParams = ' ' + (originalManuscript?.meta?.generationConfig?.extras?.buildParams || defaultBuildParams) + ' '; | |
| if (originalManuscript?.meta?.generationConfig?.extras?.additionalBuildParams) { | |
| tempBuildParams += ' ' + originalManuscript?.meta?.generationConfig?.extras?.additionalBuildParams + ' '; | |
| } | |
| // Extract codec from build params to determine file extension | |
| const codecMatch = tempBuildParams.match(/--codec[=\s]+([^\s]+)/); | |
| const codec = codecMatch ? codecMatch[1] : 'h264'; | |
| let videoExtension = '.mp4'; | |
| if (codec === 'vp8' || codec === 'vp9') { | |
| videoExtension = '.webm'; | |
| } else if (codec === 'prores') { | |
| videoExtension = '.mov'; | |
| } | |
| let outFile = path.join(process.cwd(), `out`, `${jobId}-video${videoExtension}`); | |
| if (target.includes('still')) { | |
| outFile = path.join(process.cwd(), `out`, `${jobId}-still.jpg`); | |
| } | |
| if (ssrOptions) { | |
| await renderSSR(outFile, ssrOptions.startFrame, ssrOptions.endFrame, controller) | |
| sendToObserver(jobId, 'completed'); | |
| return outFile | |
| } | |
| else if (proxyOptions) { | |
| await renderProxy(outFile, jobId, proxyOptions, controller) | |
| sendToObserver(jobId, 'completed'); | |
| return outFile | |
| } | |
| const renderComposition = composition || 'SemibitComposition'; | |
| // Use the build params we computed above for codec detection | |
| const buildParams = tempBuildParams; | |
| // Directly call remotion instead of going through npm scripts to avoid parameter parsing issues | |
| const remotionArgs = ['-y', 'remotion', target, ...buildParams.trim().split(/\s+/).filter(arg => arg), renderComposition, outFile]; | |
| const cmd = `npx ${remotionArgs.join(' ')}`; | |
| const spawnOptions = { | |
| detached: true, // Always detach to create new process group | |
| shell: true, // Use shell for proper parameter parsing | |
| stdio: ['ignore', 'pipe', 'pipe'] // Ensure we can capture output | |
| }; | |
| // On Unix systems, create a new process group for easier cleanup | |
| if (platform() !== 'win32') { | |
| spawnOptions.detached = true; | |
| } | |
| const childProcess = spawn('npx', remotionArgs, spawnOptions); | |
| let isProcessKilled = false; | |
| const processKiller = new ProcessKiller(); | |
| if (controller && controller.stop) { | |
| controller.stop = async () => { | |
| console.log('Stopping render studio cli process'); | |
| if (isProcessKilled) { | |
| console.log('Process already terminated'); | |
| return; | |
| } | |
| isProcessKilled = true; | |
| // Use the ProcessKiller utility for comprehensive termination | |
| const success = await processKiller.terminateProcess(childProcess.pid, { | |
| gracefulTimeout: 2000, | |
| forceTimeout: 1000, | |
| processPattern: "remotion.*render", | |
| onProgress: (message) => console.log(message) | |
| }); | |
| if (success) { | |
| console.log('Process termination completed'); | |
| } else { | |
| console.log('Process termination attempted (may have already been terminated)'); | |
| } | |
| } | |
| } | |
| console.log('Starting video render. ' + cmd); | |
| console.log(`Spawned process with PID: ${childProcess.pid}`); | |
| // Track the process for cleanup | |
| processKiller.trackPid(childProcess.pid); | |
| let updateCounter = 0; | |
| childProcess.stdout.on('data', (data) => { | |
| sendToObserver(jobId, data); | |
| if (!process.env.is_pm2) console.log(data?.toString()); | |
| if (updateCounter++ % 100 == 0 || updateCounter < 5) { | |
| if (data?.split?.('\n')?.[0]) | |
| console.log(data?.split?.('\n')?.[0]); | |
| } | |
| }); | |
| childProcess.stderr.on('data', (data) => { | |
| sendToObserver(jobId, data); | |
| console.error(data.toString()); | |
| }); | |
| return new Promise((resolve, reject) => { | |
| childProcess.on('close', (code, signal) => { | |
| isProcessKilled = true; // Mark process as terminated | |
| console.log(`Render process closed with code: ${code}, signal: ${signal}`); | |
| // Clean up tracked PIDs | |
| processKiller.clearTrackedPids(); | |
| sendToObserver(jobId, code === 0 ? 'completed' : 'failed'); | |
| if (code === 0) { | |
| resolve(outFile); | |
| console.log(`'${target}' completed successfully.`); | |
| } else { | |
| const message = signal === 'SIGTERM' || signal === 'SIGKILL' ? | |
| `'${target}' was terminated by user request.` : | |
| `'${target}' failed with code ${code}.`; | |
| reject({ message }); | |
| console.error(message); | |
| } | |
| }); | |
| childProcess.on('error', (error) => { | |
| isProcessKilled = true; // Mark process as terminated | |
| processKiller.clearTrackedPids(); | |
| console.error('Child process error:', error); | |
| sendToObserver(jobId, 'failed'); | |
| reject(error); | |
| }); | |
| // Handle process exit | |
| childProcess.on('exit', (code, signal) => { | |
| console.log(`Render process exited with code: ${code}, signal: ${signal}`); | |
| }); | |
| }); | |
| } | |
| export function clear(skipOutput, skipPublic, skipUploads) { | |
| try { | |
| const preserve = ['assets', 'mp3']; | |
| if (!skipPublic) Utils.clearFolder(join(__dirname, './public'), preserve); | |
| if (!skipOutput) Utils.clearFolder(join(__dirname, './out')); | |
| if (!skipUploads) Utils.clearFolder(join(__dirname, './uploads')); | |
| } catch (e) { } | |
| } | |