ffmpegvideo-processingautomation

FFmpeg blackdetect: Find Black Frames and Split Video Automatically

·Javid Jamae·11 min read
FFmpeg blackdetect: Find Black Frames and Split Video Automatically

You've got a 90-minute recording that's really six segments glued together, each one separated by a fade to black. You want six files, cut at the fades, without scrubbing a timeline. FFmpeg can find those fades for you, but the default settings will miss most of them.

Quick answer: FFmpeg blackdetect is a video filter that reports every run of black frames in a file. Run ffmpeg -i input.mp4 -vf blackdetect=d=0.5:pic_th=0.90:pix_th=0.12 -an -f null - and FFmpeg prints black_start, black_end, and black_duration for each detected range to stderr. Feed the midpoint of each range into the segment muxer or a -ss/-to pair to split the video automatically.

Conventional wisdom says black-frame detection is a solved one-liner. It mostly is, and the one-liner still misses fades on real footage. The reason isn't that the filter is weak. It's that blackdetect has three independent thresholds and the defaults are tuned for broadcast-clean black, not for the compression noise, letterbox bars, and half-second dips you get out of OBS, Zoom exports, or a concatenated render.

What blackdetect actually measures

blackdetect decides "is this frame black?" per frame, then groups consecutive black frames into ranges and reports any range longer than a minimum duration. Three parameters control it, and all three matter.

  • pix_th (pixel_black_th, default 0.10) sets how dark a single pixel must be to count as black. The value is scaled to the format's luma range, so on standard limited-range YUV (16 to 235) a pix_th of 0.10 means a pixel counts as black only if its luma is at or below about 38.
  • pic_th (picture_black_ratio_th, default 0.98) sets what fraction of the frame's pixels must be black for the frame to count as black. At the default, a frame with a 3% station bug, a timecode burn-in, or a stray gradient never qualifies.
  • d (black_min_duration, default 2) sets the shortest run of black frames worth reporting, in seconds. A 20-frame fade at 30 fps lasts 0.67 seconds and is silently discarded.

That default d=2 is the single biggest reason people conclude "blackdetect doesn't find my fades." It found them. It threw them away.

Here's a starting point that works on real screen recordings and ad-supported captures:

ffmpeg -hide_banner -i input.mp4 \
  -vf blackdetect=d=0.5:pic_th=0.90:pix_th=0.12 \
  -an -sn -f null - 2>&1 | grep blackdetect

-an -sn skips audio and subtitle decoding, and -f null - throws the decoded video away instead of encoding it. On Windows, use -f null NUL. Detection is still a full video decode, so budget roughly 20x to 40x realtime for 1080p H.264 on one modern CPU core. A 90-minute file lands in the two-to-five-minute range.

Reading the blackdetect log output

blackdetect writes to stderr at the info log level, one line per detected range:

[blackdetect @ 0x7f9a4] black_start:0 black_end:1.5 black_duration:1.5
[blackdetect @ 0x7f9a4] black_start:745.037 black_end:747.587 black_duration:2.55
[blackdetect @ 0x7f9a4] black_start:1502.44 black_end:1504.11 black_duration:1.67

Timestamps are in seconds from the start of the input. If you set -loglevel error or -v quiet, you get nothing back, because you just suppressed the only output the filter produces. That trips up people wiring this into CI scripts.

For anything programmatic, skip log scraping. blackdetect also exports frame metadata as lavfi.black_start and lavfi.black_end, which ffprobe will hand you as JSON:

ffprobe -hide_banner -f lavfi \
  -i "movie=input.mp4,blackdetect=d=0.5:pic_th=0.90:pix_th=0.12" \
  -show_entries frame=pkt_pts_time:frame_tags=lavfi.black_start,lavfi.black_end \
  -of json

That gives you structured output with no regex, which matters when this runs unattended. If you want a maintained wrapper instead, Werner Robitza's ffmpeg-black-split (pip install ffmpeg-black-split, source at slhck/ffmpeg-black-split on GitHub) runs the detection pass, emits the black periods as JSON, and can split the file in one command.

Turning black ranges into cut points

A detected range is an interval, not a cut point. You have to pick a single timestamp inside it.

Cut at the midpoint, not the start

Cutting at black_start leaves the entire fade-out tacked onto the end of the previous segment. Cutting at black_end leaves the fade-in on the front of the next one. The midpoint splits the difference and gives every segment a clean, short fade on each side, which is what you want if the clips get published separately.

This reads the log, computes midpoints, and prints a comma-separated list:

import re, subprocess

log = subprocess.run(
    ["ffmpeg", "-hide_banner", "-i", "input.mp4",
     "-vf", "blackdetect=d=0.5:pic_th=0.90:pix_th=0.12",
     "-an", "-sn", "-f", "null", "-"],
    capture_output=True, text=True).stderr

ranges = [(float(a), float(b)) for a, b in
          re.findall(r"black_start:([\d.]+) black_end:([\d.]+)", log)]

# drop a leading range that starts at 0 and a trailing one that runs to EOF
cuts = [round((a + b) / 2, 3) for a, b in ranges if a > 0.1]
print(",".join(str(c) for c in cuts))

Split in one pass with the segment muxer

Once you have the timestamps, don't loop -ss/-to once per segment. That decodes the file N times. The segment muxer does it in a single pass:

ffmpeg -i input.mp4 -c copy -map 0 \
  -f segment -segment_times 746.312,1503.275 \
  -reset_timestamps 1 \
  -segment_format mp4 out_%03d.mp4

-reset_timestamps 1 makes each output start at zero, so players and downstream tools don't choke on a segment whose first PTS is 746 seconds.

One caveat worth internalizing: with -c copy, cuts snap to the nearest keyframe. If your source has a 10-second GOP, a cut point can land up to 10 seconds off target. That's usually fine for fade-separated content, since the black run absorbs the error. When you need frame-accurate cuts, drop -c copy and re-encode the video, or force keyframes at your cut points on the way in.

If you need per-segment control instead, the two-argument form still works:

ffmpeg -ss 0 -to 746.312 -i input.mp4 -c copy seg_01.mp4
ffmpeg -ss 746.312 -to 1503.275 -i input.mp4 -c copy seg_02.mp4

blackdetect vs blackframe vs scene detection

Three different filters get recommended for this and they answer different questions.

FilterWhat it reportsBest for
`blackdetect`Ranges: start, end, durationSplitting at fades and ad breaks
`blackframe`One line per black frame, with a per-frame black percentageTuning thresholds, QC reports
`select='gt(scene,0.4)'`Frames where the picture changes sharplyCutting at hard camera or shot changes

Use blackframe=amount=90:threshold=40 when you're not sure what threshold your footage needs. It prints the actual black percentage of each frame it flags, which tells you exactly where to set pic_th. Then switch to blackdetect for the production run. If your source has no fades at all and you're chasing shot boundaries instead, the scene filter is the right tool, and we covered that separately in FFmpeg scene detection.

For ad breaks specifically, pair black with silence. Run silencedetect=n=-50dB:d=0.5 in the same graph and only cut where a black range and a silent range overlap. Ad boundaries are almost always both. A fade inside a program segment often isn't.

Common pitfalls

  • Default d=2 eats short fades. Most editorial fades run 0.5 to 1.5 seconds. Set d=0.5 or lower and filter the results afterward.
  • Letterbox bars inflate the black ratio. A pillarboxed 4:3 video inside a 16:9 frame is already 25% black, so pic_th behaves differently than you expect. Crop first: -vf "crop=iw:ih*0.75,blackdetect=...".
  • Compression noise keeps frames from reaching true black. H.264 at low bitrates leaves blocking artifacts with luma in the 20 to 45 range. Raise pix_th to 0.15 before assuming the fade isn't there.
  • A watermark or timecode caps your black ratio. If a logo covers 4% of the frame, no frame will ever pass pic_th=0.98. Drop it to 0.90.
  • The first and last ranges are usually not cuts. Files often open and close on black. Discard a range starting at 0 and one ending at the file duration, or you'll get two empty segments.
  • -ss before -i during detection shifts every timestamp. Detect on the full file, then apply the offset yourself if you're working on a trimmed region.
  • Suppressing logs suppresses results. blackdetect output only appears at info level or higher.

Running the same detection without a server

The detection pass is cheap to write and annoying to run. It needs FFmpeg installed at a known version, enough CPU to decode a long file, disk for the source and the segments, and a process that can stay alive for minutes without a platform timeout killing it. That's the actual cost, and it's why so many teams end up maintaining a media microservice for what is conceptually two commands.

FFmpeg Micro runs both passes as jobs against a managed FFmpeg toolkit. You submit a job over REST, poll or take a webhook, and download the outputs. No binaries, no versions, no servers to run.

curl -X POST https://api.ffmpeg-micro.com/jobs \
  -H "Authorization: Bearer $FFMPEG_MICRO_KEY" \
  -H "Content-Type: application/json" \
  -d @job.json

The job returns an id and a status you can poll, or you can hand it a webhook URL and get called back when the outputs are ready. Field names for the job body are in the docs. Same idea from n8n, Make, or Zapier: an HTTP node submits the job, a webhook node catches the result, and the workflow keeps going without a long-running execution sitting there burning a timeout. That polling pattern is worth reading if you've hit n8n timeouts on video steps. If you want to check duration, frame rate, or GOP length before you split, ffprobe covers the inspection side.

When not to use blackdetect

Don't reach for it when the segments in your file aren't separated by black at all. Hard cuts between scenes, jump cuts in a vlog, and chapter transitions with a graphic wipe produce zero black frames, and no threshold tuning will change that. Scene detection or a chapter-metadata read is the right call there.

It's also the wrong tool for finding a single known boundary. If you already know the ad starts at 12:30, a full decode pass to rediscover that is wasted work. And if your source is a DVD or broadcast capture with interlaced fades, deinterlace before detecting, because field-based fades confuse the per-frame ratio math.

FAQ

What is the default d value in ffmpeg blackdetect?

The default black_min_duration is 2 seconds, meaning FFmpeg only reports black runs lasting 2 seconds or longer. Most editorial fades are shorter than that, so set d=0.5 or d=0.2 when you're detecting fade transitions.

How do I get blackdetect output as JSON instead of log text?

Run the filter through ffprobe with the lavfi input format and request the frame tags: ffprobe -f lavfi -i "movie=input.mp4,blackdetect=d=0.5" -show_entries frame_tags=lavfi.black_start,lavfi.black_end -of json. The filter exports lavfi.black_start and lavfi.black_end as frame metadata, so you get structured output with no log parsing.

Why does ffmpeg blackdetect find nothing in my video?

The three usual causes are d being longer than your fades, pic_th at its 0.98 default while a watermark or letterbox keeps frames from reaching that ratio, and pix_th at 0.10 while compression noise leaves "black" pixels around luma 40. Try blackdetect=d=0.1:pic_th=0.85:pix_th=0.2 as a diagnostic, then tighten from there.

Can I split on black frames without re-encoding?

Yes. Pass your cut points to the segment muxer with -c copy, as in -f segment -segment_times 746.3,1503.2 -reset_timestamps 1. Cuts snap to the nearest keyframe, which can shift a boundary by up to one GOP length, but the black run usually absorbs that. Re-encode only when you need frame-accurate cuts.

Does blackdetect work on audio-only or silent sections?

No. blackdetect is a video filter and ignores audio entirely. For silence, use silencedetect=n=-50dB:d=0.5. Running both and cutting only where the two overlap is the most reliable way to find ad breaks in a recorded stream.

If your split step currently means keeping FFmpeg installed somewhere and hoping the process outlives the platform's timeout, run it as a job instead: sign up free and submit the detection pass and the segment cuts against the same API.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Ready to process videos at scale?

Start using FFmpeg Micro's simple API today. No infrastructure required.

Get Started Free