Files
RoadTripsGenerator/Scripts/compress.sh
T
2026-08-17 23:50:11 +02:00

313 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# =============================================================================
# COMPRESSION ROAD-TRIP — STRATÉGIE HYBRIDE + ENCODAGE PARALLÈLE
# Source : GoPro Hero 9, rushs en HEVC 1080p
#
# Sorties (noms de fichiers INCHANGÉS par rapport à l'ancien script) :
# <base>.mp4 1080p natif, HEVC/H.265, tag hvc1
# → gros fichier, là où les 40 % d'économie comptent
# <base>_mobile.mp4 720p, H.264, tag avc1, profil High
# → fichier chargé par défaut, se lit sur TOUS
# les navigateurs sans exception
#
# PARALLÉLISME
# Deux niveaux, tous deux actifs par défaut :
# 1. Les deux paliers d'un même rush sont indépendants → lancés ensemble.
# 2. JOBS rushs traités simultanément (pool géré par xargs -P).
# Le nombre de threads par encodeur est borné pour ne pas surcharger :
# x265 sature vite (scaling sublinéaire au-delà de ~6-8 threads), donc
# plusieurs encodages courts valent mieux qu'un seul très parallélisé.
#
# Variables d'environnement :
# JOBS=N nombre de rushs en parallèle
# (défaut : cœurs performance / 4, minimum 1)
# THREADS=N threads par encodeur (défaut : cœurs / JOBS)
# ENCODER=cpu (défaut) libx265 + libx264 — meilleure qualité/octet
# ENCODER=vt VideoToolbox — ~10x plus rapide sur Apple Silicon
# (⚠️ le parallélisme n'y sert à rien : le bloc média
# de la puce est une ressource fixe → JOBS=1 forcé)
# PRESET=medium preset CPU : ultrafast…placebo
# CRF_HEVC=26 qualité du 1080p HEVC (échelle x265)
# CRF_H264=25 qualité du 720p H.264 (échelle x264 — PAS la même !)
# VTQ_HD=60 VTQ_SD=58 qualité VideoToolbox (1-100, plus haut = meilleur)
# FPS_MAX=60 plafonne le framerate (un rush 30p reste en 30p,
# un rush 120p/240p est ramené à 60)
# FORCE=1 réencode même si le fichier de sortie existe
#
# Exemples :
# ./compress.sh mon-voyage
# JOBS=4 ./compress.sh mon-voyage
# JOBS=1 ./compress.sh mon-voyage # séquentiel + barre de progression
# ENCODER=vt ./compress.sh mon-voyage
# =============================================================================
set -uo pipefail
shopt -s nocaseglob nullglob
# --- 0. MODE INTERNE ---------------------------------------------------------
# Quand le script est rappelé par xargs pour traiter UN rush, il arrive ici.
if [ "${1:-}" = "--encode-one" ]; then
INTERNAL=1
BASE_DIR="${RT_BASE_DIR:-.}"
TARGET="${2:?--encode-one attend un fichier}"
else
INTERNAL=0
BASE_DIR="${1:-.}"
fi
SOURCE_DIR="$BASE_DIR/rushs"
DEST_DIR="$BASE_DIR/videos"
# --- 1. CONFIGURATION --------------------------------------------------------
ENCODER="${ENCODER:-cpu}"
PRESET="${PRESET:-medium}"
CRF_HEVC="${CRF_HEVC:-26}"
CRF_H264="${CRF_H264:-25}"
VTQ_HD="${VTQ_HD:-60}"
VTQ_SD="${VTQ_SD:-58}"
FPS_MAX="${FPS_MAX:-60}"
FORCE="${FORCE:-0}"
# Détection des cœurs. Sur Apple Silicon on ne compte que les cœurs
# PERFORMANCE : les cœurs d'efficience sont si lents qu'ils deviennent des
# traînards dans un pool x265 et pénalisent l'encodage entier.
detect_cores() {
local c=""
c=$(sysctl -n hw.perflevel0.logicalcpu 2>/dev/null); [ -n "$c" ] && { echo "$c"; return; }
c=$(sysctl -n hw.logicalcpu 2>/dev/null); [ -n "$c" ] && { echo "$c"; return; }
c=$(nproc 2>/dev/null); [ -n "$c" ] && { echo "$c"; return; }
echo 4
}
CORES="${CORES:-$(detect_cores)}"
# En VideoToolbox, le parallélisme ne sert à rien (un seul moteur d'encodage
# matériel sur les puces M de base) — sauf si l'utilisateur force JOBS.
JOBS_EXPLICIT=0
[ -n "${JOBS+x}" ] && JOBS_EXPLICIT=1
if [ "$ENCODER" = "vt" ] && [ "$JOBS_EXPLICIT" = "0" ]; then
JOBS=1
else
JOBS="${JOBS:-$(( CORES / 4 ))}"
fi
[ "$JOBS" -lt 1 ] 2>/dev/null && JOBS=1
[ -z "$JOBS" ] && JOBS=1
THREADS="${THREADS:-$(( CORES / JOBS ))}"
[ "$THREADS" -lt 2 ] 2>/dev/null && THREADS=2
# Le palier 720p H.264 est 3 à 5× plus rapide que le 1080p HEVC : on lui donne
# moitié moins de threads. Il finit largement avant, et la fenêtre pendant
# laquelle les deux paliers tournent ensemble ne surcharge pas la machine.
THREADS_SD=$(( THREADS / 2 ))
[ "$THREADS_SD" -lt 2 ] && THREADS_SD=2
# --- 2. FONCTION D'ENCODAGE --------------------------------------------------
# encode <entrée> <sortie> <filtre_vf|none> <hevc|h264> <crf> <vtq> <maxrate> <bufsize> <audio>
encode() {
local in="$1" out="$2" vf="$3" codec="$4" crf="$5" vtq="$6" maxrate="$7" bufsize="$8" abr="$9"
local -a vopts vf_args statopt
if [ "$vf" = "none" ]; then vf_args=(); else vf_args=(-vf "$vf"); fi
# Barre de progression uniquement en séquentiel : à plusieurs jobs, les
# sorties de progression s'entremêlent et deviennent illisibles.
if [ "$JOBS" = "1" ]; then statopt=(-stats); else statopt=(); fi
# ⚠️ Les tableaux VIDES sont développés avec l'idiome ${a[@]+"${a[@]}"}.
# Le /bin/bash d'Apple est en 3.2, où `"${vide[@]}"` sous `set -u` provoque
# une erreur "unbound variable" (corrigé seulement en bash 4.4).
if [ "$codec" = "hevc" ]; then
if [ "$ENCODER" = "vt" ]; then
vopts=(-c:v hevc_videotoolbox -profile:v main -q:v "$vtq" -allow_sw 1 -realtime 0 -tag:v hvc1)
else
vopts=(
-c:v libx265 -preset "$PRESET" -profile:v main -crf "$crf"
# keyint=120 -> 1 image-clé toutes les 2 s à 60 fps.
# scenecut laissé ACTIF (défaut) : un GOP strictement fixe n'est
# utile qu'en streaming segmenté, pas pour du MP4 progressif.
# pools=N borne le pool WPP pour cohabiter avec les autres jobs.
-x265-params "keyint=120:min-keyint=24:aq-mode=3:pools=$THREADS"
-maxrate "$maxrate" -bufsize "$bufsize"
# -tag:v hvc1 est INDISPENSABLE : sans lui ffmpeg écrit 'hev1',
# que Safari et QuickTime refusent de lire.
-tag:v hvc1
)
fi
else
if [ "$ENCODER" = "vt" ]; then
vopts=(-c:v h264_videotoolbox -profile:v high -q:v "$vtq" -allow_sw 1 -realtime 0)
else
vopts=(
-c:v libx264 -preset "$PRESET" -profile:v high -level 4.0 -crf "$crf"
-g 120 -keyint_min 24
-threads "$THREADS_SD"
-maxrate "$maxrate" -bufsize "$bufsize"
)
fi
fi
ffmpeg -nostdin -hide_banner -loglevel error ${statopt[@]+"${statopt[@]}"} \
-i "$in" \
-map 0:v:0 -map 0:a:0\? \
"${vopts[@]}" \
${vf_args[@]+"${vf_args[@]}"} \
-pix_fmt yuv420p \
-fpsmax "$FPS_MAX" \
-color_primaries bt709 -color_trc bt709 -colorspace bt709 \
-c:a aac -b:a "$abr" -ac 1 -ar 48000 \
-map_metadata -1 \
-movflags +faststart \
-y "$out"
}
fsize() { stat -f%z "$1" 2>/dev/null || stat -c%s "$1"; }
# --- 3. TRAITEMENT D'UN RUSH -------------------------------------------------
process_one() {
local f="$1"
local filename base desktop_out mobile_out src_h vf_hd
filename=$(basename "$f")
base="${filename%.*}"
desktop_out="$DEST_DIR/${base}.mp4"
mobile_out="$DEST_DIR/${base}_mobile.mp4"
if [ "$FORCE" != "1" ] && [ -s "$desktop_out" ] && [ -s "$mobile_out" ]; then
echo "⏭️ $filename — déjà encodé (FORCE=1 pour refaire)"
return 0
fi
src_h=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 "$f" 2>/dev/null | head -1)
src_h=${src_h:-1080}
# Rushs déjà en 1080p : pas de rééchelonnage, on garde le pixel natif.
# Si un rush est plus grand (2.7K/4K), on redescend proprement à 1080p.
if [ "$src_h" -gt 1080 ]; then vf_hd="scale=-2:1080:flags=lanczos"; else vf_hd="none"; fi
echo "🎬 $filename (${src_h}p) — encodage des 2 paliers en parallèle…"
# Les deux paliers sont indépendants : même source, sorties distinctes.
# On les lance ensemble — le 720p H.264, bien plus léger, se glisse dans
# les trous laissés par le 1080p HEVC.
local pid_hd pid_sd rc_hd rc_sd
encode "$f" "$desktop_out" "$vf_hd" hevc "$CRF_HEVC" "$VTQ_HD" 8M 16M 96k &
pid_hd=$!
encode "$f" "$mobile_out" "scale=-2:720:flags=lanczos" h264 "$CRF_H264" "$VTQ_SD" 5M 10M 64k &
pid_sd=$!
wait "$pid_hd"; rc_hd=$?
wait "$pid_sd"; rc_sd=$?
if [ "$rc_hd" = "0" ] && [ "$rc_sd" = "0" ] && [ -s "$desktop_out" ] && [ -s "$mobile_out" ]; then
printf " ✅ %-24s %s → 1080p %s (HEVC) + 720p %s (H.264)\n" \
"$filename" "$(du -h "$f" | cut -f1)" \
"$(du -h "$desktop_out" | cut -f1)" "$(du -h "$mobile_out" | cut -f1)"
[ -n "${RT_STATS:-}" ] && \
echo "$(fsize "$f") $(( $(fsize "$desktop_out") + $(fsize "$mobile_out") ))" >> "$RT_STATS"
return 0
else
echo " ❌ Échec sur $filename"
return 1
fi
}
# Point de sortie du mode interne (un seul rush, appelé par xargs)
if [ "$INTERNAL" = "1" ]; then
process_one "$TARGET"
exit $?
fi
# =============================================================================
# MODE PRINCIPAL
# =============================================================================
if [ ! -d "$SOURCE_DIR" ]; then
echo "❌ Erreur : Dossier source $SOURCE_DIR introuvable."
exit 1
fi
mkdir -p "$DEST_DIR"
# Vérification des encodeurs disponibles.
# (on stocke la liste : un `| grep -q` déclencherait un SIGPIPE que pipefail
# transformerait en faux négatif)
ENCODERS_LIST="$(ffmpeg -hide_banner -encoders 2>/dev/null)"
has_enc() { printf '%s' "$ENCODERS_LIST" | grep -q "$1"; }
if [ "$ENCODER" = "vt" ]; then
has_enc "hevc_videotoolbox" || { echo "❌ hevc_videotoolbox indisponible. Utilise ENCODER=cpu."; exit 1; }
has_enc "h264_videotoolbox" || { echo "❌ h264_videotoolbox indisponible. Utilise ENCODER=cpu."; exit 1; }
else
has_enc "libx265" || { echo "❌ libx265 absent de ton ffmpeg (brew install ffmpeg)."; exit 1; }
has_enc "libx264" || { echo "❌ libx264 absent de ton ffmpeg (brew install ffmpeg)."; exit 1; }
fi
# Liste des rushs
files=()
for f in "$SOURCE_DIR"/*.mp4 "$SOURCE_DIR"/*.mov; do
[ -e "$f" ] && files+=("$f")
done
echo "--- COMPRESSION ROAD-TRIP (HYBRIDE HEVC / H.264) ---"
echo "📂 Source : $SOURCE_DIR"
echo "📤 Destination : $DEST_DIR"
if [ "$ENCODER" = "vt" ]; then
echo "⚙️ Encodeur : VideoToolbox (q=$VTQ_HD / $VTQ_SD)"
[ "$JOBS" = "1" ] && [ "$JOBS_EXPLICIT" = "0" ] && \
echo " ️ JOBS=1 : le bloc média de la puce est une ressource fixe,"
[ "$JOBS" = "1" ] && [ "$JOBS_EXPLICIT" = "0" ] && \
echo " paralléliser ne ferait que mettre les jobs en file d'attente."
else
echo "⚙️ Encodeur : CPU preset=$PRESET (HEVC CRF $CRF_HEVC / H.264 CRF $CRF_H264)"
fi
echo "🧵 Parallélisme : $JOBS rush(s) à la fois × 2 paliers ($THREADS threads en 1080p, $THREADS_SD en 720p)"
echo " ($CORES cœur(s) performance détecté(s))"
echo " 1080p → HEVC (hvc1) | 720p → H.264 (avc1, lisible partout)"
echo "📼 ${#files[@]} rush(s) à traiter"
echo
if [ "${#files[@]}" -eq 0 ]; then
echo "️ Rien à faire."
exit 0
fi
RT_STATS="$(mktemp "${TMPDIR:-/tmp}/rtstats.XXXXXX")"
trap 'rm -f "$RT_STATS"' EXIT
SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
export RT_BASE_DIR="$BASE_DIR" RT_STATS
export ENCODER PRESET CRF_HEVC CRF_H264 VTQ_HD VTQ_SD FPS_MAX FORCE JOBS THREADS THREADS_SD CORES
T0=$(date +%s)
# xargs -P gère le pool de jobs : portable (y compris le bash 3.2 d'Apple,
# qui ne connaît pas `wait -n`) et il relance un job dès qu'un autre se termine.
printf '%s\0' "${files[@]}" | xargs -0 -P "$JOBS" -n1 "$SCRIPT_PATH" --encode-one
rc=$?
T1=$(date +%s)
ELAPSED=$((T1 - T0))
echo
if [ -s "$RT_STATS" ]; then
awk -v el="$ELAPSED" '
{ n++; s += $1; o += $2 }
END {
if (n > 0) {
printf "✅ %d vidéo(s) traitée(s) en %d min %02d s\n", n, el/60, el%60
printf "📊 Rushs : %d Mo → Web : %d Mo (%.0f%% du volume source)\n", \
s/1048576, o/1048576, o*100/s
}
}' "$RT_STATS"
else
printf "️ Aucun nouvel encodage (durée : %d min %02d s)\n" $((ELAPSED/60)) $((ELAPSED%60))
fi
echo
echo "🔍 Contrôle des tags (attendu : hvc1 pour les 1080p, avc1 pour les _mobile) :"
echo " ffprobe -v error -select_streams v:0 -show_entries stream=codec_tag_string \\"
echo " -of csv=p=0 $DEST_DIR/*.mp4"
echo
echo "💡 Pour trouver ton JOBS optimal, chronomètre 3-4 rushs à JOBS=1, 2 puis 4 :"
echo " le gain plafonne vite, et au-delà le cache thrashe."
exit $rc