audio joins the vocabulary, and -vn is a flag rather than an option
FORMATS was mp4/webm/mp3/image2 — a web-delivery list, because that is what av-club was first for. a right-click on a .mkv that offers "mp4 or webm" is not a conversion tool, so: mkv, mov for video; m4a, flac, wav, ogg, opus beside mp3 for audio. the KEY is what a caller asks for; `container` is what ffmpeg is told, because ffmpeg's names for two of them are not what anybody calls them — `matroska` for .mkv, `ipod` for .m4a. making a caller know that is making them learn ffmpeg trivia to convert a file. audio encoders too: libmp3lame, libvorbis, flac, alac, pcm_s16le beside aac and libopus, plus q:a for the encoders that want a quality rather than a bitrate, and map_metadata/id3v2_version so tags survive a conversion. A REAL BUG FOUND ON THE WAY, and it had shipped. `an` was declared as an enum whose only value was "1", so it emitted `-an 1` — and `-an` takes no argument, so ffmpeg read the `1` as an output filename and died with "Error opening output files: Invalid argument", naming neither the option nor the problem. `an` had never worked. I reproduced it exactly by adding `vn` the same way. so there is a `flag` kind now: given `true` it emits the option alone, and anything else is refused with a message that says so. `vn: "1"` is a 400 rather than a mystery. verified through the running service: mkv remux, mov, and extraction to all six audio formats — mp3, flac, wav, ogg, opus, m4a — all 200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+13
-3
@@ -38,8 +38,12 @@ export type FfmpegRequest = {
|
||||
|
||||
const BITRATE = /^[0-9]+(\.[0-9]+)?[kKmM]?$/;
|
||||
|
||||
function value(spec: FfmpegParam, raw: unknown, path: string): string {
|
||||
/** `null` means "emit the flag with no value" — see the `flag` kind in vocabulary.ts. */
|
||||
function value(spec: FfmpegParam, raw: unknown, path: string): string | null {
|
||||
switch (spec.kind) {
|
||||
case "flag":
|
||||
if (raw !== true) throw invalid(`${path} must be true, or left out`, path);
|
||||
return null;
|
||||
case "int":
|
||||
if (typeof raw !== "number" || !Number.isInteger(raw)) throw invalid(`${path} must be an integer`, path);
|
||||
if (raw < spec.min || raw > spec.max) throw invalid(`${path} must be between ${spec.min} and ${spec.max}`, path);
|
||||
@@ -71,7 +75,11 @@ function options(table: Record<string, FfmpegParam>, raw: unknown, path: string)
|
||||
if (!spec) {
|
||||
throw invalid(`Unknown option ${JSON.stringify(key)}. Accepted here: ${Object.keys(table).join(", ")}`, `${path}.${key}`);
|
||||
}
|
||||
args.push(`-${key}`, value(spec, entry, `${path}.${key}`));
|
||||
const resolved = value(spec, entry, `${path}.${key}`);
|
||||
// A flag is the option and nothing else. Pushing an empty string here would hand ffmpeg an
|
||||
// argument it reads as a filename, which is the bug this replaced.
|
||||
if (resolved === null) args.push(`-${key}`);
|
||||
else args.push(`-${key}`, resolved);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
@@ -199,7 +207,9 @@ export async function runFfmpeg(body: FfmpegRequest): Promise<FfmpegResult> {
|
||||
...presetOptions,
|
||||
// After the preset, so an explicit option wins.
|
||||
...outputArgs,
|
||||
"-f", format,
|
||||
// ffmpeg's own container name, which is not always what the format is called — `matroska` for
|
||||
// mkv, `ipod` for m4a. The caller says the familiar one.
|
||||
"-f", FORMATS[format]!.container ?? format,
|
||||
output,
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user