File size: 6,835 Bytes
926fa9d
 
5a54bc2
040aee9
926fa9d
 
59c0ef4
b27ecce
6306a39
93540c2
7f36d25
926fa9d
5a54bc2
 
 
 
59c0ef4
5a54bc2
 
 
 
 
 
 
 
 
 
 
 
 
 
926fa9d
5a54bc2
 
 
 
 
 
 
 
ef340fc
 
7ec5753
59c0ef4
7ec5753
59c0ef4
 
ef340fc
 
 
8df6cbf
 
926fa9d
8df6cbf
 
 
040aee9
 
2f9c678
 
 
3d8e650
2f9c678
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53c0795
 
 
6306a39
040aee9
93540c2
 
 
 
040aee9
6306a39
 
 
5a54bc2
2f9c678
 
 
 
 
 
 
00d8f4e
 
 
2f9c678
926fa9d
00d8f4e
 
926fa9d
 
 
2f9c678
20b3971
7f36d25
20b3971
040aee9
00d8f4e
040aee9
00d8f4e
 
 
 
 
 
 
7f36d25
 
 
 
 
 
 
00d8f4e
7f36d25
 
 
 
040aee9
 
 
5a54bc2
 
00d8f4e
 
7f36d25
 
00d8f4e
5a54bc2
 
 
040aee9
5a54bc2
2f9c678
 
5a54bc2
 
 
 
 
 
 
 
 
00d8f4e
20b3971
00d8f4e
 
 
7f36d25
00d8f4e
5a54bc2
 
 
7ec5753
5a54bc2
00d8f4e
 
 
 
 
5a54bc2
 
20b3971
 
 
7f36d25
20b3971
 
 
 
00d8f4e
 
 
 
 
5a54bc2
 
 
 
 
 
 
 
 
926fa9d
5a54bc2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
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) { }
}