def _docker_ffmpeg_args(cpus=None, cpuset=None, use_gpu=False):
args = ['docker','run','--rm',
'-e','DOCKER_CONFIG=/dev/null']
if cpus: args += ['--cpus', str(cpus)]
if cpuset: args += ['--cpuset-cpus', str(cpuset)]
if use_gpu:
args += ['--gpus=all',
'-e','NVIDIA_VISIBLE_DEVICES=all',
'-e','NVIDIA_DRIVER_CAPABILITIES=all']
args += [
'--security-opt','seccomp=unconfined',
'--cap-add','SYS_NICE',
'-v', docker_mount_point,
'ghcr.io/aperim/nvidia-cuda-ffmpeg:latest',
]
return args
def _nvenc_cmd(
file_path: str,
out_path: str,
vkbps: int,
vf_h: int,
is_10bit: bool, # non usato qui
n_audio: int,
ext_low: str, # compat
audio_map_relidx: list[int] | None = None,
force_stereo: bool = FORCE_STEREO,
use_dplii_matrix: bool = True,
keep_subtitles: bool = True, # nuovo: se True, mantieni i sottotitoli
) -> list[str]:
cmd = _docker_ffmpeg_args(use_gpu=True) + [
'-y','-nostdin','-hide_banner','-loglevel','error',
'-hwaccel','cuda',
'-i', file_path,
'-vf', f'scale=-2:{vf_h}:force_original_aspect_ratio=decrease',
]
# --- Mapping ---
# Video principale
cmd += ['-map','0:v:0']
# Audio: o filtro esplicito o tutte
if audio_map_relidx is not None:
cmd += ['-map','-0:a'] # drop tutte le audio
for idx in sorted(audio_map_relidx):
cmd += ['-map', f'0:a:{idx}']
eff_n_audio = len(audio_map_relidx)
else:
cmd += ['-map','0:a?']
eff_n_audio = n_audio
# Sottotitoli (se richiesto)
if keep_subtitles:
cmd += ['-map','0:s?']
eff_n_subs = 1 # serve solo per flag copy; non puntiamo gli indici qui
else:
eff_n_subs = 0
# --- Video (come tuo originale NVENC) ---
cmd += [
'-c:v','hevc_nvenc',
'-rc','vbr_hq','-multipass','fullres',
'-b:v', f'{int(vkbps)}k',
'-maxrate', f'{int(vkbps*1.08)}k',
'-bufsize', f'{int(vkbps*2.0)}k',
'-rc-lookahead','32',
'-spatial_aq','1','-temporal_aq','1','-aq-strength','8',
'-bf','3','-b_ref_mode','middle',
'-g','240',
]
# --- Audio ---
if eff_n_audio > 0:
cmd += ['-c:a','libfdk_aac']
# Default globale (copre eventuali stream non indicizzati)
cmd += ['-b:a', f'{AUDIO_KBPS_PER_TRACK}k']
# Impostazioni per ciascuna traccia audio
for i in range(eff_n_audio):
cmd += [f'-b:a:{i}', f'{AUDIO_KBPS_PER_TRACK}k']
if force_stereo:
cmd += [f'-ac:a:{i}', '2']
if use_dplii_matrix:
# indica encoding matrix dolby pro logic II (metadato utile)
cmd += [f'-filter:a:{i}', 'aresample=matrix_encoding=dplii']
# Titolo pulito della traccia
cmd += [f'-metadata:s:a:{i}', f'title=AAC LC 2.0 {AUDIO_KBPS_PER_TRACK} kbps']
# Disposition: prima audio default, le altre non-default
cmd += ['-disposition:a:0','default']
for i in range(1, eff_n_audio):
cmd += [f'-disposition:a:{i}','0'] # rimuove default/forced se ereditati
else:
cmd += ['-an'] # nessuna traccia audio
# --- Sottotitoli: copia "as-is" (MKV è perfetto per srt/ass/pgs/vobsub) ---
if keep_subtitles and eff_n_subs:
cmd += ['-c:s','copy']
# opzionale: assicurati che nessun sottotitolo sia "default"
# (evita sorprese quando ffmpeg ereditasse i flag)
cmd += ['-disposition:s','0']
# --- Metadati/Capitoli del container ---
# Puliamo: BPS/NUMBER_OF_BYTES/encoder ecc. (ri-setteremo noi dopo il size-check)
cmd += ['-map_metadata','-1']
# Matroska container (lavoriamo solo in mkv)
cmd.append(out_path)
return cmd
def _run(cmd: List[str]) -> Tuple[int, float, str]:
start = time.time()
p = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
elapsed = time.time() - start
return p.returncode, elapsed, (p.stderr.decode('utf-8', errors='ignore') if p.stderr else "")
def run_encode_once(out_path: str, ctx: EncodeContext, plan: TargetPlan, audio_map_relidx: Optional[List[int]] = None) -> Tuple[int, float, int]:
try:
if os.path.exists(out_path):
os.remove(out_path)
except Exception:
pass
cmd = _nvenc_cmd(
file_path=ctx.file_path,
out_path=out_path,
vkbps=int(plan.v_kbps),
vf_h=int(plan.vf_height),
is_10bit=ctx.is_10bit,
n_audio=ctx.n_audio,
ext_low=Path(ctx.file_path).suffix.lower(),
audio_map_relidx=audio_map_relidx
)
eff_n_audio = (len(audio_map_relidx) if audio_map_relidx is not None else ctx.n_audio)
logger.info(
"[encode] start: out=%s, v=%s kb/s, a_total=%s kb/s, total=%s kb/s, vf_h=%s, audio_streams=%d, dur=%s (%.0fs)",
out_path, plan.v_kbps, plan.target_audio_kbps_total, plan.safe_total_kbps,
plan.vf_height, eff_n_audio, _fmt_hms(ctx.duration_s), ctx.duration_s
)
rc, elapsed, stderr = _run(cmd)
out_size = os.path.getsize(out_path) if (rc == 0 and os.path.exists(out_path)) else -1
if rc != 0:
tail = (stderr[-400:] if stderr else "")
logger.error(
"[encode] failed: rc=%s, elapsed=%.1fs, out_exists=%s, stderr_tail=%s",
rc, elapsed, os.path.exists(out_path), tail
)
else:
# bitrate effettivo totale e fattore realtime
eff_total_kbps = int((out_size * 8) / max(1.0, ctx.duration_s) / 1000)
delta_pct = ( (eff_total_kbps / plan.safe_total_kbps - 1.0) * 100.0 ) if plan.safe_total_kbps else 0.0
rt_factor = (ctx.duration_s / max(1e-6, elapsed))
logger.info(
"[encode] done: rc=0, elapsed=%.1fs (%.2fx realtime), size=%s (%d B), eff_total=%d kb/s "
"(plan_total=%s, Δ=%.2f%%)",
elapsed, rt_factor, _fmt_bytes(out_size), out_size,
eff_total_kbps, plan.safe_total_kbps, delta_pct
)
return rc, elapsed, out_size