mirror of
https://gitlab.com/dabruh/dotfiles.git
synced 2025-12-07 10:46:43 +01:00
234 lines
7.8 KiB
Bash
Executable file
234 lines
7.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Video encoding script using ffmpeg with H.264 and scaling to FHD.
|
|
|
|
set -euo pipefail
|
|
|
|
# Colors for log output
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
RED='\033[0;31m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
# Logging functions for user-friendly output
|
|
log_info() { echo -e "${BLUE}INFO:${NC} $1"; }
|
|
log_success() { echo -e "${GREEN}SUCCESS:${NC} $1"; }
|
|
log_warn() { echo -e "${YELLOW}WARN:${NC} $1"; }
|
|
log_error() { echo -e "${RED}ERROR:${NC} $1" >&2; }
|
|
|
|
# Gets the duration of a video file in seconds using ffprobe.
|
|
get_duration() {
|
|
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$1" 2>/dev/null || echo "0"
|
|
}
|
|
|
|
# Default encoding parameters tuned for quality and compatibility.
|
|
readonly DEFAULT_CRF="23"
|
|
readonly DEFAULT_PRESET="medium"
|
|
readonly DEFAULT_MAXRATE="6M"
|
|
readonly DEFAULT_BUFSIZE="12M"
|
|
readonly DEFAULT_MAX_W="1920"
|
|
readonly DEFAULT_MAX_H="1080"
|
|
|
|
# Global state variables modified during argument parsing and validation.
|
|
INFILE=""
|
|
OUTFILE=""
|
|
CRF="${DEFAULT_CRF}"
|
|
PRESET="${DEFAULT_PRESET}"
|
|
MAXRATE="${DEFAULT_MAXRATE}"
|
|
BUFSIZE="${DEFAULT_BUFSIZE}"
|
|
EXTRA_ARGS=()
|
|
MAX_W="${DEFAULT_MAX_W}"
|
|
MAX_H="${DEFAULT_MAX_H}"
|
|
OVERWRITE_SAME=false
|
|
OVERWRITE_DIFFERENT=false
|
|
IS_BATCH=false
|
|
INPUT_DIR=""
|
|
INPUT_FILES=()
|
|
|
|
# Prints usage information to stdout.
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") -i input.mp4 [options]
|
|
|
|
Options:
|
|
-i, --input Input file or directory (required)
|
|
-o, --output Output file or directory (default: <input>_videncode.mp4 or original name if different dir)
|
|
-c, --crf CRF value (default: ${DEFAULT_CRF})
|
|
-p, --preset x264 preset (default: ${DEFAULT_PRESET})
|
|
-m, --maxrate Maxrate constraint (default: ${DEFAULT_MAXRATE})
|
|
-b, --bufsize Bufsize constraint (default: ${DEFAULT_BUFSIZE})
|
|
--extra Extra ffmpeg args (wrap in quotes). May be specified multiple times.
|
|
--max-w Maximum width in pixels (default: ${DEFAULT_MAX_W})
|
|
--max-h Maximum height in pixels (default: ${DEFAULT_MAX_H})
|
|
--overwrite-same Overwrite output even if duration matches input
|
|
--overwrite-different Overwrite output even if duration differs from input
|
|
-h, --help Show this help
|
|
EOF
|
|
}
|
|
|
|
# Parses command-line arguments, supporting both short and long options.
|
|
parse_args() {
|
|
if [[ $# -eq 0 ]]; then
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-i|--input) INFILE="$2"; shift 2 ;;
|
|
-o|--output) OUTFILE="$2"; shift 2 ;;
|
|
-c|--crf) CRF="$2"; shift 2 ;;
|
|
-p|--preset) PRESET="$2"; shift 2 ;;
|
|
-m|--maxrate) MAXRATE="$2"; shift 2 ;;
|
|
-b|--bufsize) BUFSIZE="$2"; shift 2 ;;
|
|
--extra) EXTRA_ARGS+=("$2"); shift 2 ;;
|
|
--max-w) MAX_W="$2"; shift 2 ;;
|
|
--max-h) MAX_H="$2"; shift 2 ;;
|
|
--overwrite-same) OVERWRITE_SAME=true; shift ;;
|
|
--overwrite-different) OVERWRITE_DIFFERENT=true; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
--) shift; break ;;
|
|
*) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Validates input and determines output path based on rules to avoid overwriting.
|
|
validate_input() {
|
|
if [[ -z "${INFILE:-}" ]]; then
|
|
log_error "input (-i/--input) is required."
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -d "$INFILE" ]]; then
|
|
IS_BATCH=true
|
|
INPUT_DIR="$INFILE"
|
|
log_info "Processing directory: $INFILE"
|
|
# Find video files in the directory
|
|
while IFS= read -r -d '' file; do
|
|
INPUT_FILES+=("$file")
|
|
done < <(find "$INPUT_DIR" -type f \( -iname "*.mp4" -o -iname "*.avi" -o -iname "*.mkv" -o -iname "*.mov" -o -iname "*.flv" -o -iname "*.wmv" -o -iname "*.webm" -o -iname "*.m4v" \) -print0)
|
|
if [[ ${#INPUT_FILES[@]} -eq 0 ]]; then
|
|
log_error "no video files found in directory: $INFILE"
|
|
exit 1
|
|
fi
|
|
log_info "Found ${#INPUT_FILES[@]} video file(s) to process"
|
|
elif [[ -f "$INFILE" ]]; then
|
|
IS_BATCH=false
|
|
INPUT_FILES=("$INFILE")
|
|
log_info "Processing single file: $INFILE"
|
|
else
|
|
log_error "input not found or not a file/directory: $INFILE"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Validates single input file and determines output path.
|
|
validate_input_single() {
|
|
# Determine output path: use _videncode.mp4 suffix in same dir, original name in different dir.
|
|
if [[ -n "$OUTFILE" && -d "$OUTFILE" ]]; then
|
|
if [[ "$OUTFILE" == "$INPUT_DIR" ]]; then
|
|
OUTFILE="${OUTFILE%/}/$(basename "${INFILE%.*}")_videncode.mp4"
|
|
else
|
|
OUTFILE="${OUTFILE%/}/$(basename "$INFILE")"
|
|
fi
|
|
elif [[ -z "$OUTFILE" ]]; then
|
|
OUTFILE="${INFILE%.*}_videncode.mp4"
|
|
fi
|
|
|
|
log_info "Output will be: $OUTFILE"
|
|
|
|
# Prevent accidental overwriting of the input file.
|
|
if [[ "$OUTFILE" == "$INFILE" ]]; then
|
|
log_error "output file would overwrite input file."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Assembles the full ffmpeg command array with all configured options.
|
|
build_ffmpeg_cmd() {
|
|
# Sets audio flags to copy streams unchanged for fidelity.
|
|
local audio_flags=("-c:a" "copy")
|
|
|
|
# Constructs the ffmpeg video scaling filter to fit within max dimensions while preserving aspect ratio.
|
|
# Scale logic: for landscape (w>h), constrain width to MAX_W and height to MAX_H;
|
|
# for portrait (h>w), constrain width to MAX_H and height to MAX_W.
|
|
# Commas are escaped for safe shell passing; force_original_aspect_ratio=decrease avoids upscaling.
|
|
local vf="scale='if(gt(iw\\,ih)\\,min(iw\\,${MAX_W})\\,min(iw\\,${MAX_H}))':'if(gt(iw\\,ih)\\,min(ih\\,${MAX_H})\\,min(ih\\,${MAX_W}))':force_original_aspect_ratio=decrease"
|
|
|
|
CMD=(ffmpeg -y -i "$INFILE" -map_metadata 0 -c:v libx264 -preset "$PRESET" -crf "$CRF" -maxrate "$MAXRATE" -bufsize "$BUFSIZE" -vf "$vf" "${audio_flags[@]}" -movflags +faststart)
|
|
|
|
# Safely append user-provided extra arguments by splitting on whitespace.
|
|
if [[ ${#EXTRA_ARGS[@]} -gt 0 ]]; then
|
|
for ea in "${EXTRA_ARGS[@]}"; do
|
|
read -r -a split <<< "$ea"
|
|
CMD+=("${split[@]}")
|
|
done
|
|
fi
|
|
|
|
CMD+=("$OUTFILE")
|
|
}
|
|
|
|
# Executes the constructed ffmpeg command after displaying it for transparency.
|
|
run_ffmpeg() {
|
|
log_info "Processing: $INFILE -> $OUTFILE"
|
|
if [[ -f "$OUTFILE" ]]; then
|
|
input_duration=$(get_duration "$INFILE")
|
|
output_duration=$(get_duration "$OUTFILE")
|
|
if [[ "$input_duration" == "$output_duration" && "$output_duration" != "0" ]]; then
|
|
if [[ "$OVERWRITE_SAME" == false ]]; then
|
|
log_warn "Skipping existing file with matching duration: $OUTFILE"
|
|
return
|
|
fi
|
|
elif [[ "$IS_BATCH" == false ]]; then
|
|
if [[ "$OVERWRITE_DIFFERENT" == false ]]; then
|
|
read -p "Output file exists with different duration, overwrite? (y/N) " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
log_warn "User chose not to overwrite: $OUTFILE"
|
|
return
|
|
fi
|
|
fi
|
|
else
|
|
if [[ "$OVERWRITE_DIFFERENT" == false ]]; then
|
|
log_warn "Skipping existing file with different duration: $OUTFILE"
|
|
return
|
|
fi
|
|
fi
|
|
fi
|
|
log_info "Starting encoding..."
|
|
log_info "Command:"
|
|
printf ' %q' "${CMD[@]}"
|
|
echo
|
|
"${CMD[@]}"
|
|
log_success "Encoding completed: $OUTFILE"
|
|
}
|
|
|
|
# Main entry point: orchestrates parsing, validation, command building, and execution.
|
|
main() {
|
|
parse_args "$@"
|
|
validate_input
|
|
if [[ "$IS_BATCH" == true ]]; then
|
|
# Save the output directory for batch processing
|
|
local OUTPUT_DIR="$OUTFILE"
|
|
local total=${#INPUT_FILES[@]}
|
|
local count=1
|
|
for file in "${INPUT_FILES[@]}"; do
|
|
log_info "Processing file $count of $total: $(basename "$file")"
|
|
INFILE="$file"
|
|
OUTFILE="$OUTPUT_DIR" # Reset OUTFILE for each iteration
|
|
validate_input_single
|
|
build_ffmpeg_cmd
|
|
run_ffmpeg
|
|
((count++))
|
|
done
|
|
log_success "Batch processing completed: $total file(s) processed"
|
|
else
|
|
validate_input_single
|
|
build_ffmpeg_cmd
|
|
run_ffmpeg
|
|
fi
|
|
}
|
|
|
|
main "$@"
|