Pre-delivery QA: black frames, silence, and a clean cut

ffmpeg guide · QA · delivery

A clip can look fine on a casual watch and still ship with a dead 2-second black hole or a silent gap where narration should be. This is the scan that catches both, the cut method that avoids the most common keyframe trap, and the habit of writing down what you shipped.

Scan for black frames and silence

Run blackdetect and silencedetect as null-output filters and grep the log for their markers:


ffmpeg -hide_banner -loglevel info -i final.mp4 \
  -vf "blackdetect=d=0.1:pix_th=0.10" -af "silencedetect=n=-45dB:d=0.4" -f null - 2>&1 \
  | grep -E "black_start|silence_start|silence_end" \
  || echo "clean: no black frames or silent gaps flagged."

blackdetect=d=0.1:pix_th=0.10 flags any run of frames at least 0.1s long where at least 90% of pixels read as near-black (pix_th is the per-pixel darkness threshold). silencedetect=n=-45dB:d=0.4 flags any stretch of audio at least 0.4s long that stays below −45dB. Both thresholds are tunable per source — a dark cinematic shot needs a stricter pix_th than a bright product demo, and a noisier recording needs a lower n floor than a clean studio track. The toolkit script prints a "clean" line only when neither filter reports anything, and returns each black_start/silence_start/silence_end marker it found otherwise so you can line them up against your edit.

Frame-accurate cut (and why it re-encodes)

Trim to the final duration with a re-encode, not a stream copy:


ffmpeg -hide_banner -y -ss 00:00:00.000 -to 00:00:05.000 -i shot01_captioned.mp4 \
  -c:v libx264 -preset veryfast -crf 18 -c:a aac -b:a 192k -avoid_negative_ts make_zero shot01_final.mp4

START/END are on the source timeline (seconds like 2.5, or HH:MM:SS.mmm).

The keyframe-snap trap

A stream-copy cut (-c copy, no re-encode) can only start a new file at an existing keyframe — it cannot decode-and-re-encode a partial GOP, so ffmpeg snaps the cut point to the nearest keyframe at or before your requested timestamp. On footage with sparse keyframes that snap can land a full second or more off, silently, with no error. Re-encoding with -c:v libx264 -preset veryfast -crf 18 forces ffmpeg to actually decode and re-encode from the exact requested frame, at the cost of encode time and a fresh compression pass instead of a lossless copy. -avoid_negative_ts make_zero resets the output timestamps to start at zero instead of inheriting the source's original offset, which avoids a common sync glitch in some players.

Delivery-receipt habit

Once QA is clean, write down what actually shipped instead of trusting memory. An ffprobe pass gives you the hard facts for the receipt:

ffprobe -v error -show_format -show_streams -of json shot01_final.mp4

A minimal receipt records: duration, resolution/fps, video codec and pixel format, audio codec/bitrate/sample rate, the integrated loudness and true peak from your loudnorm verification pass, subtitle burn-in status, source provenance, and QA status (clean scan or explained exceptions). It takes two minutes and turns "I think this is fine" into something checkable later, by you or anyone else on the delivery.