This repository has no description
1// Frame extraction via ffmpeg. Operates on a local video FILE — it decodes frames,
2// it does not "play" anything. (mp4/mov in → png out.)
3
4import { execFile } from "node:child_process";
5import { mkdir } from "node:fs/promises";
6import { resolve } from "node:path";
7import { promisify } from "node:util";
8
9const exec = promisify(execFile);
10
11/** "1:23" | "0:09" | "83" -> seconds. */
12export function toSeconds(at: string): number {
13 const parts = at.split(":").map(Number);
14 if (parts.some((n) => Number.isNaN(n))) return 0;
15 return parts.reduce((acc, n) => acc * 60 + n, 0);
16}
17
18function hhmmss(sec: number): string {
19 const h = Math.floor(sec / 3600);
20 const m = Math.floor((sec % 3600) / 60);
21 const s = Math.floor(sec % 60);
22 return [h, m, s].map((n) => String(n).padStart(2, "0")).join(":");
23}
24
25/** Cut a single frame at `at` from `videoPath` into outDir; returns the png path. */
26export async function extractFrameAt(videoPath: string, at: string, outDir: string): Promise<string> {
27 await mkdir(outDir, { recursive: true });
28 const sec = toSeconds(at);
29 const out = resolve(outDir, `frame_${String(sec).padStart(4, "0")}s.png`);
30 // -ss before -i = fast, frame-accurate seek; one frame; scale down for a ≤1MB blob.
31 await exec("ffmpeg", [
32 "-y",
33 "-ss",
34 hhmmss(sec),
35 "-i",
36 videoPath,
37 "-frames:v",
38 "1",
39 "-vf",
40 "scale='min(1600,iw)':-2",
41 out,
42 "-loglevel",
43 "error",
44 ]);
45 return out;
46}