Skip to content

Instantly share code, notes, and snippets.

@gphg
Last active June 15, 2026 17:10
Show Gist options
  • Select an option

  • Save gphg/e20597972a3c69f7f174dfd8281bd8b5 to your computer and use it in GitHub Desktop.

Select an option

Save gphg/e20597972a3c69f7f174dfd8281bd8b5 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# ==========================================
# Configuration & Helpers
# ==========================================
VIDEO_IN="$1"
AUDIO_SRC="${2:-${VIDEO_IN%.*}.mp4}"
TARGET_MB="${3:-20}"
# Check dependencies
for cmd in ffmpeg ffprobe bc; do
if ! command -v $cmd &> /dev/null; then
echo "Error: '$cmd' is not installed."
exit 1
fi
done
if [[ -z "$VIDEO_IN" || ! -f "$VIDEO_IN" || ! -f "$AUDIO_SRC" ]]; then
echo "Usage: $0 <input_video> [audio_source_mp4] [target_size_mb]"
exit 1
fi
# Opus bitrates constraints
MIN_BITRATE=12000
MAX_BITRATE=512000
TARGET_SIZE_BYTES=$(echo "$TARGET_MB * 1048576" | bc)
TEMP_FILE=".tmp_remux_$$.webm"
trap 'rm -f "$TEMP_FILE"' EXIT INT TERM
# ==========================================
# 1. Analyze Components
# ==========================================
VIDEO_SIZE_BYTES=$(ffprobe -v error -select_streams v:0 -show_entries packet=size -bitexact -of csv=p=0 "$VIDEO_IN" | awk '{sum+=$1} END {print sum}')
DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$VIDEO_IN")
VIDEO_MB=$(echo "scale=2; $VIDEO_SIZE_BYTES / 1048576" | bc)
echo "Video Size: $VIDEO_MB MB and $DURATION seconds."
# Estimate initial bitrate to get into the ballpark
AVAILABLE_BYTES=$(echo "$TARGET_SIZE_BYTES - $VIDEO_SIZE_BYTES" | bc)
INITIAL_BITRATE=$(echo "($AVAILABLE_BYTES * 8) / $DURATION" | bc)
# ==========================================
# 2. Refined Binary Search
# ==========================================
# Define range +/- 20% of the initial estimate
LOW=$(echo "$INITIAL_BITRATE * 0.8" / 1 | bc)
HIGH=$(echo "$INITIAL_BITRATE * 1.2" / 1 | bc)
BEST_BITRATE=$INITIAL_BITRATE
ITERATIONS=16 # Sufficient for bitrate precision
echo "Optimizing audio bitrate with ${ITERATIONS}-iteration refinement..."
[ "$LOW" -lt "$MIN_BITRATE" ] && LOW=$MIN_BITRATE
[ "$HIGH" -gt "$MAX_BITRATE" ] && HIGH=$MAX_BITRATE
# If constraints make search range invalid, just set to max or min
[ "$LOW" -gt "$HIGH" ] && LOW=$MAX_BITRATE && HIGH=$MAX_BITRATE
for ((i=1; i<=ITERATIONS; i++)); do
MID=$(( (LOW + HIGH) / 2 ))
# Run ffmpeg and capture the exit code
ffmpeg -nostdin -y -i "$VIDEO_IN" -i "$AUDIO_SRC" \
-map 0:v:0 -map 1:a \
-c:v copy -c:a libopus -b:a "${MID}" \
"$TEMP_FILE" > /dev/null 2>&1
_EXIT_CODE=$?
# Only process results if ffmpeg succeeded
if [ $_EXIT_CODE -eq 0 ]; then
FILE_SIZE=$(stat -c%s "$TEMP_FILE")
if [ "$FILE_SIZE" -le "$TARGET_SIZE_BYTES" ]; then
DIFF=$(( TARGET_SIZE_BYTES - FILE_SIZE ))
BEST_BITRATE=$MID
printf "\r[Iter %2d/%d] Captured best: %dbps... (diff: %d)" "$i" "$ITERATIONS" "$MID" "$DIFF"
LOW=$(( MID + 1 ))
else
HIGH=$(( MID - 1 ))
fi
else
echo "Warning: FFmpeg failed at bitrate ${MID}. Skipping iteration."
# Optionally adjust your bounds or break if the failure is critical
HIGH=$(( MID - 1 ))
fi
done
echo ""
# Final check
FINAL_SIZE=$(stat -c%s "$TEMP_FILE")
if [ "$FINAL_SIZE" -gt "$TARGET_SIZE_BYTES" ]; then
echo "Warning: Even at the lowest bitrate, the file exceeds the target size."
fi
# ==========================================
# 3. Final Output
# ==========================================
FILENAME=$(basename "${VIDEO_IN%.*}")
OUTPUT="audio_optimized_${FILENAME}_${TARGET_MB}MB.webm"
ffmpeg -nostdin -y -i "$VIDEO_IN" -i "$AUDIO_SRC" \
-map 0:v:0 -map 1:a \
-c:v copy -c:a libopus -b:a "${BEST_BITRATE}" \
"$OUTPUT" > /dev/null 2>&1
BEST_BITRATE_KB=$(( BEST_BITRATE / 1024 ))
FINAL_MB=$(echo "scale=2; $(stat -c%s "$OUTPUT") / 1048576" | bc)
echo "------------------------------------------------"
echo "Success! Final Size: ${FINAL_MB} MB (target: ${TARGET_MB} MB)"
echo "Selected Audio Bitrate: ${BEST_BITRATE_KB} kbps (${BEST_BITRATE})"
echo "Saved to: $OUTPUT"
echo "------------------------------------------------"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment