Oct 15, 8:22pm

On Linux the standard tool for this is ffmpeg: use blackdetect to find the black intervals, then rebuild the video with a trim + tpad (frame cloning) + concat filtergraph — each black segment is replaced by a clone of the last non-black frame, so total duration and audio sync are preserved.

Approach

  1. blackdetect logs every black interval (black_start / black_end).
  2. For each interval, build a filtergraph that:
    • keeps the video before the black segment (trim),
    • takes a tiny clip ending at the black start and extends it with tpad=stop_mode=clone (repeats the last frame for the whole black duration),
  3. concat all pieces back together and stream-copy the audio untouched.

Script

bash
#!/usr/bin/env bash
# usage: ./deblack.sh input.mp4 output.mp4
set -euo pipefail
in=$1; out=$2

# 1. Detect black segments (tune d= and pic_th= to your content)
ffmpeg -hide_banner -nostats -i "$in" \
  -vf blackdetect=d=0.1:pix_th=0.10:pic_th=0.98 \
  -an -f null - 2>&1 | grep blackdetect > black.log

grep -oP 'black_start:\K[0-9.]+|black_end:\K[0-9.]+' black.log | paste - - > blacks.txt

if [ ! -s blacks.txt ]; then
  echo "No black segments detected"; ffmpeg -i "$in" -c copy "$out"; exit 0
fi

fps=$(ffprobe -v error -select_streams v:0 -show_entries stream=avg_frame_rate -of csv=p=0 "$in")
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$in")

filter=""; maps=""; prev=0; i=0; segs=0

while read -r bs be; do
  bs=${bs%	*}; be=${be#*	}                       # split "start<TAB>end"
  # -- keep everything before the black segment
  filter+="[0:v]trim=start=$prev:end=$bs,setpts=PTS-STARTPTS,fps=$fps,settb=AVTB[p$i];"
  maps+="[p$i]"; i=$((i+1)); segs=$((segs+1))

  # -- frozen part: clip ending at black start, last frame cloned for the rest
  pre=$(awk -v x="$bs" 'BEGIN{d=x-0.05; if(d<0)d=0; printf "%.3f", d}')
  stop=$(awk -v s="$bs" -v e="$be" 'BEGIN{d=e-s-0.05; if(d<0)d=0; printf "%.3f", d}')
  filter+="[0:v]trim=start=$pre:end=$bs,setpts=PTS-STARTPTS,fps=$fps,tpad=stop_mode=clone:stop_duration=$stop,settb=AVTB[f$i];"
  maps+="[f$i]"; i=$((i+1)); segs=$((segs+1))

  prev=$be
done < blacks.txt

# -- tail after the last black segment
if awk -v p="$prev" -v d="$dur" 'BEGIN{exit !(p+0 < d+0)}'; then
  filter+="[0:v]trim=start=$prev,setpts=PTS-STARTPTS,fps=$fps,settb=AVTB[p$i];"
  maps+="[p$i]"; segs=$((segs+1))
fi

filter+="${maps}concat=n=$segs:v=1:a=0[v]"

# 2. Rebuild: re-encode video, copy audio (duration unchanged → stays in sync)
ffmpeg -i "$in" -filter_complex "$filter" \
  -map "[v]" -map 0:a? -c:v libx264 -crf 18 -preset medium -c:a copy "$out"

Tuning & caveats

  • blackdetect parameters: d= is the minimum black duration to report (default is ~2.36 s, so set it explicitly for short blips); pix_th=0.10 is the darkness threshold, pic_th=0.98 means 98 % of pixels must be black. Run it standalone first and check the log before encoding.
  • Video starts black: there is no "last frame" yet, so the first black segment would freeze a black frame. Trim an initial black manually (e.g. -ss) if that's your case.
  • Re-encoding is unavoidable (filters), but audio can be stream-copied since the timeline length is unchanged.
  • Very short blacks (< ~50 ms) are skipped by the stop_duration guard.
  • If you have many segments, the single-input trim chains decode the whole file repeatedly — it still works, just slower; for huge files you can split first and process per segment.

Test on a short excerpt first (e.g. add -t 30 around the detection step and compare timestamps in black.log).

Oct 15, 8:22pm

This is a shared conversation and may include unverified content that doesn't reflect Mistral AI's views. Login to resume chatting.

Vibe can make mistakes. Check answers. Learn more