mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(tools): 2.0 phase 5 wave 3b - audio depth (14 tools) (#222)
This commit is contained in:
@@ -13,12 +13,14 @@ const STDERR_RING_MAX = 16 * 1024;
|
||||
/**
|
||||
* Runs ffmpeg with `-progress pipe:1` appended, parsing progress blocks from
|
||||
* stdout. Rejects with the tail of stderr on non-zero exit, timeout or abort.
|
||||
* Output must go to a FILE path in args (no stdout piping of media data).
|
||||
* Resolves with the captured stderr tail (filters like silencedetect report
|
||||
* there). Output must go to a FILE path in args (no stdout piping of media
|
||||
* data).
|
||||
*/
|
||||
export async function runFfmpeg(args: string[], opts: RunFfmpegOptions = {}): Promise<void> {
|
||||
export async function runFfmpeg(args: string[], opts: RunFfmpegOptions = {}): Promise<string> {
|
||||
const bin = resolveFfmpeg();
|
||||
if (!bin) throw new Error("ffmpeg binary not found (set FFMPEG_PATH or install ffmpeg)");
|
||||
await new Promise<void>((resolvePromise, reject) => {
|
||||
return new Promise<string>((resolvePromise, reject) => {
|
||||
const child = spawn(bin, ["-hide_banner", "-nostdin", "-y", ...args, "-progress", "pipe:1"], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
@@ -73,7 +75,7 @@ export async function runFfmpeg(args: string[], opts: RunFfmpegOptions = {}): Pr
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
if (code === 0) resolvePromise();
|
||||
if (code === 0) resolvePromise(stderrTail);
|
||||
else reject(new Error(`ffmpeg exited ${code ?? signal}: ${stderrTail.slice(-2000)}`));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface MediaStreamInfo {
|
||||
width?: number;
|
||||
height?: number;
|
||||
sampleRate?: number;
|
||||
channels?: number;
|
||||
}
|
||||
|
||||
export interface MediaInfo {
|
||||
@@ -14,6 +15,7 @@ export interface MediaInfo {
|
||||
durationS: number | null;
|
||||
bitrateKbps: number | null;
|
||||
streams: MediaStreamInfo[];
|
||||
tags?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ProbeOptions {
|
||||
@@ -70,17 +72,28 @@ export async function probeMedia(filePath: string, opts: ProbeOptions = {}): Pro
|
||||
});
|
||||
});
|
||||
const parsed = JSON.parse(stdout) as {
|
||||
format?: { format_name?: string; duration?: string; bit_rate?: string };
|
||||
format?: {
|
||||
format_name?: string;
|
||||
duration?: string;
|
||||
bit_rate?: string;
|
||||
tags?: Record<string, unknown>;
|
||||
};
|
||||
streams?: Array<{
|
||||
codec_type?: string;
|
||||
codec_name?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
sample_rate?: string;
|
||||
channels?: number;
|
||||
}>;
|
||||
};
|
||||
const duration = parsed.format?.duration ? Number(parsed.format.duration) : null;
|
||||
const bitRate = parsed.format?.bit_rate ? Number(parsed.format.bit_rate) : null;
|
||||
const rawTags = (parsed.format?.tags ?? {}) as Record<string, unknown>;
|
||||
const tags: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(rawTags)) {
|
||||
if (typeof v === "string" && v.length > 0) tags[k.toLowerCase()] = v;
|
||||
}
|
||||
return {
|
||||
container: parsed.format?.format_name ?? "unknown",
|
||||
durationS: Number.isFinite(duration as number) ? (duration as number) : null,
|
||||
@@ -93,7 +106,9 @@ export async function probeMedia(filePath: string, opts: ProbeOptions = {}): Pro
|
||||
width: s.width,
|
||||
height: s.height,
|
||||
sampleRate: Number.isFinite(sr) && (sr as number) > 0 ? sr : undefined,
|
||||
channels: typeof s.channels === "number" && s.channels > 0 ? s.channels : undefined,
|
||||
} as MediaStreamInfo;
|
||||
}),
|
||||
tags: Object.keys(tags).length > 0 ? tags : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user