From 608334b9d8da6c5c7e2cb291a963f172d9eda4ac Mon Sep 17 00:00:00 2001 From: Fabrice Date: Fri, 9 Oct 2026 13:36:38 +0000 Subject: [PATCH] =?UTF-8?q?rtg=20compresser=20:=20une=20ligne=20par=20vid?= =?UTF-8?q?=C3=A9o=20et=20pourcentage=20en=20direct=20;=20banni=C3=A8re=20?= =?UTF-8?q?x265=20masqu=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim --- rtg/video.py | 86 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 18 deletions(-) diff --git a/rtg/video.py b/rtg/video.py index a0379fc..1c319ab 100644 --- a/rtg/video.py +++ b/rtg/video.py @@ -10,6 +10,7 @@ interrompu ne laisse jamais un fichier abîmé sous le vrai nom. """ import os import subprocess +import sys import threading import time from concurrent.futures import ThreadPoolExecutor @@ -42,7 +43,7 @@ def repartition(p): 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" if hd: 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. vopts = ["-c:v", "libx265", "-preset", p["preset"], "-profile:v", "main", "-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"] audio = "96k" 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", "-threads", str(threads_sd), "-maxrate", "5M", "-bufsize", "10M"] audio = "64k" - cmd = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error"] - if stats: - cmd.append("-stats") + # -progress pipe:1 : avancement lisible par RTG (out_time_us=…) au lieu de + # la ligne -stats, illisible quand deux encodages tournent ensemble. + cmd = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-nostats", "-progress", "pipe:1"] cmd += ["-i", src, "-map", "0:v:0", "-map", "0:a:0?"] + vopts if vf: cmd += ["-vf", vf] @@ -89,6 +90,27 @@ def _hauteur(src): 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): base = os.path.splitext(os.path.basename(src))[0] 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): - """Compresse les rushs du voyage. `progression(nom, etat)` est appelée à - chaque changement (pour l'interface). Renvoie True si tout a réussi.""" + """Compresse les rushs du voyage. `progression(nom, etat, pourcentage)` est + appelée à chaque changement (pour l'interface). Renvoie True si tout a réussi.""" videos = analyse.videos os.makedirs(os.path.join(voyage, "videos"), exist_ok=True) p = parametres_video(cfg) @@ -126,27 +148,55 @@ def compresser(voyage, cfg, analyse, force=False, progression=None): verrou = threading.Lock() 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: - 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) + rang = f"[{i:3d}/{total}]" hd, sd = sorties(voyage, src) if not force and lisible(hd) and lisible(sd): with verrou: bilan["sautes"] += 1 + dire(f"⏭️ {rang} {nom} — déjà faite") signaler(nom, "deja_faite") return True - h = _hauteur(src) - dire(f"🎬 {nom} ({h}p) — encodage des 2 paliers en parallèle…") - signaler(nom, "en_cours") + h, duree = _hauteur(src), _duree(src) + dire(f"🎬 {rang} {nom} ({h}p, {int(duree) // 60} min {int(duree) % 60:02d} s) — encodage…") + signaler(nom, "en_cours", 0) 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)) - stats = jobs == 1 - procs = [subprocess.Popen(_commande(src, tmp_hd, "hd", p, threads, threads_sd, h, stats)), - subprocess.Popen(_commande(src, tmp_sd, "sd", p, threads, threads_sd, h, stats))] - codes = [pr.wait() for pr in procs] + avancement = {"hd": 0.0, "sd": 0.0} + procs, lecteurs = [], [] + for palier, tmp in (("hd", tmp_hd), ("sd", tmp_sd)): + 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): os.replace(tmp_hd, hd) os.replace(tmp_sd, sd) @@ -174,7 +224,7 @@ def compresser(voyage, cfg, analyse, force=False, progression=None): t0 = time.time() 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) dire() if bilan["faits"]: