Files
STCourses/packages/feature-import/src/media.ts
T

41 lines
1.7 KiB
TypeScript

import { link, mkdir, copyFile } from "node:fs/promises";
import { dirname } from "node:path";
import { run } from "./util/exec";
export interface Tools { ffmpeg: string; ffprobe: string }
export async function probeDuration(t: Tools, file: string): Promise<number | undefined> {
try {
const out = await run([t.ffprobe, "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", file]);
const s = Number(out.trim());
return Number.isFinite(s) && s > 0 ? Math.round(s) : undefined;
} catch {
return undefined;
}
}
/** One JPEG frame at `fraction` of the clip, 640px wide. */
export async function extractFrame(t: Tools, file: string, out: string, fraction = 0.2): Promise<void> {
await mkdir(dirname(out), { recursive: true });
const dur = (await probeDuration(t, file)) ?? 0;
const at = Math.max(0, dur * fraction);
await run([t.ffmpeg, "-v", "error", "-y", "-ss", String(at), "-i", file, "-frames:v", "1", "-vf", "scale=640:-2", "-q:v", "4", out]);
}
/**
* Puts a large file into the course. "copy" uses `cp --reflink=auto` (a CoW clone where the filesystem supports it,
* a plain copy elsewhere); "link" makes a hard link and falls back to copying across filesystems.
* Either way the original is only read, and the course folder stays self-contained (the media route refuses symlinks that leave it).
*/
export async function putFile(src: string, dest: string, mode: "copy" | "link" = "copy"): Promise<void> {
await mkdir(dirname(dest), { recursive: true });
if (mode === "link") {
try { await link(src, dest); return; } catch { /* cross-device or unsupported: copy */ }
}
try {
await run(["cp", "--reflink=auto", "--", src, dest]);
} catch {
await copyFile(src, dest);
}
}