import { execSync } from 'node:child_process'; import { renameSync, statSync } from 'node:fs'; import { LEAD_IN_S } from './config.js'; import { viewportFor, type Storyboard } from './script.js'; export function trimRecording( rawPath: string, trimmedPath: string, storyboard: Storyboard, times: { recordStartMs: number; sceneStartMs: number; sceneEndMs: number } ) { const sceneSpan = (times.sceneEndMs - times.sceneStartMs) / 1000; const trimStart = Math.max( 0, (times.sceneStartMs - times.recordStartMs) / 1000 - LEAD_IN_S ); const trimEnd = (times.sceneEndMs - times.recordStartMs) / 1000; const wallDuration = trimEnd - trimStart; const finalDuration = wallDuration; const { outputFps, webmBitrate, maxDurationS } = storyboard.video; const viewport = viewportFor(storyboard.video); if (finalDuration > maxDurationS) { console.log( `[${storyboard.name}] Scene output duration is ${finalDuration.toFixed(2)}s ` + `(guard ${maxDurationS.toFixed(2)}s); keeping the full take.` ); } const filter = `trim=start=${trimStart.toFixed(3)}:duration=${wallDuration.toFixed(3)},` + `setpts=PTS-STARTPTS,fps=${outputFps},` + `trim=duration=${finalDuration.toFixed(3)},setpts=PTS-STARTPTS`; // Keep trimming inside the filter graph: it is frame-accurate for WebM // without the keyframe leakage of input seeking. execSync( `ffmpeg -y -i "${rawPath}" -vf "${filter}" ` + `-fps_mode cfr -r ${outputFps} -c:v libvpx -b:v ${webmBitrate} -deadline good -cpu-used 5 ` + `"${trimmedPath}"`, { stdio: 'inherit' } ); try { statSync(rawPath); renameSync(rawPath, rawPath + '.untrimmed'); } catch { /* ignore */ } console.log( `[${storyboard.name}] Wrote ${trimmedPath} (${finalDuration.toFixed(2)}s, scene=${sceneSpan.toFixed(2)}s, capture=${viewport.width}x${viewport.height})` ); }