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
blackdetect logs every black interval (black_start / black_end).
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),
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.