perfect-postcode/video/src/video.ts
2026-05-11 21:38:26 +01:00

55 lines
1.9 KiB
TypeScript

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