export interface ProbeStream { streamIndex: number; type: "Video" | "Audio" | "Subtitle" | "Data" | "EmbeddedImage"; codec: string | null; profile: string | null; language: string | null; title: string | null; isDefault: number; isForced: number; isHearingImpaired: number; channels: number | null; channelLayout: string | null; bitRate: number | null; sampleRate: number | null; bitDepth: number | null; } export interface ProbeResult { fileSize: number | null; durationSeconds: number | null; container: string | null; streams: ProbeStream[]; } function mapCodecType(codecType: string): ProbeStream["type"] { switch (codecType) { case "video": return "Video"; case "audio": return "Audio"; case "subtitle": return "Subtitle"; case "data": return "Data"; case "attachment": return "EmbeddedImage"; default: return "Data"; } } function parseIntOrNull(value: string | number | null | undefined): number | null { if (value === null || value === undefined) return null; const n = typeof value === "number" ? value : parseInt(value, 10); return Number.isFinite(n) ? n : null; } function parseFloatOrNull(value: string | number | null | undefined): number | null { if (value === null || value === undefined) return null; const n = typeof value === "number" ? value : parseFloat(value); return Number.isFinite(n) ? n : null; } /** Parse ffprobe JSON output. Exported for unit testing. */ export function parseProbeOutput(json: string): ProbeResult { const raw = JSON.parse(json); const fmt = raw.format ?? {}; const fileSize = parseIntOrNull(fmt.size ?? null); const durationSeconds = parseFloatOrNull(fmt.duration ?? null); const container = fmt.format_name ? (fmt.format_name as string).split(",")[0] || null : null; const streams: ProbeStream[] = (raw.streams ?? []).map( // biome-ignore lint/suspicious/noExplicitAny: raw ffprobe JSON (s: any): ProbeStream => { const tags = s.tags ?? {}; const disp = s.disposition ?? {}; return { streamIndex: s.index as number, type: mapCodecType(s.codec_type ?? ""), codec: (s.codec_name as string | undefined) ?? null, profile: (s.profile as string | undefined) ?? null, language: (tags.language ?? tags.LANGUAGE ?? null) as string | null, title: (tags.title ?? tags.TITLE ?? null) as string | null, isDefault: (disp.default as number | undefined) ?? 0, isForced: (disp.forced as number | undefined) ?? 0, isHearingImpaired: (disp.hearing_impaired as number | undefined) ?? 0, channels: parseIntOrNull(s.channels ?? null), channelLayout: (s.channel_layout as string | undefined) ?? null, bitRate: parseIntOrNull(s.bit_rate ?? null), sampleRate: parseIntOrNull(s.sample_rate ?? null), bitDepth: parseIntOrNull(s.bits_per_raw_sample ?? null), }; }, ); return { fileSize, durationSeconds, container, streams }; } /** Run ffprobe on a local file. */ export async function probeFile(filePath: string): Promise { const proc = Bun.spawn( ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath], { stdout: "pipe", stderr: "pipe" }, ); const [stdout, exitCode] = await Promise.all([ new Response(proc.stdout).text(), proc.exited, ]); if (exitCode !== 0) { const stderr = await new Response(proc.stderr).text(); throw new Error(`ffprobe exited with code ${exitCode}: ${stderr.trim()}`); } return parseProbeOutput(stdout); }