mise à jours HEVC pour 1080p
This commit is contained in:
Regular → Executable
+297
-68
@@ -1,83 +1,312 @@
|
||||
#!/bin/bash
|
||||
shopt -s nocaseglob
|
||||
# =============================================================================
|
||||
# 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
|
||||
|
||||
# --- 1. CONFIGURATION ---
|
||||
# Utilise l'argument 1 comme dossier racine, ou le dossier courant par défaut
|
||||
BASE_DIR="${1:-.}"
|
||||
SOURCE_DIR="$BASE_DIR/rushs"
|
||||
DEST_DIR="$BASE_DIR/videos"
|
||||
|
||||
# Vérification du dossier source
|
||||
# --- 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
|
||||
|
||||
# Création du dossier de destination
|
||||
mkdir -p "$DEST_DIR"
|
||||
|
||||
echo "--- COMPRESSION ROAD-TRIP (CPU OPTIMISÉ) ---"
|
||||
echo "📂 Source : $SOURCE_DIR"
|
||||
echo "📤 Destination : $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"; }
|
||||
|
||||
# 2. BOUCLE DE TRAITEMENT
|
||||
for f in "$SOURCE_DIR"/*.{mp4,mov}; do
|
||||
[ -e "$f" ] || continue
|
||||
|
||||
# Extraction du nom de base (ex: GH011182.mp4 -> GH011182)
|
||||
filename=$(basename "$f")
|
||||
base="${filename%.*}"
|
||||
|
||||
# Conventions de nommage validées
|
||||
desktop_out="$DEST_DIR/${base}.mp4"
|
||||
mobile_out="$DEST_DIR/${base}_mobile.mp4"
|
||||
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
|
||||
|
||||
echo "🎬 Traitement de : $filename ..."
|
||||
|
||||
# --- A. VERSION DESKTOP (1080p60 - Validé 233 Mo) ---
|
||||
# Réglages : CRF 28, Maxrate 12M, GOP 120, FPS source exact
|
||||
ffmpeg -i "$f" \
|
||||
-c:v libx264 -preset slow -profile:v high \
|
||||
-crf 28 \
|
||||
-tune film \
|
||||
-g 120 -keyint_min 120 -sc_threshold 0 \
|
||||
-maxrate 15M -bufsize 30M \
|
||||
-pix_fmt yuv420p \
|
||||
-r 60000/1001 \
|
||||
-vf "scale=1920:-2:flags=lanczos" \
|
||||
-c:a aac -b:a 96k -ac 1 \
|
||||
-map_metadata -1 \
|
||||
-movflags +faststart \
|
||||
-y -loglevel error \
|
||||
"$desktop_out"
|
||||
|
||||
# --- B. VERSION MOBILE (720p60 - Optimisé 4G/Smartphone) ---
|
||||
# Réglages : On baisse le maxrate à 6M pour la fluidité mobile
|
||||
ffmpeg -i "$f" \
|
||||
-c:v libx264 -preset slow -profile:v high \
|
||||
-crf 28 \
|
||||
-tune film \
|
||||
-g 120 -keyint_min 120 -sc_threshold 0 \
|
||||
-maxrate 6M -bufsize 12M \
|
||||
-pix_fmt yuv420p \
|
||||
-r 60000/1001 \
|
||||
-vf "scale=1280:-2:flags=lanczos" \
|
||||
-c:a aac -b:a 64k -ac 1 \
|
||||
-map_metadata -1 \
|
||||
-movflags +faststart \
|
||||
-y -loglevel error \
|
||||
"$mobile_out"
|
||||
|
||||
# Petit bilan de taille pour confirmer l'économie
|
||||
if [ -f "$desktop_out" ]; then
|
||||
d_size=$(du -h "$desktop_out" | cut -f1)
|
||||
m_size=$(du -h "$mobile_out" | cut -f1)
|
||||
echo " ✅ Desktop : $d_size (${base}.mp4)"
|
||||
echo " ✅ Mobile : $m_size (${base}_mobile.mp4)"
|
||||
fi
|
||||
|
||||
echo "---------------------------------------------------"
|
||||
# Liste des rushs
|
||||
files=()
|
||||
for f in "$SOURCE_DIR"/*.mp4 "$SOURCE_DIR"/*.mov; do
|
||||
[ -e "$f" ] && files+=("$f")
|
||||
done
|
||||
|
||||
shopt -u nocaseglob
|
||||
echo "✅ TOUTES LES VIDÉOS SONT PRÊTES."
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user