Skip to content

Instantly share code, notes, and snippets.

@MarcusOtter
Created July 29, 2026 21:42
Show Gist options
  • Select an option

  • Save MarcusOtter/cb4d9a7bf39fc502632210a233edf5f7 to your computer and use it in GitHub Desktop.

Select an option

Save MarcusOtter/cb4d9a7bf39fc502632210a233edf5f7 to your computer and use it in GitHub Desktop.
Compress any video to 8mb similar to 8mb.video. Usage: "./to8mb.sh input.mp4"
#!/usr/bin/env bash
set -Eeuo pipefail
export LC_ALL=C
usage() {
printf 'Usage: %s INPUT [OUTPUT]\n' "$(basename "$0")" >&2
exit 2
}
die() {
printf 'Error: %s\n' "$*" >&2
exit 1
}
[[ $# -ge 1 && $# -le 2 ]] || usage
input=$1
output=${2:-}
limit_bytes=${LIMIT_BYTES:-8000000}
target_bytes=$((limit_bytes - 100000))
[[ -f "$input" ]] || die "input file not found: $input"
(( limit_bytes > 500000 )) || die "LIMIT_BYTES is too small"
for cmd in ffmpeg ffprobe awk stat mktemp; do
command -v "$cmd" >/dev/null || die "missing dependency: $cmd"
done
if [[ -z "$output" ]]; then
dir=$(dirname -- "$input")
file=$(basename -- "$input")
stem=${file%.*}
output="$dir/${stem}_8mb.mp4"
fi
[[ "$input" != "$output" ]] || die "input and output paths must differ"
video_stream=$(ffprobe -v error -select_streams V:0 \
-show_entries stream=index -of csv=p=0 "$input" | head -n1)
[[ -n "$video_stream" ]] || die "no video stream found"
duration=$(ffprobe -v error -show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 "$input" | head -n1)
awk -v d="$duration" 'BEGIN { exit !(d > 0) }' || die "could not determine video duration"
has_audio=$(ffprobe -v error -select_streams a:0 \
-show_entries stream=index -of csv=p=0 "$input" | head -n1 || true)
# Reserve about 1.5% for MP4/container overhead.
total_kbps=$(awk -v b="$target_bytes" -v d="$duration" \
'BEGIN { printf "%d", (b * 8 / d / 1000) * 0.985 }')
if [[ -n "$has_audio" ]]; then
if (( total_kbps >= 1200 )); then audio_kbps=128
elif (( total_kbps >= 600 )); then audio_kbps=96
elif (( total_kbps >= 300 )); then audio_kbps=64
elif (( total_kbps >= 180 )); then audio_kbps=48
else audio_kbps=32
fi
else
audio_kbps=0
fi
video_kbps=$((total_kbps - audio_kbps))
if (( video_kbps < 40 && audio_kbps > 0 )); then
audio_kbps=0
video_kbps=$total_kbps
fi
(( video_kbps >= 10 )) || die "video is too long to fit meaningfully within 8 MB"
if (( video_kbps >= 2200 )); then max_dim=1920
elif (( video_kbps >= 900 )); then max_dim=1280
elif (( video_kbps >= 500 )); then max_dim=960
elif (( video_kbps >= 300 )); then max_dim=854
elif (( video_kbps >= 160 )); then max_dim=640
else max_dim=426
fi
vf="scale='min(iw,${max_dim})':'min(ih,${max_dim})':force_original_aspect_ratio=decrease:force_divisible_by=2,setsar=1"
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
passlog="$tmpdir/pass"
tmp_output="$tmpdir/output.mp4"
encode() {
local bitrate=$1
rm -f "${passlog}"* "$tmp_output"
ffmpeg -hide_banner -loglevel warning -stats -y -i "$input" \
-map 0:V:0 -vf "$vf" -fpsmax 30 \
-c:v libx264 -preset slow -b:v "${bitrate}k" -pix_fmt yuv420p \
-pass 1 -passlogfile "$passlog" \
-an -sn -dn -f null /dev/null
audio_args=(-an)
if (( audio_kbps > 0 )); then
audio_args=(-map '0:a:0?' -c:a aac -b:a "${audio_kbps}k" -ac 2)
(( audio_kbps <= 48 )) && audio_args=(-map '0:a:0?' -c:a aac -b:a "${audio_kbps}k" -ac 1)
fi
ffmpeg -hide_banner -loglevel warning -stats -y -i "$input" \
-map 0:V:0 "${audio_args[@]}" -vf "$vf" -fpsmax 30 \
-c:v libx264 -preset slow -b:v "${bitrate}k" -pix_fmt yuv420p \
-pass 2 -passlogfile "$passlog" \
-sn -dn -map_metadata -1 -map_chapters -1 \
-movflags +faststart "$tmp_output"
}
printf 'Duration: %.1f s\n' "$duration"
printf 'Video: %d kbps, audio: %d kbps, max dimension: %d px\n' \
"$video_kbps" "$audio_kbps" "$max_dim"
for attempt in 1 2 3; do
printf 'Encoding pass set %d/3...\n' "$attempt"
encode "$video_kbps"
actual_bytes=$(stat -c %s "$tmp_output")
if (( actual_bytes <= limit_bytes )); then
break
fi
video_kbps=$(awk -v r="$video_kbps" -v max="$limit_bytes" -v got="$actual_bytes" \
'BEGIN { n = int(r * (max * 0.98 / got)); if (n < 10) n = 10; print n }')
done
actual_bytes=$(stat -c %s "$tmp_output")
(( actual_bytes <= limit_bytes )) || die "could not get output below 8 MB after 3 attempts"
mv -f -- "$tmp_output" "$output"
printf 'Created: %s (%.2f MB)\n' "$output" "$(awk -v b="$actual_bytes" 'BEGIN { print b / 1000000 }')"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment