-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathffmpeg_engine.py
More file actions
202 lines (163 loc) · 7.69 KB
/
Copy pathffmpeg_engine.py
File metadata and controls
202 lines (163 loc) · 7.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
"""
ffmpeg_engine.py — Costruzione del filtro ffmpeg ed esecuzione dell'encoding.
Contiene:
* build_filter_complex() -> genera la stringa filter_complex per ffmpeg
* run_ffmpeg() -> esegue ffmpeg con barra di avanzamento in tempo reale
Perché filter_complex invece di approcci più semplici (es. più passate):
filter_complex applica TUTTI i tagli in un'unica passata ffmpeg,
garantendo sincronizzazione audio/video frame-accurate e precisione
al microsecondo (6 cifre decimali nei timestamp). Approcci alternativi
(es. concatenare file separati) introducono rischi di desincronizzazione.
Struttura del filter_complex (2 fasi):
Fase A -> trim: ritaglia ogni segmento e resetta i timestamp
Fase B -> concat: unisce tutti i segmenti in un unico flusso
Dipendenze interne: utils (per col, C, fmt_duration, timecode_to_sec, print_ok, print_err)
"""
import sys
import subprocess
import time
from pathlib import Path
from utils import col, C, fmt_duration, timecode_to_sec, print_ok, print_err
def build_filter_complex(
keep_segments: list[tuple[float, float]],
has_audio: bool,
) -> str:
"""
Costruisce la stringa filter_complex per ffmpeg.
Il filtro applica due operazioni in sequenza su un'unica passata:
FASE A — Trim di ogni segmento:
trim/atrim ritaglia la finestra temporale del segmento.
setpts/asetpts resetta il timer da 0 per evitare buchi di tempo
tra un segmento e il successivo nel concat.
FASE B — Concatenazione:
concat unisce tutti i segmenti nell'ordine, producendo
i flussi finali [vout] e [aout].
Input:
keep_segments -> lista (start, end) in secondi dei segmenti da tenere
has_audio -> True se il video ha una traccia audio
Output: stringa da passare a ffmpeg come valore di -filter_complex
"""
parts: list[str] = []
n = len(keep_segments)
# ── FASE A: trim di ogni segmento ─────────────────────────────────
v_labels: list[str] = []
a_labels: list[str] = []
for i, (start, end) in enumerate(keep_segments):
s6 = f"{start:.6f}" # 6 decimali = precisione al microsecondo
e6 = f"{end:.6f}"
# Taglia il segmento video e resetta il timer a 0
parts.append(f"[0:v]trim=start={s6}:end={e6},setpts=PTS-STARTPTS[v{i}]")
v_labels.append(f"[v{i}]")
if has_audio:
# Stessa finestra temporale per l'audio: garantisce sync A/V perfetta
parts.append(f"[0:a]atrim=start={s6}:end={e6},asetpts=PTS-STARTPTS[a{i}]")
a_labels.append(f"[a{i}]")
# ── FASE B: concatenazione finale ─────────────────────────────────
if has_audio:
concat_in = "".join(f"{v}{a}" for v, a in zip(v_labels, a_labels))
parts.append(f"{concat_in}concat=n={n}:v=1:a=1[vout][aout]")
else:
concat_in = "".join(v_labels)
parts.append(f"{concat_in}concat=n={n}:v=1:a=0[vout]")
# I segmenti del filtro sono separati da punto e virgola
return ";\n".join(parts)
def run_ffmpeg(
video_path: Path,
output_path: Path,
keep_segments: list[tuple[float, float]],
video_info: dict,
hw_accel: dict | None = None,
) -> None:
"""
Applica i tagli al video con una singola passata ffmpeg.
Qualità video: sempre lossless.
* CPU: libx264 -crf 0 -> lossless matematico (nessuna perdita)
* HW: massima qualità per l'encoder hardware disponibile
* Audio: AAC 320kbps 48kHz -> soglia di trasparenza percettiva assoluta
Mostra una barra di avanzamento in tempo reale con:
[████░░░░] 42.3% 00:08:43.200 velocità 2.4x ETA 3m 47s
Input:
video_path -> file video originale da elaborare
output_path -> percorso del file video finale
keep_segments -> segmenti da mantenere (da compute_keep_segments)
video_info -> metadati del video (da get_video_info)
hw_accel -> encoder HW da usare (da detect_hw_encoder), None = CPU
Output: nessuno — crea output_path su disco
"""
has_audio = video_info["audio_stream"] is not None
filter_str = build_filter_complex(keep_segments, has_audio)
output_dur = sum(e - s for s, e in keep_segments) # durata stimata output
# Selezione encoder e parametri qualità
if hw_accel:
video_codec = hw_accel["codec"]
quality_flags = hw_accel["quality_arg"]
extra_flags = hw_accel.get("extra", [])
preset_flags = [] # gli encoder HW non usano -preset software
else:
video_codec = "libx264"
quality_flags = ["-crf", "0"] # lossless matematico
extra_flags = []
preset_flags = ["-preset", "medium"] # bilanciato velocità/compressione
cmd = [
"ffmpeg", "-y",
"-threads", "0", # usa tutti i core CPU disponibili
"-i", str(video_path),
"-filter_complex", filter_str,
"-map", "[vout]",
"-c:v", video_codec,
*preset_flags,
*quality_flags,
*extra_flags,
"-pix_fmt", "yuv420p", # formato pixel compatibile con tutti i player
"-movflags", "+faststart", # ottimizza per riproduzione immediata (streaming)
]
if has_audio:
cmd += ["-map", "[aout]", "-c:a", "aac",
"-b:a", "320k", "-ar", "48000"]
cmd.append(str(output_path))
# ── Barra di avanzamento in tempo reale ───────────────────────────
print(f"\n {col(C.CYAN, chr(0x27F3))} Avvio encoding...", end="", flush=True)
t0 = time.time()
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
last_display = ""
stderr_lines: list[str] = []
for line in process.stderr:
stderr_lines.append(line)
if "time=" not in line or "speed=" not in line:
continue
try:
time_part = line.split("time=")[1].split(" ")[0].strip()
speed_str = line.split("speed=")[1].split(" ")[0].strip().rstrip("x")
speed = float(speed_str) if speed_str not in ("N/A", "") else 0.0
current = timecode_to_sec(time_part)
# ETA = secondi rimanenti / velocità attuale
eta_str = (fmt_duration(max(0.0, output_dur - current) / speed)
if speed > 0 and current > 0 else "...")
# Barra visuale proporzionale al progresso
pct = min(1.0, current / output_dur) if output_dur > 0 else 0
done = int(pct * 30)
bar = col(C.GREEN, "\u2588" * done) + col(C.GRAY, "\u2591" * (30 - done))
display = (f"\r {col(C.CYAN, chr(0x27F3))} [{bar}] {pct*100:5.1f}% "
f"{col(C.CYAN, time_part)} "
f"velocita' {col(C.YELLOW, speed_str + 'x')} "
f"ETA {col(C.GREEN, eta_str)} ")
if display != last_display:
last_display = display
print(display, end="", flush=True)
except (IndexError, ValueError):
pass
process.wait()
print() # newline dopo la barra di avanzamento
elapsed = time.time() - t0
if process.returncode != 0:
print_err("Errore ffmpeg:")
for line in stderr_lines[-20:]:
print_err(f" {line.strip()}")
if output_path.exists():
output_path.unlink() # rimuove il file parziale
sys.exit(1)
size_mb = output_path.stat().st_size / 1_048_576
print_ok(f"Video salvato: {output_path} ({size_mb:.1f} MB)")
print_ok(f"Tempo di elaborazione: {fmt_duration(elapsed)}")