remote-rdr / renderer.js
shiveshnavin's picture
Fixes
3d8e650
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) { }
}