Ahosting Logo
Knowledge Base

How to Detect Scenes, Silence and Black Frames with FFmpeg

Four detection filters and what each answersFFmpeg as an analyser rather than a converterScene changetimestamps where the picturechanges substantiallySilencewhere the audio drops below athreshold, and for how longBlack frameswhere the picture is black,which usually means a join or agapFreeze frameswhere nothing changes, whichusually means a faultWhat it is forchapters, ad breaks, qualitychecks, automatic trimmingThe outputtimestamps on the log, which ascript readsThese answer questions about content that would otherwise need somebody to watch the whole file.

FFmpeg is usually reached for as a converter. Its detection filters do something different: they read a file and tell you about its contents, which is what turns manual review into a script.

All of them share one shape: decode the file, produce no output, read the report:

ffmpeg -i input.mp4 -vf ... -f null - 2>&1

The -f null - discards the video. The 2>&1 matters: these filters write to the error stream, and piping the obvious way returns nothing at all.

Scene changes

ffmpeg -i input.mp4 -vf "select='gt(scene,0.4)',metadata=print" -f null - 2>&1 | grep pts_time

Each line is a timestamp where the picture changed substantially. The threshold is between 0 and 1: lower finds more, higher finds only hard cuts. 0.3 to 0.4 is a reasonable starting range, and the right value depends on the material.

The most useful application is thumbnails. Picking a frame at ten seconds gives you whatever happened to be there, which is regularly a blur or a black frame between shots. Picking the first scene change gives you a frame that begins a shot, which is far more often a usable image.

ffmpeg -i input.mp4 -vf "select='gt(scene,0.4)',scale=640:-2" -frames:v 1 thumb.jpg

Creating video thumbnails and previews goes into the rest of that job.

Silence

ffmpeg -i input.mp4 -af "silencedetect=noise=-30dB:d=1" -f null - 2>&1 | grep silence_

Two parameters: the level below which audio counts as silence, and how long it must stay there. Raising the threshold above −30 dB starts catching room tone; lowering it misses genuine pauses.

The output gives start, end and duration for each silent run. Two things fall out of it directly.

Trimming dead air. Recordings routinely begin with several seconds of someone finding the record button. The first silence end is where the content starts.

Chapter boundaries. In a long recording, silences longer than a few seconds are usually topic changes. That is a workable first draft of a chapter list. For writing it into the file, see editing metadata and chapters.

Black frames

ffmpeg -i input.mp4 -vf "blackdetect=d=0.5:pix_th=0.1" -f null - 2>&1 | grep black_start

Reports sections that are essentially black for at least the given duration.

The obvious use is finding gaps between segments. The more valuable one is validation: if the whole file is reported black, the encode failed and produced a technically valid video of nothing.

That failure mode is common with user uploads and unusual source formats, and it is invisible to any check that only asks whether the file exists and has a duration. A black check on every processed upload catches it before a customer does. Handling video uploads sets out where to put it.

Audio levels

ffmpeg -i input.mp4 -af volumedetect -f null - 2>&1 | grep -E 'mean_volume|max_volume'

Peak and mean in one line each. This is the measurement to take before deciding anything about audio.

A max_volume of 0 dB means the audio is already at the ceiling and may be clipped. A mean far below −20 dB means it will sound quiet next to everything else the visitor plays.

Do not correct by guessing at a gain figure, loudness normalisation targets a standard and is the right tool. Normalising audio loudness explains it.

Splitting on detected points

Once you have timestamps, splitting is a copy operation instead of a re-encode:

ffmpeg -i input.mp4 -ss 0 -to 125.4 -c copy part1.mp4
ffmpeg -i input.mp4 -ss 125.4 -to 302.1 -c copy part2.mp4

Because nothing is decoded, the cuts land on keyframes and may move by a second or two, fine for segmenting, not fine for precise editing. Trimming, cutting and concatenating goes over the accurate alternative.

Detection costs a full decode

Every one of these reads the whole file. On a long video that is not instant, and running four separate detections means decoding four times.

Combine what you can into a single command with several filters, and treat detection as part of the processing queue rather than something to run on request in front of a waiting user.

Detect on a sample, then act on the whole file

Every detection reads the entire video, which on a long file is the expensive part. Two ways to avoid paying it twice.

ffmpeg -ss 0 -t 300 -i input.mp4 -vf "select='gt(scene,0.4)',metadata=print" -f null - 2>&1 | grep pts_time

Analysing the first few minutes is enough to calibrate a threshold. Once the number is right, run it against the whole file once rather than adjusting and re-running repeatedly.

The other saving is combining detections into a single decode:

ffmpeg -i input.mp4 -vf "blackdetect=d=0.5" -af "silencedetect=n=-30dB:d=1" -f null - 2>&1 \
 | grep -E 'black_start|silence_'

One pass, two answers. On a queue processing many files that difference is substantial. Running FFmpeg jobs in parallel sets out where the time actually goes.

Turning detections into usable timestamps

The output is text and needs parsing before it is useful. The pattern worth having:

ffmpeg -i input.mp4 -vf "select='gt(scene,0.4)',metadata=print" -f null - 2>&1 \
 | grep -oP 'pts_time:\K[0-9.]+' > scenes.txt
head -5 scenes.txt

That produces one number per line, in seconds, which can be fed directly into a thumbnail command or a chapter file.

For silence, the useful values are the ends rather than the starts; a silence ending is where content resumes:

... | grep -oP 'silence_end: \K[0-9.]+' > content-starts.txt

Editing metadata and chapters deals with writing those into the file.

Thresholds are content-dependent

The default values work on typical material and fail on two kinds.

A video shot in low light never reaches the black threshold, so blackdetect reports nothing on footage that looks black to a viewer. Raising pix_th catches it.

A recording with constant room tone never falls below the silence threshold, so silencedetect finds no pauses at all. Raising the noise figure from −30 dB towards −25 dB finds them, at the cost of catching genuine quiet passages.

Calibrate against a file you know rather than trusting a default, and check the result against what you can hear or see. A detection that finds nothing is more often a wrong threshold than an absence.

Validation is the highest-value use

Of everything on this page, the check worth automating is the simplest: did the output actually contain a picture.

dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 out.mp4)
blk=$(ffmpeg -i out.mp4 -vf "blackdetect=d=1:pix_th=0.1" -f null - 2>&1 | grep -c black_start)
[ "$blk" -gt 0 ] && echo "WARNING: black sections in output"

An encode that succeeds and produces a valid video of nothing is a common failure with unusual source formats, and it is invisible to any check that only asks whether the file exists. Handling video uploads explains where in the pipeline it belongs.