Skip to content

Instantly share code, notes, and snippets.

@mathieu-b
Created September 14, 2026 11:35
Show Gist options
  • Select an option

  • Save mathieu-b/ce4d3bcc168a3b85f8456a417d92f527 to your computer and use it in GitHub Desktop.

Select an option

Save mathieu-b/ce4d3bcc168a3b85f8456a417d92f527 to your computer and use it in GitHub Desktop.
Reusable script for converting arbitrary videos into WhatsApp-compatible MP4 files. The script uses ffmpeg to produce H.264/AAC output with yuv420p pixel format, fast-start MP4 metadata, and a conservative default max dimension of 1280 px. It supports macOS/Homebrew and Ubuntu/apt-get dependency installation paths.
#!/usr/bin/env bash
set -euo pipefail
readonly DEFAULT_MAX_DIMENSION_PX=1280
readonly DEFAULT_CRF=23
readonly DEFAULT_PRESET="medium"
readonly DEFAULT_SUFFIX="_whatsapp"
max_dimension_px="${DEFAULT_MAX_DIMENSION_PX}"
crf="${DEFAULT_CRF}"
preset="${DEFAULT_PRESET}"
suffix="${DEFAULT_SUFFIX}"
output_dir=""
output_file=""
overwrite=false
auto_install_dependencies=true
input_files=()
function show_usage() {
echo "Usage: $0 [options] <video-file> [video-file ...]"
echo
echo "Converts videos to WhatsApp-compatible MP4 files:"
echo " - H.264 video"
echo " - AAC audio"
echo " - yuv420p pixel format"
echo " - fast-start MP4 metadata"
echo " - longest side capped to 1280 px by default, without upscaling"
echo
echo "Options:"
echo " --output <file> Output file path. Only valid with one input."
echo " --output-dir <dir> Directory for converted files."
echo " --suffix <suffix> Output filename suffix before .mp4. Default: ${DEFAULT_SUFFIX}"
echo " --max-dimension <px> Cap the longest side to this size. Default: ${DEFAULT_MAX_DIMENSION_PX}"
echo " --crf <value> H.264 quality: lower is larger/better. Default: ${DEFAULT_CRF}"
echo " --preset <preset> libx264 preset. Default: ${DEFAULT_PRESET}"
echo " --overwrite Replace existing output files."
echo " --no-install-deps Fail instead of installing/reinstalling ffmpeg."
echo " --help, -h Show this help."
echo
echo "Examples:"
echo " $0 VID_20260908_093552.mp4"
echo " $0 --overwrite --output-dir /tmp *.mov"
echo " $0 --max-dimension 1920 --crf 21 input.mkv"
}
function parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--output)
output_file="${2:-}"
if [[ -z "${output_file}" ]]; then
echo "ERROR: --output requires a file path."
exit 1
fi
shift 2
;;
--output-dir)
output_dir="${2:-}"
if [[ -z "${output_dir}" ]]; then
echo "ERROR: --output-dir requires a directory path."
exit 1
fi
shift 2
;;
--suffix)
suffix="${2:-}"
if [[ -z "${suffix}" ]]; then
echo "ERROR: --suffix requires a non-empty value."
exit 1
fi
shift 2
;;
--max-dimension)
max_dimension_px="${2:-}"
if ! [[ "${max_dimension_px}" =~ ^[1-9][0-9]*$ ]]; then
echo "ERROR: --max-dimension must be a positive integer."
exit 1
fi
shift 2
;;
--crf)
crf="${2:-}"
if ! [[ "${crf}" =~ ^[0-9]+$ ]] || [[ "${crf}" -gt 51 ]]; then
echo "ERROR: --crf must be an integer between 0 and 51."
exit 1
fi
shift 2
;;
--preset)
preset="${2:-}"
if [[ -z "${preset}" ]]; then
echo "ERROR: --preset requires a value."
exit 1
fi
shift 2
;;
--overwrite)
overwrite=true
shift
;;
--no-install-deps)
auto_install_dependencies=false
shift
;;
--help|-h)
show_usage
exit 0
;;
--)
shift
while [[ $# -gt 0 ]]; do
input_files+=("$1")
shift
done
;;
-*)
echo "ERROR: Unknown option: $1"
show_usage
exit 1
;;
*)
input_files+=("$1")
shift
;;
esac
done
}
function command_works() {
local command_name="$1"
command -v "${command_name}" >/dev/null 2>&1 && "${command_name}" -version >/dev/null 2>&1
}
function install_or_repair_ffmpeg_on_macos() {
if ! command -v brew >/dev/null 2>&1; then
echo "ERROR: ffmpeg is unavailable and Homebrew is not installed."
echo "Install Homebrew first: https://brew.sh/"
exit 1
fi
echo "Installing or repairing ffmpeg with Homebrew ..."
if command -v ffmpeg >/dev/null 2>&1; then
brew reinstall ffmpeg || brew reinstall ffmpeg-full || brew install ffmpeg
else
brew install ffmpeg || brew install ffmpeg-full
fi
}
function install_or_repair_ffmpeg_on_linux() {
if ! command -v apt-get >/dev/null 2>&1; then
echo "ERROR: ffmpeg is unavailable and apt-get is not installed."
exit 1
fi
local sudo_command=""
if [[ "$(id -u)" -ne 0 ]]; then
sudo_command="sudo"
fi
echo "Installing or repairing ffmpeg with apt-get ..."
${sudo_command} apt-get update
${sudo_command} apt-get install -y ffmpeg
}
function ensure_ffmpeg_available() {
if command_works ffmpeg; then
return
fi
if ! ${auto_install_dependencies}; then
echo "ERROR: ffmpeg is not available or does not run correctly."
exit 1
fi
case "$(uname -s)" in
Darwin)
install_or_repair_ffmpeg_on_macos
;;
Linux)
install_or_repair_ffmpeg_on_linux
;;
*)
echo "ERROR: Unsupported OS: $(uname -s)"
echo "Install ffmpeg manually, then rerun this script."
exit 1
;;
esac
if ! command_works ffmpeg; then
echo "ERROR: ffmpeg still does not run correctly after installation attempt."
exit 1
fi
}
function validate_args() {
if [[ "${#input_files[@]}" -eq 0 ]]; then
show_usage
exit 1
fi
if [[ -n "${output_file}" && "${#input_files[@]}" -ne 1 ]]; then
echo "ERROR: --output can only be used with one input file."
exit 1
fi
if [[ -n "${output_file}" && -n "${output_dir}" ]]; then
echo "ERROR: Use either --output or --output-dir, not both."
exit 1
fi
for input_file in "${input_files[@]}"; do
if [[ ! -f "${input_file}" ]]; then
echo "ERROR: Input file does not exist: ${input_file}"
exit 1
fi
done
if [[ -n "${output_dir}" ]]; then
mkdir -p "${output_dir}"
fi
}
function get_output_path() {
local input_file="$1"
if [[ -n "${output_file}" ]]; then
printf "%s\n" "${output_file}"
return
fi
local input_dir
local input_basename
local input_stem
input_dir="$(dirname "${input_file}")"
input_basename="$(basename "${input_file}")"
input_stem="${input_basename%.*}"
if [[ -n "${output_dir}" ]]; then
printf "%s/%s%s.mp4\n" "${output_dir%/}" "${input_stem}" "${suffix}"
else
printf "%s/%s%s.mp4\n" "${input_dir}" "${input_stem}" "${suffix}"
fi
}
function convert_file() {
local input_file="$1"
local output_path
output_path="$(get_output_path "${input_file}")"
if [[ "${input_file}" == "${output_path}" ]]; then
echo "ERROR: Refusing to overwrite input file: ${input_file}"
exit 1
fi
if [[ -e "${output_path}" && "${overwrite}" != true ]]; then
echo "ERROR: Output file already exists: ${output_path}"
echo "Use --overwrite to replace it."
exit 1
fi
local overwrite_flag="-n"
if ${overwrite}; then
overwrite_flag="-y"
fi
echo "Converting '${input_file}' -> '${output_path}'"
ffmpeg \
-hide_banner \
"${overwrite_flag}" \
-i "${input_file}" \
-map 0:v:0 \
-map "0:a?" \
-vf "scale='if(gt(iw,ih),min(${max_dimension_px},iw),-2)':'if(gt(iw,ih),-2,min(${max_dimension_px},ih))',format=yuv420p" \
-c:v libx264 \
-preset "${preset}" \
-crf "${crf}" \
-profile:v high \
-level 4.0 \
-c:a aac \
-b:a 128k \
-ac 2 \
-movflags +faststart \
"${output_path}"
}
function main() {
parse_args "$@"
validate_args
ensure_ffmpeg_available
for input_file in "${input_files[@]}"; do
convert_file "${input_file}"
done
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment