rtg compresser : une ligne par vidéo et pourcentage en direct ; bannière x265 masquée
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
This commit is contained in:
1 parent
96b1e53a2f
commit
608334b9d8
1 file changed
+68
-18
+68
-18
@@ -10,6 +10,7 @@ interrompu ne laisse jamais un fichier abîmé sous le vrai nom.
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
@@ -42,7 +43,7 @@ def repartition(p):
|
|||||||
return jobs, threads, max(2, threads // 2), coeurs
|
return jobs, threads, max(2, threads // 2), coeurs
|
||||||
|
|
||||||
|
|
||||||
def _commande(src, dest, palier, p, threads, threads_sd, hauteur_src, stats):
|
def _commande(src, dest, palier, p, threads, threads_sd, hauteur_src):
|
||||||
hd = palier == "hd"
|
hd = palier == "hd"
|
||||||
if hd:
|
if hd:
|
||||||
vf = "scale=-2:1080:flags=lanczos" if hauteur_src > 1080 else None
|
vf = "scale=-2:1080:flags=lanczos" if hauteur_src > 1080 else None
|
||||||
@@ -54,7 +55,7 @@ def _commande(src, dest, palier, p, threads, threads_sd, hauteur_src, stats):
|
|||||||
# pool de threads x265. hvc1 : sans lui, Safari refuse la vidéo.
|
# pool de threads x265. hvc1 : sans lui, Safari refuse la vidéo.
|
||||||
vopts = ["-c:v", "libx265", "-preset", p["preset"], "-profile:v", "main",
|
vopts = ["-c:v", "libx265", "-preset", p["preset"], "-profile:v", "main",
|
||||||
"-crf", str(p["crf_hevc"]),
|
"-crf", str(p["crf_hevc"]),
|
||||||
"-x265-params", f"keyint=120:min-keyint=24:aq-mode=3:pools={threads}",
|
"-x265-params", f"keyint=120:min-keyint=24:aq-mode=3:pools={threads}:log-level=error",
|
||||||
"-maxrate", "8M", "-bufsize", "16M", "-tag:v", "hvc1"]
|
"-maxrate", "8M", "-bufsize", "16M", "-tag:v", "hvc1"]
|
||||||
audio = "96k"
|
audio = "96k"
|
||||||
else:
|
else:
|
||||||
@@ -67,9 +68,9 @@ def _commande(src, dest, palier, p, threads, threads_sd, hauteur_src, stats):
|
|||||||
"-crf", str(p["crf_h264"]), "-g", "120", "-keyint_min", "24",
|
"-crf", str(p["crf_h264"]), "-g", "120", "-keyint_min", "24",
|
||||||
"-threads", str(threads_sd), "-maxrate", "5M", "-bufsize", "10M"]
|
"-threads", str(threads_sd), "-maxrate", "5M", "-bufsize", "10M"]
|
||||||
audio = "64k"
|
audio = "64k"
|
||||||
cmd = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error"]
|
# -progress pipe:1 : avancement lisible par RTG (out_time_us=…) au lieu de
|
||||||
if stats:
|
# la ligne -stats, illisible quand deux encodages tournent ensemble.
|
||||||
cmd.append("-stats")
|
cmd = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-nostats", "-progress", "pipe:1"]
|
||||||
cmd += ["-i", src, "-map", "0:v:0", "-map", "0:a:0?"] + vopts
|
cmd += ["-i", src, "-map", "0:v:0", "-map", "0:a:0?"] + vopts
|
||||||
if vf:
|
if vf:
|
||||||
cmd += ["-vf", vf]
|
cmd += ["-vf", vf]
|
||||||
@@ -89,6 +90,27 @@ def _hauteur(src):
|
|||||||
return 1080
|
return 1080
|
||||||
|
|
||||||
|
|
||||||
|
def _duree(src):
|
||||||
|
r = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
|
||||||
|
"-of", "csv=p=0", src], capture_output=True, text=True)
|
||||||
|
try:
|
||||||
|
return float(r.stdout.strip())
|
||||||
|
except ValueError:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _suivre(proc, palier, duree, avancement):
|
||||||
|
"""Lit la sortie -progress de ffmpeg et met à jour avancement[palier] (0-100)."""
|
||||||
|
for ligne in proc.stdout:
|
||||||
|
if ligne.startswith(("out_time_us=", "out_time_ms=")) and duree > 0:
|
||||||
|
try:
|
||||||
|
avancement[palier] = min(100.0, int(ligne.split("=")[1]) / 1e6 / duree * 100)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
elif ligne.startswith("progress=end"):
|
||||||
|
avancement[palier] = 100.0
|
||||||
|
|
||||||
|
|
||||||
def sorties(voyage, src):
|
def sorties(voyage, src):
|
||||||
base = os.path.splitext(os.path.basename(src))[0]
|
base = os.path.splitext(os.path.basename(src))[0]
|
||||||
dossier = os.path.join(voyage, "videos")
|
dossier = os.path.join(voyage, "videos")
|
||||||
@@ -104,8 +126,8 @@ def deja_faite(voyage, src, rapide=False):
|
|||||||
|
|
||||||
|
|
||||||
def compresser(voyage, cfg, analyse, force=False, progression=None):
|
def compresser(voyage, cfg, analyse, force=False, progression=None):
|
||||||
"""Compresse les rushs du voyage. `progression(nom, etat)` est appelée à
|
"""Compresse les rushs du voyage. `progression(nom, etat, pourcentage)` est
|
||||||
chaque changement (pour l'interface). Renvoie True si tout a réussi."""
|
appelée à chaque changement (pour l'interface). Renvoie True si tout a réussi."""
|
||||||
videos = analyse.videos
|
videos = analyse.videos
|
||||||
os.makedirs(os.path.join(voyage, "videos"), exist_ok=True)
|
os.makedirs(os.path.join(voyage, "videos"), exist_ok=True)
|
||||||
p = parametres_video(cfg)
|
p = parametres_video(cfg)
|
||||||
@@ -126,27 +148,55 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
|
|||||||
verrou = threading.Lock()
|
verrou = threading.Lock()
|
||||||
bilan = {"faits": 0, "sautes": 0, "echecs": 0, "octets_src": 0, "octets_web": 0}
|
bilan = {"faits": 0, "sautes": 0, "echecs": 0, "octets_src": 0, "octets_web": 0}
|
||||||
|
|
||||||
def signaler(nom, etat):
|
def signaler(nom, etat, detail=None):
|
||||||
if progression:
|
if progression:
|
||||||
progression(nom, etat)
|
progression(nom, etat, detail)
|
||||||
|
|
||||||
def un_rush(src):
|
total = len(videos)
|
||||||
|
en_direct = jobs == 1 and sys.stdout.isatty()
|
||||||
|
|
||||||
|
def un_rush(arg):
|
||||||
|
i, src = arg
|
||||||
nom = os.path.basename(src)
|
nom = os.path.basename(src)
|
||||||
|
rang = f"[{i:3d}/{total}]"
|
||||||
hd, sd = sorties(voyage, src)
|
hd, sd = sorties(voyage, src)
|
||||||
if not force and lisible(hd) and lisible(sd):
|
if not force and lisible(hd) and lisible(sd):
|
||||||
with verrou:
|
with verrou:
|
||||||
bilan["sautes"] += 1
|
bilan["sautes"] += 1
|
||||||
|
dire(f"⏭️ {rang} {nom} — déjà faite")
|
||||||
signaler(nom, "deja_faite")
|
signaler(nom, "deja_faite")
|
||||||
return True
|
return True
|
||||||
h = _hauteur(src)
|
h, duree = _hauteur(src), _duree(src)
|
||||||
dire(f"🎬 {nom} ({h}p) — encodage des 2 paliers en parallèle…")
|
dire(f"🎬 {rang} {nom} ({h}p, {int(duree) // 60} min {int(duree) % 60:02d} s) — encodage…")
|
||||||
signaler(nom, "en_cours")
|
signaler(nom, "en_cours", 0)
|
||||||
tmp_hd = os.path.join(os.path.dirname(hd), ".tmp-" + os.path.basename(hd))
|
tmp_hd = os.path.join(os.path.dirname(hd), ".tmp-" + os.path.basename(hd))
|
||||||
tmp_sd = os.path.join(os.path.dirname(sd), ".tmp-" + os.path.basename(sd))
|
tmp_sd = os.path.join(os.path.dirname(sd), ".tmp-" + os.path.basename(sd))
|
||||||
stats = jobs == 1
|
avancement = {"hd": 0.0, "sd": 0.0}
|
||||||
procs = [subprocess.Popen(_commande(src, tmp_hd, "hd", p, threads, threads_sd, h, stats)),
|
procs, lecteurs = [], []
|
||||||
subprocess.Popen(_commande(src, tmp_sd, "sd", p, threads, threads_sd, h, stats))]
|
for palier, tmp in (("hd", tmp_hd), ("sd", tmp_sd)):
|
||||||
codes = [pr.wait() for pr in procs]
|
pr = subprocess.Popen(_commande(src, tmp, palier, p, threads, threads_sd, h),
|
||||||
|
stdout=subprocess.PIPE, text=True)
|
||||||
|
th = threading.Thread(target=_suivre, args=(pr, palier, duree, avancement), daemon=True)
|
||||||
|
th.start()
|
||||||
|
procs.append(pr)
|
||||||
|
lecteurs.append(th)
|
||||||
|
dernier = -1
|
||||||
|
while any(pr.poll() is None for pr in procs):
|
||||||
|
time.sleep(0.5)
|
||||||
|
moyenne = int((avancement["hd"] + avancement["sd"]) / 2)
|
||||||
|
if en_direct:
|
||||||
|
print(f"\r 1080p {avancement['hd']:3.0f} % · 720p {avancement['sd']:3.0f} % ",
|
||||||
|
end="", flush=True)
|
||||||
|
elif moyenne // 25 > dernier // 25 and moyenne < 100:
|
||||||
|
dire(f" {nom} : {moyenne} %")
|
||||||
|
if moyenne != dernier:
|
||||||
|
signaler(nom, "en_cours", moyenne)
|
||||||
|
dernier = moyenne
|
||||||
|
if en_direct:
|
||||||
|
print()
|
||||||
|
for th in lecteurs:
|
||||||
|
th.join(timeout=5)
|
||||||
|
codes = [pr.returncode for pr in procs]
|
||||||
if codes == [0, 0] and lisible(tmp_hd) and lisible(tmp_sd):
|
if codes == [0, 0] and lisible(tmp_hd) and lisible(tmp_sd):
|
||||||
os.replace(tmp_hd, hd)
|
os.replace(tmp_hd, hd)
|
||||||
os.replace(tmp_sd, sd)
|
os.replace(tmp_sd, sd)
|
||||||
@@ -174,7 +224,7 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
|
|||||||
|
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
with ThreadPoolExecutor(jobs) as pool:
|
with ThreadPoolExecutor(jobs) as pool:
|
||||||
resultats = list(pool.map(un_rush, videos))
|
resultats = list(pool.map(un_rush, enumerate(videos, 1)))
|
||||||
duree = int(time.time() - t0)
|
duree = int(time.time() - t0)
|
||||||
dire()
|
dire()
|
||||||
if bilan["faits"]:
|
if bilan["faits"]:
|
||||||
|
|||||||
Reference in new issue
Block a user