File size: 9,945 Bytes
93540c2
040aee9
93540c2
 
 
9a00d8d
 
3903eb1
122af68
93540c2
 
 
 
9a00d8d
4983042
122af68
4983042
7e81bd7
4983042
 
 
 
040aee9
 
4983042
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122af68
 
4983042
d969d2b
040aee9
93540c2
 
 
 
 
 
9a00d8d
122af68
040aee9
90db87a
d8115ec
92ab15b
d8115ec
 
1ebbab2
92ab15b
1ebbab2
 
 
 
 
d8115ec
 
 
 
 
 
 
 
 
 
 
 
040aee9
 
d8115ec
 
7e81bd7
040aee9
 
 
d8115ec
 
 
 
 
 
 
 
 
 
 
122af68
d8115ec
 
 
92ab15b
 
d8115ec
1ebbab2
122af68
 
3903eb1
 
 
 
 
 
 
 
040aee9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3903eb1
 
 
 
 
 
 
 
 
 
 
 
 
d8115ec
3903eb1
 
 
 
d8115ec
 
 
 
 
92ab15b
d8115ec
 
 
 
13a8267
 
 
 
 
 
 
 
040aee9
 
13a8267
d8115ec
 
 
 
d969d2b
9a00d8d
 
 
 
87967b7
9a00d8d
13a8267
9a00d8d
 
93540c2
 
9a00d8d
 
f2e9e21
9a00d8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fcebca2
9a00d8d
 
 
 
 
 
 
 
 
 
 
 
 
 
040aee9
 
d969d2b
0d87392
ad7098d
9a00d8d
 
040aee9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4983042
791ae61
239231e
4983042
7e81bd7
239231e
4983042
7e81bd7
791ae61
4983042
791ae61
4983042
 
 
 
 
 
13a8267
4983042
791ae61
4983042
791ae61
4983042
 
 
 
 
 
 
 
791ae61
4983042
 
 
 
 
791ae61
4983042
 
 
 
791ae61
4983042
 
 
9a00d8d
791ae61
13a8267
040aee9
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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import { renderFrames, renderStill, stitchFramesToVideo } from '@remotion/renderer';

import { bundle } from '@remotion/bundler';
import path from 'path';
import fs from 'fs';
import axios from 'axios';
import os from 'os'
import { exec, spawn } from 'child_process';

const { RenderUtils } = await import('./src/RenderUtils.cjs');
const { GenerateScript } = await import('./src/GenerateScript.cjs');

const originalManuScriptPath = path.join(process.cwd(), 'public/original_manuscript.json');
let cmd = `npm run preview`;
let childProcess = null;
var ffmpegLocation = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';

export function startChildProcess() {
  if (childProcess && !childProcess.killed) {
    return childProcess;
  }

  const isWindows = os.platform() === 'win32';
  childProcess = spawn(isWindows ? 'npm.cmd' : 'npm', ['run', 'preview'], { detached: true });

  childProcess.on('error', (err) => {
    console.error('Preview child process error:', err);
  });

  childProcess.on('close', (code, signal) => {
    console.log('Render studio exited with', code, signal, '. Restarting in 2s...');
    // clear reference so future callers can recreate
    childProcess = null;
    setTimeout(() => {
      try {
        startChildProcess();
      } catch (e) {
        console.error('Failed to restart preview process', e);
      }
    }, 2000);
  });

  return childProcess;
}



// start immediately
// startChildProcess();
export const renderProxy = async (outFile, jobId, options, controller) => {
  const ScriptStr = fs.readFileSync(originalManuScriptPath);
  const ScriptInput = JSON.parse(ScriptStr);
  let {
    duration,
    Script,
  } = GenerateScript(ScriptInput)
  const composition = ScriptInput?.meta?.renderComposition;
  duration = options?.duration || duration

  const framesPerChunk = options?.framesPerChunk ?? 500;
  let framesRendered = 0;
  const chunkFiles = [];
  while (framesRendered < duration) {
    const endFrame = Math.min(framesRendered + framesPerChunk, duration);
    const chunkOutFile = path.join(process.cwd(), `out/${jobId}-chunk-${framesRendered}-${endFrame}.mp4`);
    chunkFiles.push(chunkOutFile)
    if (fs.existsSync(chunkOutFile)) {
      console.log(`Job ${jobId} chunk ${framesRendered}-${endFrame} already rendered. Skipping.`);
      framesRendered = endFrame;
      continue;
    }
    let retryAttemptsLeft = options?.retry ?? 1
    while (retryAttemptsLeft >= 0) {
      try {
        await renderChunk(
          {
            ...options,
            startFrame: framesRendered,
            endFrame: endFrame - 1,
            outName: chunkOutFile,
          },
          composition,
          duration,
          chunkOutFile,
          controller);
        break;
      } catch (error) {
        console.error(`Render chunk failed. Retrying... (${retryAttemptsLeft - 1} attempts left)`);
        if (controller._proxy_stopped) {
          retryAttemptsLeft = 0
        }
        retryAttemptsLeft--;
        if (retryAttemptsLeft === 0) {
          throw error;
        }
        await new Promise(resolve => setTimeout(resolve, 2000));
      }
    }
    framesRendered = endFrame;
  }


  const ffmpegPath = ffmpegLocation;
  const concatListPath = path.join(process.cwd(), 'out/concat_list.txt');
  fs.writeFileSync(concatListPath, chunkFiles.map(f => `file '${f}'`).join('\n'));

  const concatCmd = `${ffmpegPath} -f concat -safe 0 -i ${concatListPath} -c:v copy -c:a aac -b:a 192k -y ${outFile}`;

  await new Promise((resolve, reject) => {
    console.log('Stitching chunks...: ' + concatCmd)
    const ffmpegProcess = spawn('npx', [
      'remotion', 'ffmpeg',
      '-f', 'concat',
      '-safe', '0',
      '-i', concatListPath,
      '-c:v', 'copy',
      '-c:a', 'aac',
      '-b:a', '192k',
      '-y',
      outFile
    ], { detached: true });


    if (controller) {
      controller.stop = () => {
        console.log('Stopping proxy render ffmpeg chunk join process');
        try {
          process.kill(-ffmpegProcess.pid, 'SIGKILL');
        } catch (e) {
          console.error(`Failed to kill process group ${-ffmpegProcess.pid}`, e);
          ffmpegProcess.kill('SIGKILL');
        }
      }
    }


    ffmpegProcess.stdout.on('data', (data) => {
      console.log(data.toString());
    });

    ffmpegProcess.stderr.on('data', (data) => {
      console.error(data.toString());
    });

    ffmpegProcess.on('close', (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`FFmpeg process exited with code ${code}`));
      }
    });

    ffmpegProcess.on('error', (err) => {
      reject(err);
    });
  });

  // Clean up chunk files
  for (const chunkFile of chunkFiles) {
    // fs.unlinkSync(chunkFile);
  }
  fs.unlinkSync(concatListPath);

  return outFile;
}



function renderChunk(
  options,
  composition,
  duration,
  finalOutFile,
  controller
) {
  console.log('Rendering chunk from frame', options.startFrame,
    'to', options.endFrame,
    'out of total duration', duration,
    'to', finalOutFile);
  return new Promise((async (resolve, reject) => {
    const renderOptions = {
      compositionId: composition,
      startFrame: options?.startFrame ?? 0,
      endFrame: options?.endFrame ?? duration - 1,
      logLevel: options?.logLevel ?? "info",
      type: options?.type ?? "video",
      outName: finalOutFile ?? "out/output.mp4",
      imageFormat: options?.imageFormat ?? "jpeg",
      jpegQuality: options?.jpegQuality ?? 70,


      scale: options?.scale ?? 1,
      codec: options?.codec ?? "h264",
      concurrency: options?.concurrency ?? 2,
      crf: options?.crf ?? 18,
      muted: options?.muted ?? false,
      enforceAudioTrack: options?.enforceAudioTrack ?? false,
      proResProfile: options?.proResProfile ?? null,
      x264Preset: options?.x264Preset ?? "medium",
      pixelFormat: options?.pixelFormat ?? "yuv420p",
      audioBitrate: options?.audioBitrate ?? null,
      videoBitrate: options?.videoBitrate ?? null,
      everyNthFrame: options?.everyNthFrame ?? 1,
      numberOfGifLoops: options?.numberOfGifLoops ?? null,
      delayRenderTimeout: options?.delayRenderTimeout ?? 300000,
      audioCodec: options?.audioCodec ?? "aac",
      disallowParallelEncoding: options?.disallowParallelEncoding ?? false,
      chromiumOptions: {
        headless: options?.chromiumOptions?.headless ?? true,
        disableWebSecurity: options?.chromiumOptions?.disableWebSecurity ?? false,
        ignoreCertificateErrors: options?.chromiumOptions?.ignoreCertificateErrors ?? false,
        gl: options?.chromiumOptions?.gl ?? null,
        userAgent: options?.chromiumOptions?.userAgent ?? null,
        enableMultiProcessOnLinux: options?.chromiumOptions?.enableMultiProcessOnLinux ?? true
      },
      envVariables: options?.envVariables ?? {},
      // serializedInputPropsWithCustomSchema: options?.serializedInputPropsWithCustomSchema ?? JSON.stringify(Script),
      offthreadVideoCacheSizeInBytes: options?.offthreadVideoCacheSizeInBytes ?? null,
      offthreadVideoThreads: options?.offthreadVideoThreads ?? null,
      colorSpace: options?.colorSpace ?? "default",
      multiProcessOnLinux: options?.multiProcessOnLinux ?? true,
      encodingBufferSize: options?.encodingBufferSize ?? null,
      encodingMaxRate: options?.encodingMaxRate ?? null,
      beepOnFinish: options?.beepOnFinish ?? false,
      repro: options?.repro ?? false,
      forSeamlessAacConcatenation: options?.forSeamlessAacConcatenation ?? false,
      separateAudioTo: options?.separateAudioTo ?? null,
      hardwareAcceleration: options?.hardwareAcceleration ?? "disable",
      chromeMode: options?.chromeMode ?? "headless-shell"
    };
    console.log('Invoking studio with', renderOptions)


    const proc = startChildProcess();
    // sleep fo4 5 sec
    await new Promise((resolve) => setTimeout(resolve, 5000));
    axios.post('http://localhost:3000/api/render', renderOptions).then(resp => {
      console.log('Studio started render', resp.data)


      if (controller) {
        controller.stop = () => {
          controller._proxy_stopped = true
          console.log('Stopping proxy render studio process');
          try {
            process.kill(-proc.pid, 'SIGKILL');
          } catch (e) {
            console.error(`Failed to kill process group ${-proc.pid}`, e);
            proc.kill('SIGKILL');
          }
        }
      }

      let settled = false;
      const cleanupHandlers = (() => {
        try {
          if (proc && proc.stdout && stdoutHandler) proc.stdout.removeListener('data', stdoutHandler);
        } catch (e) { }
        try {
          if (proc && proc.stderr && stderrHandler) proc.stderr.removeListener('data', stderrHandler);
        } catch (e) { }
      }).bind(this);

      const stdoutHandler = ((chunk) => {
        const data = String(chunk);
        console.log(data);
        if (data.includes('Cleanup: Closing browser instance')) {
          if (settled) return;
          settled = true;
          cleanupHandlers();
          resolve(finalOutFile);
        }
      }).bind(this);

      const stderrHandler = ((chunk) => {
        const data = String(chunk);
        console.error(data);
        if (data.includes('Failed to render')) {
          if (settled) return;
          settled = true;
          cleanupHandlers();
          reject(new Error(data));
        }
      }).bind(this);

      if (proc && proc.stdout) proc.stdout.on('data', stdoutHandler);
      if (proc && proc.stderr) proc.stderr.on('data', stderrHandler);

      // safety: if proc exits before we resolve, clean up and let the caller retry
      const onProcClose = (() => {
        if (settled) return;
        settled = true;
        cleanupHandlers();
        reject(new Error('Preview process exited before render finished'));
      }).bind(this);

      proc.once('close', onProcClose);

    }).catch(reject)
  }).bind(this))

}