Spaces:
Running
Running
File size: 6,394 Bytes
6b85c8b 1bc35d7 51570c3 0f9c090 6b85c8b 78aa4a5 7ad7585 78aa4a5 6b85c8b 926fa9d ffef181 1c1cec6 ffef181 51570c3 6b85c8b 926fa9d 51570c3 d480910 51570c3 d480910 51570c3 d480910 0f9c090 4665111 d480910 4665111 d480910 0f9c090 d480910 51570c3 6b85c8b d480910 6b85c8b |
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 |
import _ from 'lodash'
import fs from 'fs'
import { Plugin } from './plugin.js';
import { exec, spawn } from 'child_process'
import _path from 'path';
import { PerformanceRecorder } from 'common-utils';
/**
* SplitRenderPlugin must be used with transparent-background
* when the Remotion renders the captions on a black background.
* This plugin will then composite the transparent video over
* the actual media files thus combining them into the final output.
*/
export class SplitRenderPlugin extends Plugin {
constructor(name, options) {
super(name, options);
}
async applyPrerender(originalManuscript, jobId) {
_.set(
originalManuscript,
'meta.generationConfig.extras.buildParams',
`--codec=prores --prores-profile=4444 --pixel-format=yuva444p10le --transparent --image-format=png --concurrency=2 --hardware-acceleration=if-possible --enable-multiprocess-on-linux --offthreadvideo-cache-size-in-bytes=8589934592`
);
originalManuscript.transcript.forEach(element => {
element._mediaAbsPaths = _.cloneDeep(element.mediaAbsPaths)
element.mediaAbsPaths = []
});
}
async applyPostrender(originalManuscript, jobId, outFiles) {
return new Promise((resolve, reject) => {
const outFile = outFiles.find(f => f.includes('.webm')) || outFiles.find(f => f.includes('.mp4')) || outFiles.find(f => f.includes('.mov'));
if (!outFile || !fs.existsSync(outFile)) return resolve('No output file found');
const perf = new PerformanceRecorder();
// 1) Probe overlay duration (master duration)
const getVideoDuration = `ffprobe -v quiet -show_entries format=duration -of csv=p=0 "${outFile}"`;
exec(getVideoDuration, (error, stdout) => {
if (error) {
console.error(`[SplitRenderPlugin] Error getting overlay duration: ${error.message}`);
return reject(error);
}
const overlayDuration = parseFloat(stdout.trim());
console.log(`[SplitRenderPlugin] Overlay duration: ${overlayDuration} seconds`);
// Build tokenized ffmpeg args (DON'T concat strings; keep tokens to preserve paths)
const ffmpegArgs = [];
const bgInputIndices = [];
let inputIndex = 0;
originalManuscript.transcript.forEach(scene => {
let durationInSeconds = scene.durationInSeconds;
scene.mediaAbsPaths = _.cloneDeep(scene._mediaAbsPaths);
delete scene._mediaAbsPaths;
for (let i = 0; i < scene.mediaAbsPaths.length; i++) {
const { path, type, dimensions, durationSec } = scene.mediaAbsPaths[i];
const fileName = _path.basename(path);
const publicFilePath = `public/${fileName}`;
if (type === 'video') {
const clipDuration = Math.min(durationSec, durationInSeconds, overlayDuration);
ffmpegArgs.push('-ss', '0', '-t', String(clipDuration), '-i', publicFilePath);
bgInputIndices.push(inputIndex++);
} else if (type === 'image') {
const clipDuration = Math.min(durationInSeconds, overlayDuration);
ffmpegArgs.push('-loop', '1', '-ss', '0', '-t', String(clipDuration), '-i', publicFilePath);
bgInputIndices.push(inputIndex++);
}
}
});
// Add the front/overlay video last; this controls duration
ffmpegArgs.push('-ss', '0', '-t', String(overlayDuration), '-i', outFile);
const frontIdx = inputIndex++; // last added input is the overlay
if (bgInputIndices.length === 0) {
return resolve('No input files to process. Skipping split-render post process');
}
// 2) Build filter_complex:
// - Concatenate all background clips -> [bg]
// - Key out black from overlay -> [fg]
// - Overlay fg on bg, keep bg playing if fg ends -> eof_action=pass
const concatInputs = bgInputIndices.map(i => `[${i}:v]`).join('');
const concatFilter = `${concatInputs}concat=n=${bgInputIndices.length}:v=1:a=0[bg]`;
const chromaKey = `[${frontIdx}:v]colorkey=0x000000:0.08:0.02[fg]`;
const overlay = `[bg][fg]overlay=0:0:format=auto:eof_action=pass`;
const filterComplex = `${concatFilter};${chromaKey};${overlay}`;
const finalOutFile = `out/final_${jobId}.mp4`;
const fullArgs = [
...ffmpegArgs,
'-filter_complex', filterComplex,
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-y', finalOutFile,
];
console.log('[SplitRenderPlugin] Running ffmpeg with args:', fullArgs);
const ffmpegProcess = spawn('ffmpeg', fullArgs);
let stdoutBuf = '';
let stderrBuf = '';
ffmpegProcess.stdout.on('data', (data) => {
const output = data.toString();
stdoutBuf += output;
console.log('[SplitRenderPlugin] [FFmpeg STDOUT]:', output.trim());
});
ffmpegProcess.stderr.on('data', (data) => {
const output = data.toString();
stderrBuf += output;
console.log('[SplitRenderPlugin] [FFmpeg STDERR]:', output.trim());
});
ffmpegProcess.on('close', (code) => {
if (code !== 0) {
const fullErrorLog = `FFmpeg process failed with code ${code}\n\nSTDOUT:\n${stdoutBuf}\n\nSTDERR:\n${stderrBuf}`;
console.error('[SplitRenderPlugin] FFmpeg failed:', fullErrorLog);
return reject(new Error(fullErrorLog));
}
try {
if (fs.existsSync(outFile)) fs.unlinkSync(outFile);
console.log('[SplitRenderPlugin] Removed initial render file:', outFile);
} catch (e) {
console.warn('[SplitRenderPlugin] Could not remove initial render file:', e.message);
}
console.log('[SplitRenderPlugin] FFmpeg process completed successfully, took ', perf.elapsedString());
console.log('[SplitRenderPlugin] Final output file:', finalOutFile);
resolve('FFmpeg completed successfully');
});
ffmpegProcess.on('error', (err) => {
console.error('[SplitRenderPlugin] FFmpeg spawn error:', err);
reject(err);
});
});
});
}
} |