Last active
September 15, 2025 05:26
-
-
Save gphg/b1b0dc152bf60a606afd6dbf55c33319 to your computer and use it in GitHub Desktop.
A handy script for sharing meme videos across the internet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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. Thebash
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) ORscoop
(user-wide: requiresgit
to be installed before-hand to get it working). I personally recommendscoop
.