ergosteur

ergosteur / Encode 4:3 video

Last active 1 month ago

Like 0

Batch encode videos to NTSC 4:3 with hardware accel (multi-OS)

encode-targetsize-43-crop.sh Raw
1#!/usr/bin/env bash
2#
3# encode-targetsize-43-crop.sh - Batch encode videos to NTSC 4:3 with hardware accel (multi-OS)
4# Targeting iGPUs (Apple Silicon, Intel HD, Intel Arc)
5# Uses VideoToolbox on macOS, VAAPI on Linux, and QuickSync on Windows.
6#
7set -euo pipefail
8
9# --- Default Settings ---
10VIDEO_BITRATE="1500k"
11AUDIO_BITRATE="128k"
12TARGET_SIZE=""
13KEEP_LOGS=0
14VIDEO_BITRATE_MANUALLY_SET=0
15
16# --- Global Counters ---
17encoded_count=0
18skipped_count=0
19failed_count=0
20
21# --- Functions ---
22
23usage() {
24 echo "Usage: $0 [options] <input_directory> <output_directory>"
25 echo "Batch encodes video files to NTSC 4:3 MP4."
26 echo
27 echo "Options:"
28 echo " --target-size <size> Set a target file size (e.g., 250MB, 700MiB). Overrides --video-bitrate."
29 echo " --video-bitrate <rate> Set video bitrate (e.g., 2000k). Default: $VIDEO_BITRATE"
30 echo " --audio-bitrate <rate> Set audio bitrate (e.g., 192k). Default: $AUDIO_BITRATE"
31 echo " --keep-logs Do not delete log files on successful encodes."
32 echo " -h, --help Show this help message."
33 exit 1
34}
35
36check_deps() {
37 for cmd in ffmpeg ffprobe awk tee bc; do
38 if ! command -v "$cmd" &> /dev/null; then
39 echo "❌ Error: Required command '$cmd' is not installed." >&2
40 exit 1
41 fi
42 done
43}
44
45# Validate bitrate format (must be ###k)
46validate_bitrate() {
47 local rate="$1"
48 if [[ ! "$rate" =~ ^[0-9]+k$ ]]; then
49 echo "❌ Error: Bitrate must be specified in kilobits with a trailing 'k' (e.g., 128k, 2000k)." >&2
50 exit 1
51 fi
52}
53
54# Converts a size string (e.g., 700MB, 700MiB) to total kilobits.
55# Requires explicit MB or MiB suffix.
56parse_size_to_kbits() {
57 local size_str="$1"
58 echo "$size_str" | awk '
59 /MiB$/ {
60 sub(/MiB$/, "", $1)
61 val=$1 * 1024 * 1024 # bytes
62 print int((val * 8) / 1000) # kilobits
63 next
64 }
65 /MB$/ {
66 sub(/MB$/, "", $1)
67 val=$1 * 1024 * 1024 # bytes (treat MB as MiB for consistency)
68 print int((val * 8) / 1000) # kilobits
69 next
70 }
71 {
72 print "Error: Target size must end with MB or MiB (case-sensitive)." > "/dev/stderr"
73 exit 1
74 }
75 '
76}
77
78process_file() {
79 local in_file="$1"
80 local rel_path="${in_file#$INPUT_DIR/}"
81 local out_file="$OUTPUT_DIR/${rel_path%.*}.ntsc43.mp4"
82 local log_file="$OUTPUT_DIR/${rel_path%.*}.ntsc43.log"
83
84 mkdir -p "$(dirname "$out_file")"
85 echo "▶ Encoding: $rel_path"
86
87 local current_video_bitrate="$VIDEO_BITRATE"
88
89 # If target size is set, calculate the required video bitrate
90 if [[ -n "$TARGET_SIZE" ]]; then
91 local duration
92 duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$in_file")
93 if [[ -z "$duration" || $(echo "$duration <= 0" | bc -l) -eq 1 ]]; then
94 echo "❌ FAILED: Could not determine duration of $rel_path. Cannot calculate target bitrate."
95 failed_count=$((failed_count + 1))
96 return
97 fi
98
99 local target_kbits audio_kbits
100 target_kbits=$(parse_size_to_kbits "$TARGET_SIZE")
101 audio_kbits=$(echo "$AUDIO_BITRATE" | tr -d 'k')
102
103 # video_bitrate = (total_kbits / duration_sec) - audio_bitrate
104 local calculated_vbitrate
105 calculated_vbitrate=$(echo "scale=2; ($target_kbits / $duration) - $audio_kbits" | bc)
106
107 if [[ "${calculated_vbitrate%.*}" -lt 100 ]]; then
108 echo "⚠️ Warning: Calculated bitrate ($calculated_vbitrate) is very low for $rel_path. Clamping to 100k."
109 calculated_vbitrate=100
110 fi
111
112 current_video_bitrate="${calculated_vbitrate}k"
113 echo " Target Size: $TARGET_SIZE -> Calculated Video Bitrate: $current_video_bitrate"
114 fi
115
116 # Skip logic AFTER validation
117 if [[ -f "$out_file" ]]; then
118 echo "⏭ Skipping (already exists): $rel_path"
119 skipped_count=$((skipped_count + 1))
120 return
121 fi
122
123 # Get dimensions and determine if we need to crop in one go
124 local is_widescreen
125 is_widescreen=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height \
126 -of csv=p=0:s=, "$in_file" | awk -F, '{w=$1; h=$2; print (w/h > 1.34) ? 1 : 0}')
127
128 local vfilt
129 if [[ "$is_widescreen" -eq 1 ]]; then
130 vfilt="crop=in_h*4/3:in_h"
131 else
132 vfilt="scale=in_w:in_h"
133 fi
134
135 (set -o pipefail && ffmpeg -y -nostdin \
136 $HWACCEL \
137 -i "$in_file" \
138 -vf "$vfilt,fps=30000/1001,format=yuv420p,scale=720:480,setsar=8/9" \
139 -c:v "$VIDEO_CODEC" -b:v "$current_video_bitrate" \
140 -c:a aac -b:a "$AUDIO_BITRATE" -ac 2 -ar 48000 \
141 -af "loudnorm=I=-16:TP=-1.5:LRA=11" \
142 "$out_file" 2>&1 | tee "$log_file")
143
144 local ffmpeg_exit_code=$?
145 if [ $ffmpeg_exit_code -ne 0 ]; then
146 echo "❌ FAILED: $rel_path (ffmpeg exited with code $ffmpeg_exit_code). See log for details: $log_file"
147 failed_count=$((failed_count + 1))
148 rm -f "$out_file"
149 return
150 fi
151
152 echo "✅ Finished: $rel_path"
153 encoded_count=$((encoded_count + 1))
154
155 if [[ "$KEEP_LOGS" -eq 0 ]]; then
156 rm -f "$log_file"
157 fi
158}
159
160# --- Main Script ---
161
162# 1. Parse Command-Line Arguments
163while [[ $# -gt 0 ]]; do
164 key="$1"
165 case $key in
166 --target-size)
167 TARGET_SIZE="$2"
168 shift 2
169 ;;
170 --video-bitrate)
171 VIDEO_BITRATE="$2"
172 VIDEO_BITRATE_MANUALLY_SET=1
173 shift 2
174 ;;
175 --audio-bitrate)
176 AUDIO_BITRATE="$2"
177 shift 2
178 ;;
179 --keep-logs)
180 KEEP_LOGS=1
181 shift
182 ;;
183 -h|--help)
184 usage
185 ;;
186 -* )
187 echo "Unknown option: $1"
188 usage
189 ;;
190 *)
191 break
192 ;;
193 esac
194done
195
196# 2. Validate Dependencies & Positional Arguments
197check_deps
198[ "$#" -ne 2 ] && usage
199INPUT_DIR=$(realpath "$1")
200OUTPUT_DIR=$(realpath "$2")
201
202if [ ! -d "$INPUT_DIR" ]; then
203 echo "❌ Error: Input directory not found: $1" >&2
204 exit 1
205fi
206
207# Enforce bitrate formats before processing
208validate_bitrate "$AUDIO_BITRATE"
209validate_bitrate "$VIDEO_BITRATE"
210
211if [[ -n "$TARGET_SIZE" && "$VIDEO_BITRATE_MANUALLY_SET" -eq 1 ]]; then
212 echo "⚠️ Warning: Both --target-size and --video-bitrate were specified."
213 echo " The --target-size option will take precedence and calculate the video bitrate automatically."
214fi
215
216mkdir -p "$OUTPUT_DIR"
217
218# 3. Set up OS-specific FFmpeg parameters
219OS="$(uname -s)"
220case "$OS" in
221 Linux*) VIDEO_CODEC="h264_vaapi"; HWACCEL="-hwaccel vaapi -vaapi_device /dev/dri/renderD128" ;;
222 Darwin*) VIDEO_CODEC="h264_videotoolbox"; HWACCEL="-hwaccel videotoolbox" ;;
223 MINGW*|MSYS*|CYGWIN*) VIDEO_CODEC="h264_qsv"; HWACCEL="-hwaccel qsv" ;;
224 *) echo "❌ Unsupported OS: $OS"; exit 1 ;;
225esac
226
227echo "🚀 Starting batch encode..."
228echo " Input: $INPUT_DIR"
229echo " Output: $OUTPUT_DIR"
230echo " OS: $OS ($VIDEO_CODEC)"
231if [[ -n "$TARGET_SIZE" ]]; then
232echo " Target Size: $TARGET_SIZE (video bitrate will be calculated per file)"
233else
234echo " Video Bitrate: $VIDEO_BITRATE"
235fi
236echo " Audio Bitrate: $AUDIO_BITRATE"
237echo " Keep Logs: $(if [ $KEEP_LOGS -eq 1 ]; then echo 'Yes'; else echo 'No'; fi)"
238echo "--------------------------------------------------"
239
240# 4. Find and process video files sequentially
241while IFS= read -r -d '' file; do
242 process_file "$file"
243 echo "--------------------------------------------------"
244done < <(find "$INPUT_DIR" -type f \
245 \( -iname '*.mp4' -o -iname '*.mov' -o -iname '*.mkv' -o -iname '*.avi' -o -iname '*.ts' -o -iname '*.m4v' \) \
246 -print0)
247
248echo "🎉 All done! Encoded files are in: $OUTPUT_DIR"
249echo " Encoded: $encoded_count"
250echo " Skipped: $skipped_count"
251echo " Failed: $failed_count"
252