mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(modality)!: SnapOtter 2.0 phase 3 modality framework: media/doc engines, pool routing, display modes (#218)
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { probeMedia } from "@snapotter/media-engine";
|
||||
import { env } from "../config.js";
|
||||
import { type InputHandler, InputValidationError, type PreparedInput } from "./contract.js";
|
||||
|
||||
/**
|
||||
* Video/audio validation via capped ffprobe (spec 4.7). ffprobe needs a real
|
||||
* file (mp4 moov atoms may trail), so the buffer lands in the scratch dir.
|
||||
*/
|
||||
export class MediaInputHandler implements InputHandler {
|
||||
constructor(private kind: "video" | "audio") {}
|
||||
|
||||
async prepare(
|
||||
raw: Buffer,
|
||||
filename: string,
|
||||
opts: { scratchDir: string },
|
||||
): Promise<PreparedInput> {
|
||||
const probeDir = join(opts.scratchDir, `probe-${randomUUID()}`);
|
||||
await mkdir(probeDir, { recursive: true });
|
||||
const probePath = join(probeDir, "input");
|
||||
try {
|
||||
await writeFile(probePath, raw);
|
||||
let info: Awaited<ReturnType<typeof probeMedia>>;
|
||||
try {
|
||||
info = await probeMedia(probePath);
|
||||
} catch (err) {
|
||||
throw new InputValidationError(
|
||||
`Unrecognized ${this.kind} file: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
const hasVideo = info.streams.some((s) => s.type === "video");
|
||||
const hasAudio = info.streams.some((s) => s.type === "audio");
|
||||
if (this.kind === "video" && !hasVideo) {
|
||||
throw new InputValidationError("File contains no video stream");
|
||||
}
|
||||
if (this.kind === "audio" && !hasAudio) {
|
||||
throw new InputValidationError("File contains no audio stream");
|
||||
}
|
||||
const durationCap =
|
||||
this.kind === "video" ? env.MAX_VIDEO_DURATION_S : env.MAX_AUDIO_DURATION_S;
|
||||
if (durationCap > 0 && info.durationS !== null && info.durationS > durationCap) {
|
||||
throw new InputValidationError(
|
||||
`Duration ${Math.round(info.durationS)}s exceeds the maximum of ${durationCap}s`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
this.kind === "video" &&
|
||||
env.MAX_VIDEO_BITRATE_KBPS > 0 &&
|
||||
info.bitrateKbps !== null &&
|
||||
info.bitrateKbps > env.MAX_VIDEO_BITRATE_KBPS
|
||||
) {
|
||||
throw new InputValidationError(
|
||||
`Bitrate ${info.bitrateKbps}kbps exceeds the maximum of ${env.MAX_VIDEO_BITRATE_KBPS}kbps`,
|
||||
);
|
||||
}
|
||||
return { buffer: raw, filename };
|
||||
} finally {
|
||||
await rm(probeDir, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user