Ffmpeg detect
Detect-and-act workflows with ffmpeg analysis filters: cropdetect (autocrop black bars), silencedetect, blackdetect, freezedetect, blurdetect, blockdetect, scdet (scene-change detection), idet (interlace detection), signalstats broadcast QC, readeia608 (CEA-608 caption read), readvitc (VITC timecode read). Use when the user asks to autocrop black bars, detect silences, find black frames, detect scene cuts, check if video is interlaced, find freeze frames, auto-split on scene changes, read embedded timecode, or parse detection output into cut points.From its SKILL.md
npx -y skills add damionrashford/media-os --skill ffmpeg-detectAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 17 stars17 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.1 KB, as published. Nobody here has run it
Ffmpeg Detect
Context: $ARGUMENTS
Detect-and-act filters in ffmpeg are read-only analysis passes. They write findings to stderr (and/or frame metadata under lavfi.*), never modify the file, and are designed to be parsed by a script that then issues a second ffmpeg call with real parameters.
Quick start
- Autocrop black bars: → Step 1 (
cropdetect) → Step 3 (applycrop=W:H:X:Y) - Trim silences: → Step 1 (
silencedetect) → Step 3 (cut with-ss/-topairs) - Find black frames / scene cuts: → Step 1 (
blackdetect/scdet) → Step 3 (split with concat demuxer) - Check if interlaced: → Step 1 (
idet) → Step 3 (decide onyadif/bwdif) - Broadcast QC: → Step 1 (
signalstats) → Step 3 (flag out-of-range YMIN/YMAX/SATMAX) - Read embedded captions / timecode: →
readeia608,readvitc - Pre-loudnorm audio stats: →
volumedetect,astats(useloudnormfrom ffmpeg-audio-filter for true LUFS)
When to use
- You need to compute parameters (crop rect, cut list, LUT threshold) from a media file before doing a real encode.
- You're doing QC on broadcast or archival content and need per-frame statistics.
- You're auto-splitting a long capture on scene changes or silences.
- You're deciding whether to deinterlace before a delivery encode.
Use ffmpeg-quality for reference-vs-distorted measurement (VMAF/PSNR/SSIM). Use ffmpeg-probe for container/codec structure — detect filters are for content analysis.
Step 1 — Pick the detector
| Goal | Filter | Typical command tail |
|---|---|---|
| Autocrop | cropdetect | -vf cropdetect=limit=24:round=2 -f null - |
| Silences in audio | silencedetect | -af silencedetect=noise=-30dB:d=0.5 -f null - |
| Black segments | blackdetect | -vf blackdetect=d=1:pix_th=0.1 -f null - |
| Freeze frames | freezedetect | -vf freezedetect=n=0.001:d=0.5 -f null - |
| Blur QC | blurdetect | -vf blurdetect -f null - |
| Blocking QC | blockdetect | -vf blockdetect -f null - |
| Scene cuts | scdet | -vf "scdet=s=1:t=10" -f null - |
| Interlace | idet | -vf idet -f null - |
| Broadcast QC | signalstats | -vf signalstats -f null - |
| CEA-608 captions | readeia608 | -vf readeia608 -f null - |
| VITC timecode | readvitc | -vf readvitc -f null - |
| Volume (peak/mean dB) | volumedetect | -af volumedetect -f null - |
All of these pair with -f null - (video) or route to -f null - (audio) so no file is written. The filter's output lives in stderr. Add -hide_banner to reduce noise. Seek first (-ss) and limit (-t) for fast sampling on long files.
Step 2 — Run and parse stderr / metadata
ffmpeg writes detector lines to stderr. Standard pattern:
ffmpeg -hide_banner -i IN -vf FILTER -f null - 2>&1 | grep PATTERN
Per-detector regexes (see references/detectors.md for full catalog):
cropdetect→crop=\d+:\d+:\d+:\d+(take last match — stabilizes over time).silencedetect→silence_start: ([\d.]+)andsilence_end: ([\d.]+) \| silence_duration: ([\d.]+)— pair consecutive start/end lines.blackdetect→black_start:([\d.]+) black_end:([\d.]+) black_duration:([\d.]+)(all on one line).freezedetect→freeze_start: ([\d.]+),freeze_duration: ([\d.]+),freeze_end: ([\d.]+).scdet→lavfi.scd.score/lavfi.scd.timein metadata (with-f null -andscdet=s=1prints to stderr).idet→ three summary lines at end:Single frame detection,Multi frame detection,Repeated Fields. Use Multi frame for decision.signalstats→ per-frame metadata keyslavfi.signalstats.{YAVG,YMIN,YMAX,UMIN,UMAX,VMIN,VMAX,SATMIN,SATMAX,HUEMED,HUEAVG,YDIF,UDIF,VDIF,SATDIF,TOUT,VREP}. Extract with-show_frameson a second ffprobe pass, or withmetadata=mode=print.readeia608→lavfi.readeia608.X.ccbytes — pipe to-f srtor parse manually for proper caption extraction.readvitc→lavfi.readvitc.tc_str=HH:MM:SS:FFper frame.volumedetect→mean_volume: X dB,max_volume: Y dBat end of run.
For metadata keys, the idiomatic pattern is:
ffmpeg -i IN -vf "signalstats,metadata=mode=print:file=-" -f null - 2>/dev/null
Step 3 — Turn parsed output into actionable parameters
Autocrop (two commands):
# Sample from 10s in, 120s long — skip opening credits / fades
ffmpeg -hide_banner -ss 10 -i in.mp4 -t 120 -vf cropdetect=limit=24:round=2 \
-f null - 2>&1 | grep -o 'crop=[0-9:]*' | tail -1
# → crop=1920:816:0:132
ffmpeg -i in.mp4 -vf crop=1920:816:0:132 -c:a copy out.mp4
Silence trim: parse silencedetect lines, invert to "keep" segments, pass to concat demuxer or -ss/-to pairs. See scripts/detect.py silences for JSON output.
Scene-split chapters: feed scdet timestamps to ffmpeg-cut-concat skill to segment on cuts.
Interlace decision: if Multi frame detection shows TFF > 100 && progressive < 10 → -vf yadif=1:0 (TFF). If BFF > progressive → -vf yadif=1:1 (BFF). If progressive dominates → skip deinterlace.
Broadcast QC: flag any frame where YMIN < 16 or YMAX > 235 (limited-range violation), SATMAX > 118.2 (NTSC 75% bars), or TOUT > 0.005 (temporal outlier / dropout).
Available scripts
scripts/detect.py— argparse dispatcher with subcommandscrop,silences,blacks,freezes,scenes,interlace,stats,volume. All return JSON (or a ready-to-use crop string forcrop). Stdlib only, non-interactive.
Workflow
# Autocrop
uv run ${CLAUDE_SKILL_DIR}/scripts/detect.py crop --input in.mp4
# Silences → JSON
uv run ${CLAUDE_SKILL_DIR}/scripts/detect.py silences --input in.mp3 --noise-db -30 --min-duration 0.5
# Scene cuts
uv run ${CLAUDE_SKILL_DIR}/scripts/detect.py scenes --input in.mp4 --threshold 10
# Interlace decision
uv run ${CLAUDE_SKILL_DIR}/scripts/detect.py interlace --input broadcast.ts
Reference docs
- Read
references/detectors.mdfor per-filter option tables, example stderr, regex patterns, signalstats metadata keys, idet interpretation, and recipes (auto-chapter, silence-trim, broadcast QC pass).
Gotchas
- Detector output is stderr, not stdout. Pipe with
2>&1beforegrep/awk.-progressgives machine-readable progress but NOT filter output. cropdetectneeds 100–200 frames of content. Run with-ss 10past fades/credits and-t 60–120to keep it fast. It updates over time; take the lastcrop=line.cropdetect limit=24= black threshold on 0–255 scale (8-bit). Raise to 32–40 for lighter grey letterbox bars.round=2keeps even dims (required by most codecs);round=16for strict macroblock alignment.silencedetect noise=-30dB:d=0.5— duration in seconds. Below-60dBis near-digital silence;-30dBis room tone. Each silence emits TWO lines (silence_start, thensilence_endwithsilence_duration) — pair them.blackdetectoutputsblack_start,black_end,black_durationall on one line.pix_th=0.1= pixel blackness threshold (normalized 0–1).freezedetect n=0.001:d=0.5—nis noise tolerance (normalized 0–1),dis minimum freeze duration in seconds.scdet t=THRESH— threshold on 0–100 score scale. Typical 10 (lots of cuts) to 30 (only hard cuts). Uses=1to emit one line per detected cut with score + time.idetprints THREE summary blocks at end: single-frame detection, multi-frame detection, and repeated fields. Use multi-frame for the decision (it uses more temporal context). Run on at least ~1000 frames (~40s at 24fps) for confidence. One run is often noisy — re-run with a different segment if counts are ambiguous.signalstatsper-frame metadata lives inlavfi.signalstats.*. Extract viaffprobe -show_framesOR themetadata=mode=printfilter. Filter alone writes nothing to stderr by default.readeia608only works on SD broadcast content with CEA-608 bytes on VBI lines 0–1 (or via captions sidecar). It does NOT read CEA-708, teletext, or burned-in subs. See the ffmpeg-captions skill for a full pipeline.- All detect filters are READ-ONLY.
-f null -sinks output. Nothing is written to disk unless you explicitly add-y output.extwithout-f null. volumedetectgives peak + mean dBFS, NOT LUFS. For loudness compliance (EBU R128 / ITU-R BS.1770) useloudnorm=print_format=summaryfrom the ffmpeg-audio-filter skill.- Scene-cut list → split: feed scdet timestamps to the ffmpeg-cut-concat skill (concat demuxer or segment muxer). Do not try to encode concatenated output in a single ffmpeg call — use stream copy on pre-split clips.
Examples
Example 1: Autocrop letterboxed film
ffmpeg -hide_banner -ss 30 -i movie.mkv -t 180 \
-vf cropdetect=limit=24:round=2 -f null - 2>&1 \
| grep -o 'crop=[0-9:]*' | tail -1
# → crop=1920:800:0:140
ffmpeg -i movie.mkv -vf crop=1920:800:0:140 -c:v libx264 -crf 18 -c:a copy cropped.mkv
Example 2: Generate a silence-trim cut list
ffmpeg -hide_banner -i podcast.wav -af silencedetect=noise=-35dB:d=0.8 \
-f null - 2>&1 | grep silence_
# silence_start: 12.3
# silence_end: 14.8 | silence_duration: 2.5
# silence_start: 120.4
# silence_end: 121.2 | silence_duration: 0.8
Invert to "keep" ranges 0–12.3, 14.8–120.4, 121.2–END, then use ffmpeg-cut-concat.
Example 3: Interlace detection on broadcast capture
ffmpeg -hide_banner -i capture.ts -vf idet -f null - 2>&1 | grep "frame detection"
# [Parsed_idet_0 @ ...] Multi frame detection: TFF: 982 BFF: 3 Progressive: 15 Undetermined: 0
Decision: TFF dominates → -vf yadif=1:0 in the delivery encode.
Example 4: Broadcast QC gate
ffmpeg -hide_banner -i master.mov \
-vf "signalstats,metadata=mode=print:key=lavfi.signalstats.YMAX:file=ymax.log" \
-f null -
awk -F= '$2+0 > 235 {print}' ymax.log # any frame over legal range
Troubleshooting
Error: cropdetect returns no crop= line
Cause: input segment is all-black or too short (<100 frames).
Solution: seek further in with -ss 30, extend duration with -t 120, lower limit (e.g. limit=16) for very dark content.
Error: silencedetect emits silence_start but no matching silence_end
Cause: file ends during a silence segment.
Solution: treat the final silence_start as silence until duration end; ffprobe the duration and synthesize the end timestamp.
Error: idet counts look random / change between runs
Cause: too few frames sampled, or the clip has mixed progressive + interlaced sections.
Solution: sample at least 1000 frames, run on multiple segments (-ss 0, -ss 60, -ss 300) and compare. Mixed content needs frame-by-frame idet metadata rather than summary.
Error: readeia608 returns empty / no metadata
Cause: content is HD (CEA-708, not 608), or captions are in a sidecar/subtitle track rather than in VBI.
Solution: use the ffmpeg-captions skill for CEA-708 / SCC / MCC extraction. readeia608 only sees SD VBI line bytes.
Error: signalstats gives no output
Cause: the filter populates frame metadata, not stderr.
Solution: add ,metadata=mode=print:file=- to the filter chain, or use ffprobe -show_frames -of json on a re-encoded null output.
Error: volumedetect output shows only n_samples, no mean_volume
Cause: ffmpeg didn't finish processing (piped output was broken, or input stream had zero audio frames).
Solution: ensure -f null - is the sink and stderr is captured; verify input has audio with ffprobe -show_streams -select_streams a.