Skip to content

Instantly share code, notes, and snippets.

@gphg
Last active September 15, 2025 05:26
Show Gist options
  • Save gphg/b1b0dc152bf60a606afd6dbf55c33319 to your computer and use it in GitHub Desktop.
Save gphg/b1b0dc152bf60a606afd6dbf55c33319 to your computer and use it in GitHub Desktop.
A handy script for sharing meme videos across the internet.
#!/usr/bin/bash
# Define custom exit codes
EXIT_SUCCESS=0
EXIT_MISSING_ARGS=1
EXIT_FFPROBE_FAILED=2
EXIT_ZERO_DURATION=3
EXIT_FFMPEG_FAILED=4
EXIT_TIME_PARSING_FAILED=5
EXIT_COMMAND_NOT_FOUND=6
EXIT_OVERWRITE_PREVENTED=7
EXIT_NAMED_PIPE_FAILED=8
# --- Script Configuration (read from environment or use defaults) ---
# Target audio bitrate in kbps.
AUDIO_BITRATE_KBPS=${AUDIO_BITRATE_KBPS:-96}
DEFAULT_TARGET_SIZE_MIB=100 # Default target output size if not specified
# Set optional trimming variables from the environment
START_TIME=${START_TIME:-""}
END_TIME=${END_TIME:-""}
# Set optional FPS variable from the environment
TARGET_FPS=${TARGET_FPS:-""}
# Set optional video speed variable from the environment
TARGET_SPEED=${TARGET_SPEED:-1.0}
# Set optional audio muting variable from the environment
MUTE_AUDIO=${MUTE_AUDIO:-""}
# --- Time Logging Variables ---
SCRIPT_START_TIME=$(date +%s)
SCRIPT_START_TIME_HUMAN=$(date +"%Y-%m-%d %H:%M:%S")
# --- Check for required commands ---
# `bc` is used for floating-point arithmetic.
# `ffprobe`, `ffmpeg`, and `realpath` are the core tools.
for cmd in bc ffprobe ffmpeg realpath; do
if ! command -v "$cmd" &> /dev/null; then
echo "Error: Required command '$cmd' not found. Please install it." >&2
exit "$EXIT_COMMAND_NOT_FOUND"
fi
done
# --- Function to parse a time string (e.g., HH:MM:SS.mmm or SS) to seconds ---
# This is a helper function to correctly handle the time calculations.
parse_time_to_seconds() {
local time_str="$1"
# Regex to check for HH:MM:SS format
if [[ "$time_str" =~ ^([0-9]+):([0-9]{2}):([0-9]{2}(\.[0-9]+)?)$ ]]; then
local hours=${BASH_REMATCH[1]}
local minutes=${BASH_REMATCH[2]}
local seconds=${BASH_REMATCH[3]}
# Use awk to handle floating point math
awk -v h="$hours" -v m="$minutes" -v s="$seconds" 'BEGIN { printf "%.3f", h*3600 + m*60 + s }'
# Regex to check for MM:SS format
elif [[ "$time_str" =~ ^([0-9]+):([0-9]{2}(\.[0-9]+)?)$ ]]; then
local minutes=${BASH_REMATCH[1]}
local seconds=${BASH_REMATCH[2]}
awk -v m="$minutes" -v s="$seconds" 'BEGIN { printf "%.3f", m*60 + s }'
# Check for simple seconds format
elif [[ "$time_str" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
# It's already in seconds, no need to convert
echo "$time_str"
else
echo "" # Return empty string for invalid format
fi
}
# --- Function to monitor and display FFmpeg progress ---
# Reads progress data from a named pipe (FIFO)
monitor_progress() {
local pass_name="$1"
local fifo_path="$2"
local total_duration="$3"
while read -r line; do
if [[ "$line" =~ ^out_time_ms ]]; then
# Extract milliseconds from the line
current_time_ms=$(echo "$line" | cut -d'=' -f2)
current_time_seconds=$(echo "scale=3; $current_time_ms / 1000000" | bc -l)
# Calculate percentage using `bc` for robust floating-point math
local percentage
if [ "$(echo "$total_duration == 0" | bc -l)" -eq 1 ]; then
percentage=0.0
else
percentage=$(echo "scale=1; ($current_time_seconds / $total_duration) * 100" | bc -l)
# Clamp the percentage to be within 0 and 100
if [ "$(echo "$percentage > 100" | bc -l)" -eq 1 ]; then
percentage=100.0
fi
if [ "$(echo "$percentage < 0" | bc -l)" -eq 1 ]; then
percentage=0.0
fi
fi
# Print the progress bar
printf "\r--- %s: %.1f%% complete ---" "$pass_name" "$percentage"
fi
done < "$fifo_path"
# Print a newline to clean up the progress bar
echo
}
# --- Argument Parsing ---
# Initialize positional arguments with empty values
OUTPUT_FILE=""
TARGET_SIZE_MIB="${DEFAULT_TARGET_SIZE_MIB}"
TARGET_SMALLEST_DIMENSION=""
INPUT_FILE="$1" # The first argument is always the input file
# --- Usage Instructions --
if [ -z "$INPUT_FILE" ]; then
echo "About this script:"
echo "------------------"
echo "This script converts video files to the WebM format while targeting a specific output file size."
echo "It is an ideal tool for sharing videos on platforms with strict file size limitations, such as"
echo "Discord's 100 MB limit for non-Nitro users. The script specifically uses the WebM format because"
echo "Discord automatically downscales MP4 videos larger than 720p, whereas WebM videos often"
echo "bypass this limitation, allowing you to maintain better quality and resolution."
echo ""
echo "This script was developed with the assistance of Gemini AI."
echo ""
echo "Usage: $0 <input_video> [output_video.webm] [target_size_mib] [target_smallest_dimension]"
echo ""
echo " <input_video> : Path to the input video file (e.g., .mp4, .mov, .mkv). (REQUIRED)"
echo " [output_video.webm] : (Optional) Desired path for the output WebM video file."
echo " If omitted, saves as original input video filename with .webm extension in current working directory."
echo " [target_size_mib] : (Optional) Desired output file size in MiB. Defaults to ${DEFAULT_TARGET_SIZE_MIB} MiB."
echo " [target_smallest_dimension] : (Optional) The target size for the video's smallest dimension (e.g., '720' for 720p equivalent)."
echo " The other dimension will be calculated to maintain aspect ratio."
echo " If omitted, no scaling is applied."
echo ""
echo "Optional environment variables:"
echo " START_TIME : The start time for trimming (e.g., '00:01:30' or '90')."
echo " END_TIME : The end time for trimming (e.g., '00:03:00' or '180')."
echo " (You can use START_TIME only, END_TIME only, or both.)"
echo ""
echo " AUDIO_BITRATE_KBPS: Target audio bitrate in kbps (e.g., '128'). Defaults to 96 kbps. Set to 0 to mute audio."
echo " TARGET_FPS: Target frames per second (e.g., '24' or '30'). If omitted, the original FPS is used."
echo " TARGET_SPEED: Playback speed factor (e.g., '0.5' for half speed, '1.5' for 1.5x speed). Defaults to 1.0."
echo " MUTE_AUDIO: Set to any non-empty value (e.g., 'true', '1') to mute the audio track."
echo ""
echo "Examples:"
echo " $0 my_video.mp4" # auto-output, default size, no scale, no trim
echo " TARGET_FPS=24 $0 my_video.mp4 80" # auto-output, 80 MiB, 24 FPS, no scale, no trim
echo " MUTE_AUDIO=true $0 my_video.mp4" # Mute audio using MUTE_AUDIO variable
echo " AUDIO_BITRATE_KBPS=0 $0 my_video.mp4" # Mute audio by setting bitrate to 0
echo " TARGET_SPEED=0.5 START_TIME=\"10\" $0 my_video.mp4" # half speed from 10s to end
echo " START_TIME=\"10\" END_TIME=\"20\" $0 my_video.mp4 80 720" # trim, scale, and resize
echo ""
exit "$EXIT_MISSING_ARGS"
fi
# Shift off INPUT_FILE ($1) so subsequent arguments can be processed as $1, $2, etc.
shift
# Process remaining arguments ($1, $2, $3 now refer to original $2, $3, $4)
# Check if the current $1 (original $2) is a number. If so, it means OUTPUT_FILE was skipped.
if [[ -n "$1" && "$1" =~ ^[0-9]+$ ]]; then
# This argument is TARGET_SIZE_MIB (OUTPUT_FILE was omitted)
TARGET_SIZE_MIB="$1"
shift # Consume this argument
# Check if the next argument ($1, original $3) is TARGET_SMALLEST_DIMENSION
if [[ -n "$1" && "$1" =~ ^[0-9]+$ ]]; then
TARGET_SMALLEST_DIMENSION="$1"
shift # Consume this argument
fi
else
# This argument is not a number, so it must be OUTPUT_FILE (or no further args)
if [ -n "$1" ]; then
OUTPUT_FILE="$1"
shift # Consume this argument
# Check if the next argument ($1, original $3) is TARGET_SIZE_MIB
if [[ -n "$1" && "$1" =~ ^[0-9]+$ ]]; then
TARGET_SIZE_MIB="$1"
shift # Consume this argument
# Check if the next argument ($1, original $4) is TARGET_SMALLEST_DIMENSION
if [[ -n "$1" && "$1" =~ ^[0-9]+$ ]]; then
TARGET_SMALLEST_DIMENSION="$1"
shift # Consume this argument
fi
fi
fi
fi
# If OUTPUT_FILE is still empty after parsing (meaning it was never explicitly provided),
# derive it from the input filename.
if [ -z "$OUTPUT_FILE" ]; then
FILENAME_NO_EXT=$(basename "${INPUT_FILE%.*}")
OUTPUT_FILE="${FILENAME_NO_EXT}.webm"
fi
# Warn if any unexpected arguments remain
if [ -n "$1" ]; then
echo "Warning: Unrecognized arguments after parsing: $*" >&2 # Output warning to stderr
fi
# --- Check for overwrite before starting ---
INPUT_REALPATH=$(realpath -s "$INPUT_FILE")
OUTPUT_REALPATH=$(realpath -s "$OUTPUT_FILE")
if [ "$INPUT_REALPATH" = "$OUTPUT_REALPATH" ]; then
echo "Error: The output file path is the same as the input file path." >&2
echo "Please specify a different output filename or path to avoid overwriting the original file." >&2
exit "$EXIT_OVERWRITE_PREVENTED"
fi
# Save pass log file. Using basename to strip the last extension (.webm)
PASSLOGFILE="$(basename "$OUTPUT_FILE" .webm)_passlog"
# Create a temporary named pipe for progress reporting
PROGRESS_FIFO="$(mktemp -u --suffix=.fifo)"
mkfifo "$PROGRESS_FIFO"
if [ $? -ne 0 ]; then
echo "Error: Failed to create a named pipe for progress reporting." >&2
exit "$EXIT_NAMED_PIPE_FAILED"
fi
# Clean up the named pipe on exit
trap "rm -f \"$PROGRESS_FIFO\"" EXIT
# --- Main Logic ---
# Check if audio should be muted
IS_AUDIO_ENABLED="true"
if [[ -n "$MUTE_AUDIO" ]] || [[ "$AUDIO_BITRATE_KBPS" -eq 0 ]]; then
IS_AUDIO_ENABLED="false"
echo "Audio is being muted."
fi
echo "--- WebM Conversion Script ---"
echo "Script started at: ${SCRIPT_START_TIME_HUMAN}"
echo "Input File: ${INPUT_FILE}"
echo "Output File: ${OUTPUT_FILE}"
echo "Target Size: ${TARGET_SIZE_MIB} MiB (Default if not specified: ${DEFAULT_TARGET_SIZE_MIB} MiB)"
echo "Pass Log File: ${PASSLOGFILE}"
if [ -n "${TARGET_FPS}" ]; then
echo "Target FPS: ${TARGET_FPS}"
fi
if [ "$(echo "$TARGET_SPEED > 1.0" | bc -l)" -eq 1 ]; then
echo "Target Speed: ${TARGET_SPEED}x (faster)"
elif [ "$(echo "$TARGET_SPEED < 1.0" | bc -l)" -eq 1 ]; then
echo "Target Speed: ${TARGET_SPEED}x (slower)"
else
echo "Target Speed: ${TARGET_SPEED}x (normal)"
fi
echo "------------------------------"
# --- Determine Cropping Arguments ---
FFMPEG_CROP_ARGS=""
FFMPEG_POST_CROP_ARGS=""
# Check for cropping conditions (either START_TIME or END_TIME is set)
if [ -n "$START_TIME" ] || [ -n "$END_TIME" ]; then
if [ -n "$START_TIME" ]; then
FFMPEG_CROP_ARGS="-ss ${START_TIME}"
echo "Trimming video from start time: ${START_TIME}"
fi
if [ -n "$END_TIME" ]; then
# When using -to with -ss, it must come after the input file for fast seeking.
# So we store it in a separate variable to be placed later.
FFMPEG_POST_CROP_ARGS="-to ${END_TIME}"
echo "Trimming video to end time: ${END_TIME}"
fi
fi
# --- Get video duration and dimensions using ffprobe ---
# Get original video duration
ORIGINAL_DURATION_SECONDS=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE")
if [ -z "$ORIGINAL_DURATION_SECONDS" ]; then
echo "Error: Could not get duration from '$INPUT_FILE'." >&2
exit "$EXIT_FFPROBE_FAILED"
fi
# Get original video dimensions
ORIGINAL_WIDTH=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE")
ORIGINAL_HEIGHT=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE")
if [ -z "$ORIGINAL_WIDTH" ] || [ -z "$ORIGINAL_HEIGHT" ]; then
echo "Error: Could not get video dimensions from '$INPUT_FILE'." >&2
exit "$EXIT_FFMPEG_FAILED"
fi
# Now calculate the effective duration based on crop args
DURATION_SECONDS=0.0
if [ -n "$START_TIME" ] || [ -n "$END_TIME" ]; then
if [ -n "$START_TIME" ] && [ -n "$END_TIME" ]; then
# Both start and end time are set, calculate the difference
START_SECONDS=$(parse_time_to_seconds "$START_TIME")
END_SECONDS=$(parse_time_to_seconds "$END_TIME")
if [ -z "$START_SECONDS" ] || [ -z "$END_SECONDS" ]; then
echo "Error: Could not parse START_TIME or END_TIME. Please check the format." >&2
exit "$EXIT_TIME_PARSING_FAILED"
fi
DURATION_SECONDS=$(awk -v start="$START_SECONDS" -v end="$END_SECONDS" 'BEGIN { printf "%.0f", end - start }')
elif [ -n "$START_TIME" ]; then
# Only start time is set, subtract from original duration
START_SECONDS=$(parse_time_to_seconds "$START_TIME")
if [ -z "$START_SECONDS" ]; then
echo "Error: Could not parse START_TIME: '$START_TIME'." >&2
exit "$EXIT_TIME_PARSING_FAILED"
fi
DURATION_SECONDS=$(awk -v start="$START_SECONDS" -v original="$ORIGINAL_DURATION_SECONDS" 'BEGIN { printf "%.0f", original - start }')
elif [ -n "$END_TIME" ]; then
# Only end time is set, use that as the duration
END_SECONDS=$(parse_time_to_seconds "$END_TIME")
if [ -z "$END_SECONDS" ]; then
echo "Error: Could not parse END_TIME: '$END_SECONDS'." >&2
exit "$EXIT_TIME_PARSING_FAILED"
fi
DURATION_SECONDS=$(printf "%.0f" "$END_SECONDS")
fi
else
# No trimming, use the original duration
DURATION_SECONDS=$(printf "%.0f" "$ORIGINAL_DURATION_SECONDS")
fi
# Final check for duration after calculations
if [ "$DURATION_SECONDS" -le 0 ]; then
echo "Error: Calculated video duration is 0 or less. Please check your start and end times." >&2
exit "$EXIT_ZERO_DURATION"
fi
# Apply speed factor to the duration
if [ "$(echo "$TARGET_SPEED != 1.0" | bc -l)" -eq 1 ]; then
NEW_DURATION_SECONDS=$(awk -v dur="$DURATION_SECONDS" -v speed="$TARGET_SPEED" 'BEGIN { printf "%.0f", dur / speed }')
echo "Adjusting duration for speed factor: ${DURATION_SECONDS} seconds -> ${NEW_DURATION_SECONDS} seconds"
DURATION_SECONDS="$NEW_DURATION_SECONDS"
fi
echo "Video duration to be processed: ${DURATION_SECONDS} seconds"
echo "Original video dimensions: ${ORIGINAL_WIDTH}x${ORIGINAL_HEIGHT}"
# --- Determine Filters and build filter graphs ---
# Initialize the filter arrays
declare -a VIDEO_FILTERS=()
declare -a AUDIO_FILTERS=()
# The CORRECT order of operations for best quality:
# 1. Add FPS filter if specified (drops frames from the source)
if [[ -n "$TARGET_FPS" && "$TARGET_FPS" =~ ^[0-9]+$ ]]; then
VIDEO_FILTERS+=("fps=fps=${TARGET_FPS}")
echo "Applying target FPS: ${TARGET_FPS}"
else
echo "No target FPS applied."
fi
# 2. Add speed filter if not normal speed (setpts for video)
if [ "$(echo "$TARGET_SPEED != 1.0" | bc -l)" -eq 1 ]; then
# Add setpts filter for video
VIDEO_FILTERS+=("setpts=PTS/${TARGET_SPEED}")
# Add atempo filter for audio (may require chaining)
if [ "$IS_AUDIO_ENABLED" = "true" ]; then
current_speed=$(echo "$TARGET_SPEED" | bc -l)
while [ "$(echo "$current_speed > 2.0" | bc -l)" -eq 1 ]; do
AUDIO_FILTERS+=("atempo=2.0")
current_speed=$(echo "$current_speed / 2.0" | bc -l)
done
while [ "$(echo "$current_speed < 0.5" | bc -l)" -eq 1 ]; do
AUDIO_FILTERS+=("atempo=0.5")
current_speed=$(echo "$current_speed / 0.5" | bc -l)
done
if [ "$(echo "$current_speed != 1.0" | bc -l)" -eq 1 ]; then
AUDIO_FILTERS+=("atempo=${current_speed}")
fi
fi
echo "Applying speed filter: ${TARGET_SPEED}x"
else
echo "No speed filter applied."
fi
# 3. Add scaling filter if specified (applied to the already-filtered frames)
if [[ -n "$TARGET_SMALLEST_DIMENSION" && "$TARGET_SMALLEST_DIMENSION" =~ ^[0-9]+$ ]]; then
if [ "$ORIGINAL_WIDTH" -gt "$ORIGINAL_HEIGHT" ]; then
VIDEO_FILTERS+=("scale=-2:${TARGET_SMALLEST_DIMENSION}")
echo "Scaling landscape video: Smallest dimension (height) set to ${TARGET_SMALLEST_DIMENSION} pixels. Width will be calculated to maintain aspect ratio."
elif [ "$ORIGINAL_HEIGHT" -gt "$ORIGINAL_WIDTH" ]; then
VIDEO_FILTERS+=("scale=${TARGET_SMALLEST_DIMENSION}:-2")
echo "Scaling portrait video: Smallest dimension (width) set to ${TARGET_SMALLEST_DIMENSION} pixels. Height will be calculated to maintain aspect ratio."
else
VIDEO_FILTERS+=("scale=${TARGET_SMALLEST_DIMENSION}:${TARGET_SMALLEST_DIMENSION}")
echo "Scaling square video: Both dimensions set to ${TARGET_SMALLEST_DIMENSION} pixels."
fi
else
echo "No video scaling applied (or invalid target_smallest_dimension)."
fi
# Join filters into a single string for FFmpeg
VIDEO_FILTER_GRAPH=""
if [ ${#VIDEO_FILTERS[@]} -gt 0 ]; then
IFS=,
VIDEO_FILTER_GRAPH="-vf $(echo "${VIDEO_FILTERS[*]}")"
unset IFS
fi
AUDIO_FILTER_GRAPH=""
if [ ${#AUDIO_FILTERS[@]} -gt 0 ]; then
IFS=,
AUDIO_FILTER_GRAPH="-af $(echo "${AUDIO_FILTERS[*]}")"
unset IFS
fi
# --- 2. Calculate target total bitrate ---
# Convert target size from MiB to kilobits (1 MiB = 8192 kbits)
TARGET_SIZE_KBITS=$((TARGET_SIZE_MIB * 8 * 1024))
# Check for division by zero if duration is 0
if [ "$DURATION_SECONDS" -eq 0 ]; then
echo "Error: Video duration is 0 seconds. Cannot calculate bitrate. Input video may be corrupt or too short." >&2
exit "$EXIT_ZERO_DURATION"
fi
# Calculate audio bitrate if audio is enabled, otherwise set it to 0
if [ "$IS_AUDIO_ENABLED" = "true" ]; then
echo "Audio Bitrate: ${AUDIO_BITRATE_KBPS} kbps"
AUDIO_BITRATE_TO_SUBTRACT="${AUDIO_BITRATE_KBPS}"
else
echo "Audio Bitrate: (Muted)"
AUDIO_BITRATE_TO_SUBTRACT=0
fi
# Calculate total target bitrate, with a small buffer for overhead
TARGET_TOTAL_BITRATE_KBPS=$(( (TARGET_SIZE_KBITS / DURATION_SECONDS) * 95 / 100 ))
# --- 3. Calculate target video bitrate ---
TARGET_VIDEO_BITRATE_KBPS=$((TARGET_TOTAL_BITRATE_KBPS - AUDIO_BITRATE_TO_SUBTRACT))
# Ensure video bitrate is not negative or too low (e.g., minimum 50 kbps for video)
if [ "$TARGET_VIDEO_BITRATE_KBPS" -le 50 ]; then
echo "Warning: Calculated video bitrate (${TARGET_VIDEO_BITRATE_KBPS}kbps) is very low. This might severely impact quality. Setting minimum to 50kbps." >&2
TARGET_VIDEO_BITRATE_KBPS=50
fi
echo "Calculated total target bitrate: ${TARGET_TOTAL_BITRATE_KBPS} kbps"
echo "Calculated video bitrate: ${TARGET_VIDEO_BITRATE_KBPS} kbps"
echo "Starting FFmpeg two-pass conversion..."
# --- FFmpeg Pass 1 ---
echo "--- Starting FFmpeg Pass 1 ---"
PASS1_START_TIME=$(date +%s)
# Start progress monitor in the background
monitor_progress "Pass 1" "$PROGRESS_FIFO" "$DURATION_SECONDS" &
MONITOR_PID=$!
ffmpeg -hide_banner -v warning ${FFMPEG_CROP_ARGS} -i "$INPUT_FILE" ${FFMPEG_POST_CROP_ARGS} \
-g 240 -c:v libvpx-vp9 -quality best -b:v "${TARGET_VIDEO_BITRATE_KBPS}k" \
${VIDEO_FILTER_GRAPH} \
-pass 1 -passlogfile "$PASSLOGFILE" -an -f webm -y /dev/null \
-progress pipe:1 > "$PROGRESS_FIFO"
FFMPEG_PASS1_EXIT_CODE=$?
wait "$MONITOR_PID"
PASS1_END_TIME=$(date +%s)
PASS1_DURATION=$((PASS1_END_TIME - PASS1_START_TIME))
echo "--- FFmpeg Pass 1 completed in ${PASS1_DURATION} seconds ---"
if [ "$FFMPEG_PASS1_EXIT_CODE" -ne "$EXIT_SUCCESS" ]; then
echo "Error: FFmpeg first pass failed with exit code $FFMPEG_PASS1_EXIT_CODE." >&2
rm -f "${PASSLOGFILE}-0.log" "${PASSLOGFILE}-0.log.temp"
exit "$EXIT_FFMPEG_FAILED"
fi
# --- FFmpeg Pass 2 ---
echo "--- Starting FFmpeg Pass 2 ---"
PASS2_START_TIME=$(date +%s)
# Build audio options for the second pass
if [ "$IS_AUDIO_ENABLED" = "true" ]; then
# The fix is here: removing the quotes around the entire `-b:a ...` part
# so that the shell correctly interprets the variable and the 'k' suffix.
FFMPEG_AUDIO_OPTIONS="${AUDIO_FILTER_GRAPH} -c:a libopus -b:a ${AUDIO_BITRATE_KBPS}k"
else
FFMPEG_AUDIO_OPTIONS="-an"
fi
# Start progress monitor for pass 2
monitor_progress "Pass 2" "$PROGRESS_FIFO" "$DURATION_SECONDS" &
MONITOR_PID=$!
ffmpeg -hide_banner -v warning ${FFMPEG_CROP_ARGS} -i "$INPUT_FILE" ${FFMPEG_POST_CROP_ARGS} \
-g 240 -c:v libvpx-vp9 -quality best -b:v "${TARGET_VIDEO_BITRATE_KBPS}k" \
${VIDEO_FILTER_GRAPH} \
-pass 2 -passlogfile "$PASSLOGFILE" \
${FFMPEG_AUDIO_OPTIONS} \
-y "$OUTPUT_FILE" \
-progress pipe:1 > "$PROGRESS_FIFO"
FFMPEG_PASS2_EXIT_CODE=$?
wait "$MONITOR_PID"
PASS2_END_TIME=$(date +%s)
PASS2_DURATION=$((PASS2_END_TIME - PASS2_START_TIME))
echo "--- FFmpeg Pass 2 completed in ${PASS2_DURATION} seconds ---"
# Clean up pass log files
rm -f "${PASSLOGFILE}-0.log" "${PASSLOGFILE}-0.log.temp"
if [ "$FFMPEG_PASS2_EXIT_CODE" -ne "$EXIT_SUCCESS" ]; then
echo "Error: FFmpeg second pass failed with exit code $FFMPEG_PASS2_EXIT_CODE." >&2
exit "$EXIT_FFMPEG_FAILED"
fi
echo "Conversion complete! Output file: $OUTPUT_FILE"
echo "Final size ( check with 'ls -lh \"$OUTPUT_FILE\"' ): "
ls -lh "$OUTPUT_FILE"
SCRIPT_END_TIME=$(date +%s)
TOTAL_DURATION=$((SCRIPT_END_TIME - SCRIPT_START_TIME))
echo "--- Total script execution time: ${TOTAL_DURATION} seconds ---"
exit "$EXIT_SUCCESS"
@gphg
Copy link
Author

gphg commented Aug 21, 2025

Known issue: If input_video is a WebM video and in current working directory and output_video.webm is omitted, then output video overwrites input_video; these have the same path. This can be avoided by set the output video into another name (.webm extension must be included) or different path than input video. In future, this will be fixed by comparing the realpath of input video and final output video. If both are the same, then error exit.

It has been fixed.

Also you'll need bc, ffmpeg, ffprobe to be installed. This can be easily be done by package manager if you are on a Linux distro. On Windows, you'll have to get these and get the binaries located by %PATH% environment variable. The bash and POSIX related on Windows are available under MinGW, MSYS2, Git for Windows (slim MSYS2), or WSL.

Windows has it own package manager, I never use it thought. Alternatively, choco (system-wide: requires administrator level to install things) OR scoop (user-wide: requires git to be installed before-hand to get it working). I personally recommend scoop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment