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);
        });
      });
    });
  }

}