ffmpeg-skill 1.11.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -157,9 +157,11 @@ TOOL_META: Dict[str, Dict[str, Any]] = {
157
157
  "multicam": dict(role="execution", inputs=["reference camera", "other cameras / recorders"], outputs=["switched multicam video artifact"],
158
158
  required=FF + [X264, AAC], optional=[HDR_X265],
159
159
  video_required=True, audio_only=False, visual=True, verify=["probe", "look"], produces_artifact=True, idempotency="content_equivalent", deterministic=True),
160
- "audio": dict(role="execution", inputs=["video or audio asset", "music bed (--music) or replacement track (--replace)"], outputs=["artifact with the processed audio (video stream-copied, or dropped when -o has an audio extension)"],
160
+ "audio": dict(role="execution", inputs=["video or audio asset", "music bed (--music) or replacement track (--replace)", "effects/atmos track (--effects)"], outputs=["artifact with the processed audio (video stream-copied, or dropped when -o has an audio extension)"],
161
161
  required=FF, optional=[{"capability": "filter:afftdn", "when": "--denoise / --voice"}, {"capability": "filter:sidechaincompress", "when": "--duck"},
162
- {"capability": "filter:acompressor", "when": "--compress / --voice"}, {"capability": "filter:alimiter", "when": "--limit"}, {"capability": "filter:agate", "when": "--gate"},
162
+ {"capability": "filter:acompressor", "when": "--compress / --voice"}, {"capability": "filter:alimiter", "when": "--limit / --voice strong"}, {"capability": "filter:agate", "when": "--gate"},
163
+ {"capability": "filter:deesser", "when": "--voice medium (the default) / --voice strong"},
164
+ {"capability": "filter:extrastereo", "when": "--stereo-widen"},
163
165
  {"capability": AAC, "when": "output extension isn't .mp3/.opus/.ogg/.flac (audio_codec_for()'s default)"}] + AUDIO_OUT,
164
166
  video_required=False, audio_only=True, visual=False, verify=["probe"], produces_artifact=True, idempotency="content_equivalent", deterministic=True),
165
167
  "loudness": dict(role="analysis_and_execution", inputs=["video or audio asset"], outputs=["loudness measurement JSON (--measure-only)", "normalised artifact (video stream-copied)"],
@@ -411,7 +413,9 @@ def output_schema(name: str, meta: Dict[str, Any]) -> Dict[str, Any]:
411
413
  extra = {"loudness": {"type": "object", "description": "platform presets with audio: the written file's lufs/tp against the platform's target_lufs/target_tp, ok true when inside the spec; normalized true when --normalize ran loudness.py on the file"},
412
414
  "notes": {"type": "array", "items": {"type": "string"}}}
413
415
  elif name == "loudness":
414
- extra = {"measured": {"type": "object", "description": "--measure-only prints the loudnorm measurement instead (input_i, input_tp, input_lra, input_thresh, target_offset)"}}
416
+ extra = {"measured": {"type": "object", "description": "the loudnorm measurement of the input (input_i, input_tp, input_lra, input_thresh, target_offset); with --measure-only it is the whole result"},
417
+ "targets": {"type": "object", "description": "the requested lufs / tp / lra"},
418
+ "result": {"type": "object", "description": "the written file measured again (input_i, input_tp, input_lra, ...), plus tp_ceiling_used, audio_bitrate_used and encodes"}}
415
419
  elif name == "cut":
416
420
  extra = {"expected_duration": {"type": "number", "description": "seconds requested"},
417
421
  "duration_error_ms": {"type": ["number", "null"], "description": "written minus requested, measured by ffprobe (null under --dry-run)"},
@@ -426,7 +430,8 @@ def output_schema(name: str, meta: Dict[str, Any]) -> Dict[str, Any]:
426
430
  elif name == "audio":
427
431
  extra = {"video": {"type": "boolean", "description": "true when the input's video stream was copied; false for an audio output extension (extraction)"},
428
432
  "audio_stream": {"type": "integer", "description": "which input audio stream was processed (--audio-stream)"},
429
- "dynamics": {"type": "array", "items": {"enum": ["agate", "acompressor", "alimiter"]}, "description": "typed dynamics filters applied, in graph order"}}
433
+ "dynamics": {"type": "array", "items": {"enum": ["agate", "acompressor", "alimiter"]}, "description": "typed dynamics filters applied, in graph order"},
434
+ "audio": {"type": "object", "description": "what the mix was built from: voice (null | light | medium | strong), stereo_widen, effects/effects_volume, and with --music the music_volume plus duck (null when --duck was not given, else the threshold in dB and linear, ratio, attack_ms, release_ms, amount_db actually used)"}}
430
435
  props = dict(base)
431
436
  props.update(extra)
432
437
  required = ["status", "output", "dry_run", "commands"]
@@ -765,9 +770,53 @@ def doctor() -> Dict[str, Any]:
765
770
 
766
771
 
767
772
  def _fonts_capability() -> Dict[str, Any]:
773
+ """The default drawtext family (issue #66) plus, since 1.12, one entry per script the tools
774
+ can detect: which languages this machine can actually RENDER, not just which filters exist.
775
+
776
+ Per script: available (a font file covers it, path in `file`), missing (fontconfig knows none),
777
+ unknown (no fontconfig to ask -- the same "unknown is not missing" rule every other capability
778
+ here follows). Informational like the default font and gpu_encoders: a machine with no Thai
779
+ font is not a broken install, it is a machine that must not be asked to burn Thai captions.
780
+ """
781
+ from _common import SCRIPTS, font_for_script, script_font_status
782
+
768
783
  font = _default_font()
769
784
  result = _font_available(font)
770
- return {"default_font": font, "status": result["status"], "detail": result["detail"]}
785
+ scripts: Dict[str, Any] = {}
786
+ for script in SCRIPTS:
787
+ if script == "latin":
788
+ continue
789
+ # script_font_status() is the one place that tells "fontconfig answered, nothing covers
790
+ # this" (missing) apart from "there is no working fontconfig to ask" (unknown) -- the same
791
+ # distinction the tools refuse or continue on.
792
+ status = script_font_status(script)
793
+ scripts[script] = {"status": status, "file": font_for_script(script) if status == "available" else None}
794
+ return {"default_font": font, "status": result["status"], "detail": result["detail"], "scripts": scripts}
795
+
796
+
797
+ def _fonts_summary_line(fonts: Dict[str, Any]) -> str:
798
+ """One line for the plain-text doctor: which scripts render here, which do not."""
799
+ scripts = fonts.get("scripts") or {}
800
+ by_state: Dict[str, List[str]] = {"available": [], "missing": [], "unknown": []}
801
+ for name, entry in scripts.items():
802
+ by_state.setdefault(entry["status"], []).append(name)
803
+ # the default font's `detail` (which family fontconfig substituted, or why it is unknown) is
804
+ # the actionable half of a non-available status, and the line has room for it
805
+ head = f"fonts: '{fonts['default_font']}' {fonts['status']}"
806
+ detail = fonts.get("detail") or ""
807
+ # The substituted family is the actionable half of a non-available status, so it goes on the
808
+ # plain line -- but only while it stays short enough to keep doctor's one-line-per-capability
809
+ # shape (the longest other line is ~85 chars). A long explanation is --json only.
810
+ if detail and fonts["status"] != "available" and len(detail) <= 60:
811
+ head += f" ({detail})"
812
+ parts = [head]
813
+ if by_state["available"]:
814
+ parts.append("renders " + " ".join(by_state["available"]))
815
+ if by_state["missing"]:
816
+ parts.append("no font for " + " ".join(by_state["missing"]))
817
+ if by_state["unknown"]:
818
+ parts.append("unknown (no fontconfig) " + " ".join(by_state["unknown"]))
819
+ return "; ".join(parts)
771
820
 
772
821
 
773
822
  def _capability_fix_hint(cap: str) -> str:
@@ -795,6 +844,11 @@ def _capability_fix_hint(cap: str) -> str:
795
844
  return f"this ffmpeg build has no {cap[7:]} filter; {full_hint}"
796
845
  if cap.startswith("bsf:"):
797
846
  return f"this ffmpeg build has no {cap[4:]} bitstream filter; {full_hint}"
847
+ if cap.startswith("font:"):
848
+ from _common import LANGUAGE_NAMES, FONT_INSTALL_HINT
849
+ script = cap[5:]
850
+ return (f"no installed font covers {LANGUAGE_NAMES.get(script, script)} text on this machine; "
851
+ f"{FONT_INSTALL_HINT} (doctor --json .fonts.scripts lists every script)")
798
852
  if cap == "external:whisper":
799
853
  return "install a local whisper (whisper-cli, whisper-cpp, faster-whisper or openai-whisper) for --transcribe"
800
854
  return f"'{cap}' is not available; see docs/contract.md"
@@ -1152,9 +1206,7 @@ def main() -> int:
1152
1206
  gpu = d["gpu_encoders"]
1153
1207
  if gpu["status"] == "parsed":
1154
1208
  print(f"GPU-backed encoders in this build: {len(gpu['present'])} (no tool here uses one; names in doctor --json)")
1155
- fonts = d["fonts"]
1156
- print(f"default drawtext font '{fonts['default_font']}': {fonts['status']}"
1157
- + (f" ({fonts['detail']})" if fonts["status"] != "available" else ""))
1209
+ print(_fonts_summary_line(d["fonts"]))
1158
1210
  print("full detail: doctor --json (capability lists, per-tool `usable`, fix hints)")
1159
1211
  for err in d["errors"]:
1160
1212
  print(f"detection error: {err}", file=sys.stderr)
package/scripts/audio.py CHANGED
@@ -7,9 +7,12 @@ drops the picture, so `audio.py talk.mp4 -o talk.wav` is an extraction.
7
7
  Examples:
8
8
  python3 audio.py interview.mp4 --denoise # FFT noise reduction
9
9
  python3 audio.py interview.mp4 --voice # highpass + de-esser + compressor + denoise
10
+ python3 audio.py interview.mp4 --voice light # highpass + gentle compression only (light|medium|strong)
10
11
  python3 audio.py talk.mp4 --music bed.mp3 --duck # music under speech, auto-ducked
11
12
  python3 audio.py talk.mp4 --music bed.mp3 --music-volume -18 --music-fade-out 3 # bed fades, voice does not
12
13
  python3 audio.py clip.mp4 --fade-in 0.5 --fade-out 1 --stereo
14
+ python3 audio.py talk.mp4 --music bed.mp3 --duck --duck-threshold -30 --duck-release 250 # ducks earlier and recovers faster
15
+ python3 audio.py band.wav --stereo-widen 0.5 -o wide.wav # wider stereo image (a real stereo source; mono is refused)
13
16
  python3 audio.py surround.mov --downmix # 5.1 -> stereo with proper centre/LFE weights
14
17
  python3 audio.py clip.mp4 --replace narration.wav # swap the audio track entirely
15
18
  python3 audio.py interview.mp4 -o interview.wav # extract the audio (no video in the output)
@@ -18,12 +21,29 @@ Examples:
18
21
  """
19
22
  import argparse
20
23
  import sys
21
- from typing import List
24
+ from typing import Any, Dict, List
22
25
 
23
26
  from _common import STATE, add_common, apply_common, audio_codec_for, db_to_linear, default_output, die, emit, ffmpeg_base, info, is_audio_output, probe, run, run_keeping_subtitles, fmt_secs
24
27
 
25
28
  VOICE_CHAIN = "highpass=f=80,deesser=i=0.4,afftdn=nf=-25:tn=1,acompressor=threshold=-18dB:ratio=3:attack=5:release=80:makeup=2"
26
29
 
30
+ # --voice [light|medium|strong] (1.13). "medium" is the chain --voice has always produced, so a
31
+ # bare --voice (and every existing call and MCP request) is byte-identical to before. "light"
32
+ # leaves the noise floor and the sibilance alone -- it only removes rumble and evens the level,
33
+ # which is what a good room recording needs; "strong" is for phone/laptop audio: a harder
34
+ # de-esser, a second compression stage and a soft limiter at -1 dBFS so the peaks stop there
35
+ # instead of at whatever the make-up gain produced.
36
+ VOICE_LEVELS = {
37
+ "light": "highpass=f=80,acompressor=threshold=-18dB:ratio=2:attack=5:release=80:makeup=1",
38
+ "medium": VOICE_CHAIN,
39
+ "strong": VOICE_CHAIN + ",deesser=i=0.6,acompressor=threshold=-24dB:ratio=4:attack=5:release=120:makeup=3,alimiter=limit=0.891251:level=disabled",
40
+ }
41
+
42
+ # The sidechain threshold the music bed has used since 1.4 is the linear 0.05 that ffmpeg's
43
+ # sidechaincompress takes; -26.0206 dBFS is that same number in the unit the flag speaks, so the
44
+ # default command line is unchanged to the byte while the value is now sayable.
45
+ DUCK_THRESHOLD_DB = -26.0206
46
+
27
47
  # Typed dynamics: every flag maps to one real option of one ffmpeg filter, validated against the
28
48
  # range that filter documents (ffmpeg -h filter=acompressor / alimiter / agate). dB flags are
29
49
  # converted to the linear value the filter takes, so no string reaches the graph unchecked.
@@ -79,13 +99,23 @@ def main() -> int:
79
99
  clean = ap.add_argument_group("clean-up")
80
100
  clean.add_argument("--denoise", action="store_true", help="FFT noise reduction (afftdn, adaptive)")
81
101
  clean.add_argument("--denoise-strength", type=float, default=25.0, help="noise floor in dB to remove, 10..60 (default 25)")
82
- clean.add_argument("--voice", action="store_true", help="speech preset: highpass 80 Hz, de-esser, denoise, gentle compression")
102
+ clean.add_argument("--voice", nargs="?", const="medium", choices=["light", "medium", "strong"], default=None,
103
+ help="speech preset (default medium when the flag is given bare): light = highpass 80 Hz + gentle compression; "
104
+ "medium = highpass, de-esser, denoise, gentle compression; strong = medium plus a harder de-esser, a second "
105
+ "compressor and a soft limiter at -1 dBFS. MCP/JSON callers may still send the 1.12 boolean true, "
106
+ "which is the bare flag and so means medium")
83
107
  clean.add_argument("--gain", type=float, help="gain in dB applied to the main track")
84
108
  music = ap.add_argument_group("music")
85
109
  music.add_argument("--music", help="music file to mix underneath")
86
110
  music.add_argument("--music-volume", type=float, default=-14.0, help="music level in dB relative to full scale (default -14)")
87
111
  music.add_argument("--duck", action="store_true", help="auto-duck the music when the main track has speech (sidechain compressor)")
88
112
  music.add_argument("--duck-amount", type=float, default=12.0, help="how many dB to duck (default 12)")
113
+ music.add_argument("--duck-threshold", type=float, default=DUCK_THRESHOLD_DB,
114
+ help="sidechain threshold in dBFS: the main track is heard as speech above this (default -26.02, the 0.05 linear used since 1.4)")
115
+ music.add_argument("--duck-attack", type=float, default=20.0, help="ms the bed takes to duck once speech starts (default 20)")
116
+ music.add_argument("--duck-release", type=float, default=400.0, help="ms the bed takes to come back up after speech (default 400)")
117
+ music.add_argument("--effects", help="a third track (sound effects/atmos) mixed in at --effects-volume; never ducked")
118
+ music.add_argument("--effects-volume", type=float, default=-14.0, help="effects level in dB relative to full scale (default -14)")
89
119
  music.add_argument("--music-loop", action="store_true", help="loop the music if shorter than the video")
90
120
  fades = ap.add_argument_group("fades / layout")
91
121
  fades.add_argument("--fade-in", type=float, default=0.0, help="seconds")
@@ -94,6 +124,10 @@ def main() -> int:
94
124
  channels = fades.add_mutually_exclusive_group()
95
125
  channels.add_argument("--stereo", action="store_true", help="force 2-channel output (mono is duplicated to both sides)")
96
126
  channels.add_argument("--mono", action="store_true", help="force 1-channel output")
127
+ fades.add_argument("--stereo-widen", type=float, default=None, metavar="AMOUNT",
128
+ help="widen the stereo image, 0..1 (0 = untouched, 1 = maximum); needs a real stereo source: a mono input is "
129
+ "refused (duplicating it leaves both channels identical, so there is nothing to widen) and more than two "
130
+ "channels are refused unless --downmix folds them to stereo first")
97
131
  fades.add_argument("--downmix", action="store_true", help="downmix 5.1/7.1 to stereo using standard weights")
98
132
  fades.add_argument("--replace", help="replace the audio with this file (trimmed/padded to the video)")
99
133
  dyn = ap.add_argument_group("dynamics (typed; each flag is one option of ffmpeg's acompressor / alimiter / agate)")
@@ -124,6 +158,26 @@ def main() -> int:
124
158
  if not getattr(args, switch) and any(getattr(args, f) is not None for f in DYNAMICS[flag_group]):
125
159
  die(f"--{switch} is off but one of its parameters was given; add --{switch}")
126
160
 
161
+ # Same rule as the typed dynamics above, for the ducking knobs: a parameter for a switch that
162
+ # is off does nothing, and a caller who says --duck-release 250 and gets the default 400 ms has
163
+ # no way to notice. --duck itself needs a bed to duck.
164
+ duck_params = [f"--duck-{name}" for name in ("threshold", "attack", "release")
165
+ if getattr(args, f"duck_{name}") != ap.get_default(f"duck_{name}")] + \
166
+ (["--duck-amount"] if args.duck_amount != ap.get_default("duck_amount") else [])
167
+ if not args.duck and duck_params:
168
+ die(f"--duck is off but {duck_params[0]} was given; add --duck")
169
+ if args.duck and not args.music:
170
+ die("--duck ducks the music bed under the main track, but no --music was given; add --music FILE")
171
+
172
+ for flag, value, lo, hi in (("--duck-amount", args.duck_amount, 0.0, 60.0),
173
+ ("--duck-threshold", args.duck_threshold, -60.0, 0.0),
174
+ ("--duck-attack", args.duck_attack, 0.01, 2000.0),
175
+ ("--duck-release", args.duck_release, 0.01, 9000.0)):
176
+ if not (lo <= value <= hi):
177
+ die(f"{flag} {value:g} is outside {lo:g}..{hi:g} (the range ffmpeg's sidechaincompress accepts)")
178
+ if args.stereo_widen is not None and not (0.0 <= args.stereo_widen <= 1.0):
179
+ die(f"--stereo-widen must be 0..1 (0 = untouched, 1 = maximum), got {args.stereo_widen:g}")
180
+
127
181
  meta = probe(args.input)
128
182
  dur = meta.get("duration") or 0.0
129
183
  has_video = bool(meta.get("video"))
@@ -136,6 +190,19 @@ def main() -> int:
136
190
  die(f"--audio-stream {args.audio_stream}: input has {len(streams)} audio stream(s), 0..{len(streams) - 1}")
137
191
  if args.audio_stream and not streams and not STATE.dry_run:
138
192
  die("--audio-stream needs an input with audio streams")
193
+ in_channels = (meta.get("audio") or {}).get("channels") or 0
194
+ if args.stereo_widen is not None:
195
+ if args.mono:
196
+ die("--stereo-widen and --mono contradict each other: there is no stereo image in a 1-channel output")
197
+ # Widening scales the side signal (L-R). Duplicating a mono track to two channels leaves
198
+ # L == R, so the side signal is exactly zero and scaling it changes nothing: --stereo is
199
+ # not a way in, it is a way to a file that measures mono no matter the amount asked for.
200
+ if in_channels == 1:
201
+ die("--stereo-widen needs a real stereo source: mono has no stereo image to widen; keep it mono or "
202
+ "use --stereo to duplicate it, but widening needs a real stereo source")
203
+ if in_channels > 2 and not args.downmix:
204
+ die(f"--stereo-widen needs a stereo track; this input has {in_channels} channels. Add --downmix to fold it "
205
+ "to stereo first (the widening then happens after the downmix), or leave the channels alone.")
139
206
 
140
207
  inputs: List[str] = ["-i", args.input]
141
208
  main_src = f"0:a:{args.audio_stream}"
@@ -150,7 +217,7 @@ def main() -> int:
150
217
  if args.downmix:
151
218
  fx.append("pan=stereo|FL=0.707*FC+FL+0.5*BL+0.5*SL+0.5*LFE|FR=0.707*FC+FR+0.5*BR+0.5*SR+0.5*LFE")
152
219
  if args.voice:
153
- fx.append(VOICE_CHAIN)
220
+ fx.append(VOICE_LEVELS[args.voice])
154
221
  elif args.denoise:
155
222
  if not 10 <= args.denoise_strength <= 60:
156
223
  die(f"--denoise-strength must be 10..60 (dB of noise floor to remove), got {args.denoise_strength:g}")
@@ -172,6 +239,11 @@ def main() -> int:
172
239
  # 1 channel: already mono; the stereo pan used to halve it (-6 dB) because c1 was silence
173
240
  elif args.stereo:
174
241
  fx.append("aformat=channel_layouts=stereo")
242
+ if args.stereo_widen is not None:
243
+ # extrastereo widens by scaling the side (L-R) signal: m=1 is the input, m=3 is as wide
244
+ # as it goes before the centre collapses. It runs after the channel layout is settled, so
245
+ # a --downmix 5.1 source is widened on the stereo fold-down rather than on six channels.
246
+ fx.append(f"extrastereo=m={1 + 2 * args.stereo_widen:g}")
175
247
 
176
248
  graph: List[str] = []
177
249
  graph.append(f"[{main_src}]{','.join(fx) if fx else 'anull'}[main]")
@@ -192,13 +264,26 @@ def main() -> int:
192
264
  if args.duck:
193
265
  graph.append("[main]asplit=2[mainA][sc]")
194
266
  graph.append(
195
- f"[music][sc]sidechaincompress=threshold=0.05:ratio={max(2.0, args.duck_amount / 3):.1f}:attack=20:release=400:makeup=1[ducked]"
267
+ f"[music][sc]sidechaincompress=threshold={db_to_linear(args.duck_threshold):.6g}"
268
+ f":ratio={max(2.0, args.duck_amount / 3):.1f}:attack={args.duck_attack:g}:release={args.duck_release:g}:makeup=1[ducked]"
196
269
  )
197
270
  graph.append("[mainA][ducked]amix=inputs=2:duration=first:dropout_transition=2:normalize=0[mix]")
198
271
  else:
199
272
  graph.append("[main][music]amix=inputs=2:duration=first:dropout_transition=2:normalize=0[mix]")
200
273
  last = "mix"
201
274
 
275
+ if args.effects:
276
+ # A third bed, mixed in at its own level and deliberately never ducked: effects are cut
277
+ # to the picture, so dipping them under speech would move them off their own frames.
278
+ probe(args.effects)
279
+ inputs += ["-i", args.effects]
280
+ e = f"{idx}:a:0"
281
+ idx += 1
282
+ efx = [f"volume={args.effects_volume:g}dB", f"atrim=0:{dur:.3f}" if dur else "anull"]
283
+ graph.append(f"[{e}]{','.join(efx)}[effects]")
284
+ graph.append(f"[{last}][effects]amix=inputs=2:duration=first:dropout_transition=2:normalize=0[mixfx]")
285
+ last = "mixfx"
286
+
202
287
  post: List[str] = []
203
288
  if args.fade_in:
204
289
  post.append(f"afade=t=in:st=0:d={args.fade_in:g}")
@@ -237,7 +322,16 @@ def main() -> int:
237
322
  die(f"{output} unexpectedly contains a video stream")
238
323
  info(f"wrote {output} ({fmt_secs(r['duration'])}, audio {a['codec']} {a['channels']}ch {a['sample_rate']}Hz"
239
324
  + (", video stream-copied" if has_video and not audio_out else ", video dropped" if has_video else "") + ")")
240
- emit(output, video=bool(has_video and not audio_out), audio_stream=args.audio_stream,
325
+ audio_block: Dict[str, Any] = {"voice": args.voice, "stereo_widen": args.stereo_widen,
326
+ "effects": bool(args.effects), "effects_volume": args.effects_volume if args.effects else None}
327
+ if args.music:
328
+ audio_block["music_volume"] = args.music_volume
329
+ audio_block["duck"] = ({"amount_db": args.duck_amount, "threshold_db": round(args.duck_threshold, 4),
330
+ "threshold_linear": float(f"{db_to_linear(args.duck_threshold):.6g}"),
331
+ "ratio": round(max(2.0, args.duck_amount / 3), 1),
332
+ "attack_ms": args.duck_attack, "release_ms": args.duck_release}
333
+ if args.duck else None)
334
+ emit(output, audio=audio_block, video=bool(has_video and not audio_out), audio_stream=args.audio_stream,
241
335
  dynamics=[f for f in (args.gate and "agate", args.compress and "acompressor", args.limit and "alimiter") if f],
242
336
  dropped_non_av_streams=dropped_streams)
243
337
  return 0