fix(hls): forced-idr en NVENC/QSV — los segmentos ignoraban force_key_frames

NVENC (ffmpeg 6.1 + drivers actuales) emite los keyframes forzados por
-force_key_frames como I-frames NO-IDR; el muxer HLS solo corta en IDR,
así que cada segmento se estiraba en silencio al GOP por defecto
(250 frames ≈ 10.4 s @24fps) mientras la playlist server-side seguía
prometiendo 2 s por segmento. Con los PTS reales ~5× fuera del mapa de
la playlist, los seeks aterrizaban donde podían y los subtítulos se
desincronizaban en cuanto se mezclaban segmentos de runs distintos
(seek-restart) en el mismo dir.

Medido: 3 segmentos por 30 s de encode en vez de 15; con -forced-idr 1
exactamente 15, y post-fix seg-150/151/158 arrancan en 300.0/302.0/316.0
clavados. Afecta a TODO el HLS por NVENC histórico (no era del rate
control nuevo: la config de bitrate fijo producía lo mismo). QSV recibe
su grafía -forced_idr. Las entradas de caché viejas nunca llegaron a
sellarse (el conteo de segmentos no cuadraba), así que no hay migración:
solo sesiones vivas estaban afectadas.
This commit is contained in:
Deivid Soto 2026-06-10 10:44:18 +02:00
parent f9ecd5ed82
commit 556c5cb05f
2 changed files with 15 additions and 3 deletions

View file

@ -1483,12 +1483,21 @@ func buildHLSFFmpegArgsAt(cfg HLSSessionConfig, probe *StreamProbe, tmpDir strin
// scene-cut). No B-frame reorder → monotonic DTS → uniform segments, no
// "Packet duration is out of range" flood. Safe with -force_key_frames
// (unlike -tune ll, which broke per-segment cuts — see note above).
args = append(args, "-preset", profile.Preset, "-rc", "vbr", "-bf", "0", "-no-scenecut", "1")
// -forced-idr 1 is LOAD-BEARING: NVENC emits -force_key_frames frames
// as plain (non-IDR) I-frames on current ffmpeg/driver combos, the HLS
// muxer only cuts on IDR, and every segment silently stretches to the
// default GOP (250 frames ≈ 10.4 s @24fps) while the server-rendered
// playlist still promises hlsSegmentDuration. The PTS↔playlist mismatch
// breaks seeks and desyncs subtitles (measured 2026-06-10: 3 segments
// per 30 s instead of 15; with -forced-idr exactly 15).
args = append(args, "-preset", profile.Preset, "-rc", "vbr", "-bf", "0", "-no-scenecut", "1", "-forced-idr", "1")
case "h264_qsv":
// veryfast is the fastest realistic QSV preset; medium was too
// conservative for first-start. look_ahead=0 keeps the encoder
// truly low-latency (no rate-control look-ahead window).
args = append(args, "-preset", profile.Preset, "-look_ahead", "0")
// -forced_idr: same non-IDR forced-keyframe failure mode as NVENC (see
// above) — QSV's AVOption spells it with an underscore.
args = append(args, "-preset", profile.Preset, "-look_ahead", "0", "-forced_idr", "1")
case "h264_videotoolbox":
// VideoToolbox has no "preset" knob; `-realtime` flips into the
// low-latency path used by FaceTime. We let the `-b:v / -maxrate