| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | ########################################################################## |
| 4 | # |
| 5 | # This bash script was written by ChatGPT, but not this comment section. |
| 6 | # Everything works after testing, but not 2 functions: |
| 7 | # - Adding metadata to compressed MP4 file. |
| 8 | # - Checking if the file has already been compressed by this script. |
| 9 | # |
| 10 | ########################################################################## |
| 11 | |
| 12 | set -u |
| 13 | export LC_NUMERIC=C |
| 14 | |
| 15 | SCRIPT_NAME="$(basename "$0")" |
| 16 | UNIQUE_PREFIX="r-" |
| 17 | |
| 18 | ######################################## |
| 19 | # Encoding presets overview: |
| 20 | # |
| 21 | # none : balanced quality, AAC 96k |
| 22 | # vlog : spoken voice / crowd, copy audio |
| 23 | # tiny : max compression, AAC 64k |
| 24 | # |
| 25 | # Resolutions: 720p | 1080p |
| 26 | ######################################## |
| 27 | |
| 28 | show_help() { |
| 29 | cat <<EOF |
| 30 | Usage: |
| 31 | $SCRIPT_NAME <file> |
| 32 | $SCRIPT_NAME [none|vlog|tiny] [720p|1080p] <file> |
| 33 | $SCRIPT_NAME batch |
| 34 | |
| 35 | Batch: |
| 36 | Compresses all MP4 + MKV files using: |
| 37 | mode=none, resolution=1080p |
| 38 | |
| 39 | Defaults: |
| 40 | mode=none |
| 41 | resolution=720p |
| 42 | |
| 43 | EOF |
| 44 | exit 0 |
| 45 | } |
| 46 | |
| 47 | [[ "${1:-}" == "--help" ]] && show_help |
| 48 | |
| 49 | MODE="none" |
| 50 | RES="720p" |
| 51 | |
| 52 | # --- Batch mode --- |
| 53 | if [[ "${1:-}" == "batch" ]]; then |
| 54 | MODE="none" |
| 55 | RES="1080p" |
| 56 | shift || true |
| 57 | |
| 58 | shopt -s nullglob |
| 59 | FILES=( *.mp4 *.MP4 *.mkv ) |
| 60 | shopt -u nullglob |
| 61 | |
| 62 | if [[ ${#FILES[@]} -eq 0 ]]; then |
| 63 | echo "No MP4 or MKV files found." |
| 64 | exit 0 |
| 65 | fi |
| 66 | |
| 67 | for f in "${FILES[@]}"; do |
| 68 | "$0" "$MODE" "$RES" "$f" |
| 69 | done |
| 70 | exit 0 |
| 71 | fi |
| 72 | |
| 73 | # --- Parse arguments --- |
| 74 | if [[ $# -eq 1 ]]; then |
| 75 | INPUT="$1" |
| 76 | elif [[ $# -eq 2 ]]; then |
| 77 | MODE="$1" |
| 78 | INPUT="$2" |
| 79 | elif [[ $# -eq 3 ]]; then |
| 80 | MODE="$1" |
| 81 | RES="$2" |
| 82 | INPUT="$3" |
| 83 | else |
| 84 | show_help |
| 85 | fi |
| 86 | |
| 87 | [[ ! -f "$INPUT" ]] && { echo "File not found: $INPUT"; exit 1; } |
| 88 | |
| 89 | BASENAME="${INPUT%.*}" |
| 90 | OUTPUT="${BASENAME}-r.mp4" |
| 91 | SUMMARY="${BASENAME}.json" |
| 92 | |
| 93 | ######################################## |
| 94 | # Resolution |
| 95 | ######################################## |
| 96 | case "$RES" in |
| 97 | 720p) SCALE="scale=-2:720" ;; |
| 98 | 1080p) SCALE="scale=-2:1080" ;; |
| 99 | *) echo "Invalid resolution"; exit 1 ;; |
| 100 | esac |
| 101 | |
| 102 | ######################################## |
| 103 | # Mode settings |
| 104 | ######################################## |
| 105 | CQ=23 |
| 106 | AUDIO_OPTS=("-c:a" "aac" "-b:a" "96k") |
| 107 | |
| 108 | case "$MODE" in |
| 109 | vlog) |
| 110 | AUDIO_OPTS=("-c:a" "copy") |
| 111 | CQ=23 |
| 112 | ;; |
| 113 | tiny) |
| 114 | AUDIO_OPTS=("-c:a" "aac" "-b:a" "64k") |
| 115 | CQ=28 |
| 116 | ;; |
| 117 | none) ;; |
| 118 | *) |
| 119 | echo "Invalid mode" |
| 120 | exit 1 |
| 121 | ;; |
| 122 | esac |
| 123 | |
| 124 | ######################################## |
| 125 | # Metadata detection (robust) |
| 126 | ######################################## |
| 127 | EXISTING_ID="$(ffprobe -v error \ |
| 128 | -show_entries format_tags=compressed_by \ |
| 129 | -of default=nk=1:nw=1 "$INPUT" 2>/dev/null || true)" |
| 130 | |
| 131 | if [[ "$EXISTING_ID" =~ ^${UNIQUE_PREFIX}[A-Za-z0-9]+$ ]]; then |
| 132 | echo "Skipping already-compressed file: $INPUT" |
| 133 | exit 0 |
| 134 | fi |
| 135 | |
| 136 | ######################################## |
| 137 | # Generate unique ID |
| 138 | ######################################## |
| 139 | COMPRESS_ID="${UNIQUE_PREFIX}$(tr -dc A-Za-z0-9 </dev/urandom | head -c 12)" |
| 140 | |
| 141 | ######################################## |
| 142 | # Encode |
| 143 | ######################################## |
| 144 | echo |
| 145 | echo "Compressing: $INPUT > $OUTPUT" |
| 146 | |
| 147 | DURATION=$(ffprobe -v error -show_entries format=duration \ |
| 148 | -of default=nk=1:nw=1 "$INPUT") |
| 149 | |
| 150 | START_TIME=$(date +%s) |
| 151 | |
| 152 | ffmpeg -y -hide_banner -loglevel error \ |
| 153 | -i "$INPUT" \ |
| 154 | -map 0:v:0 -map 0:a? \ |
| 155 | -vf "$SCALE" \ |
| 156 | -c:v hevc_nvenc \ |
| 157 | -preset p6 -tune hq \ |
| 158 | -rc vbr -multipass fullres \ |
| 159 | -profile:v main -pix_fmt yuv420p \ |
| 160 | -spatial_aq 1 -temporal_aq 1 -aq-strength 8 \ |
| 161 | -cq "$CQ" \ |
| 162 | "${AUDIO_OPTS[@]}" \ |
| 163 | -movflags +faststart \ |
| 164 | -metadata compressed_by="$COMPRESS_ID" \ |
| 165 | -progress pipe:1 \ |
| 166 | "$OUTPUT" 2>/dev/null | while IFS='=' read -r key value; do |
| 167 | |
| 168 | if [[ "$key" == "out_time_ms" ]]; then |
| 169 | CURRENT_SEC=$(awk "BEGIN { print $value / 1000000 }") |
| 170 | |
| 171 | PERCENT=$(awk -v c="$CURRENT_SEC" -v d="$DURATION" \ |
| 172 | 'BEGIN { if (d>0) printf "%.1f", (c/d)*100; else print 0 }') |
| 173 | |
| 174 | NOW=$(date +%s) |
| 175 | ELAPSED_SEC=$(( NOW - START_TIME )) |
| 176 | |
| 177 | ETA_SEC=$(awk -v e="$ELAPSED_SEC" -v p="$PERCENT" ' |
| 178 | BEGIN { |
| 179 | if (p>0) printf "%.0f", e*(100-p)/p; |
| 180 | else print 0 |
| 181 | }') |
| 182 | |
| 183 | printf "\rProgress: %5.1f%% | Elapsed: %02d:%02d | ETA: %02d:%02d" \ |
| 184 | "$PERCENT" \ |
| 185 | $((ELAPSED_SEC/60)) $((ELAPSED_SEC%60)) \ |
| 186 | $((ETA_SEC/60)) $((ETA_SEC%60)) |
| 187 | fi |
| 188 | |
| 189 | done |
| 190 | |
| 191 | ######################################## |
| 192 | # Size check |
| 193 | ######################################## |
| 194 | ORIG_SIZE=$(stat -c%s "$INPUT") |
| 195 | NEW_SIZE=$(stat -c%s "$OUTPUT") |
| 196 | |
| 197 | if (( NEW_SIZE >= ORIG_SIZE )); then |
| 198 | echo |
| 199 | echo "Result larger than original - skipping" |
| 200 | rm -f "$OUTPUT" |
| 201 | exit 0 |
| 202 | fi |
| 203 | |
| 204 | ######################################## |
| 205 | # JSON summary |
| 206 | ######################################## |
| 207 | HUMAN_ORIG=$(numfmt --to=iec --suffix=B "$ORIG_SIZE") |
| 208 | HUMAN_NEW=$(numfmt --to=iec --suffix=B "$NEW_SIZE") |
| 209 | OCCURRED=$(date +"%Y-%m-%d %H:%M:%S") |
| 210 | |
| 211 | ######################################## |
| 212 | # Safe reduction percent calculation |
| 213 | ######################################## |
| 214 | if [[ -z "$ORIG_SIZE" ]] || (( ORIG_SIZE == 0 )); then |
| 215 | REDUCTION_PERCENT=0 |
| 216 | else |
| 217 | REDUCTION_PERCENT=$(awk "BEGIN { pct=(1-$NEW_SIZE/$ORIG_SIZE)*100; if(pct<0) pct=0; printf \"%.2f\", pct }") |
| 218 | fi |
| 219 | |
| 220 | cat > "$SUMMARY" <<EOF |
| 221 | { |
| 222 | "input": "$(basename "$INPUT")", |
| 223 | "output": "$(basename "$OUTPUT")", |
| 224 | |
| 225 | "original_bytes": $ORIG_SIZE, |
| 226 | "compressed_bytes": $NEW_SIZE, |
| 227 | |
| 228 | "original": "$HUMAN_ORIG", |
| 229 | "compressed": "$HUMAN_NEW", |
| 230 | |
| 231 | "reduction_percent": $REDUCTION_PERCENT, |
| 232 | "occurred": "$OCCURRED", |
| 233 | |
| 234 | "compression_id": "$COMPRESS_ID", |
| 235 | |
| 236 | "settings": { |
| 237 | "codec": "hevc_nvenc", |
| 238 | "resolution": "$RES", |
| 239 | "mode": "$MODE", |
| 240 | "cq": $CQ, |
| 241 | "scale_filter": "$SCALE", |
| 242 | "audio": "${AUDIO_OPTS[*]}", |
| 243 | "preset": "p6", |
| 244 | "rate_control": "vbr", |
| 245 | "multipass": "fullres", |
| 246 | "pixel_format": "yuv420p", |
| 247 | "aq": { |
| 248 | "spatial": 1, |
| 249 | "temporal": 1, |
| 250 | "strength": 8 |
| 251 | } |
| 252 | } |
| 253 | } |
| 254 | EOF |
| 255 | |
| 256 | REDUCTION_HR=$(awk "BEGIN { printf \"%.2f\", (1 - $NEW_SIZE / $ORIG_SIZE) * 100 }") |
| 257 | |
| 258 | echo |
| 259 | echo "Done. Final reduction: $REDUCTION_HR% ($HUMAN_ORIG > $HUMAN_NEW)" |
| 260 | echo |
airikr / Compress MKV and MP4 to MP4 with options
Last active 4 months ago
Created by ChatGPT, but not the first comment section.
Revision 540f1008866988c7c5be137e2cd780695da4dec3