fix(playout): use CFR rate + frame GOP for uniform HLS segments

CasparCG's FFMPEG consumer ignores -force_key_frames ("Unused option")
because it routes args to the muxer, not the encoder. Revert to the
frame-based GOP (-g 60 -keyint_min 60) but keep the forced CFR rate
(-r 30000/1001): at 29.97fps a 60-frame GOP is exactly 2.0s, so keyframes
and HLS splits land on clean 2s boundaries. CFR is what was missing
originally — with the channel's irregular feed rate, "60 frames" drifted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Zac Gaetano 2026-05-31 13:38:21 -04:00
parent f28799317d
commit 87d988810f

View file

@ -169,11 +169,16 @@ export class PlayoutManager {
// reloads forever ("sliding 0.00 / prev-sn na / MISSED") and never appends
// a fragment, so the preview stays black.
//
// Fix: force a constant output frame rate (-r, regenerates uniform PTS) and
// TIME-based keyframes every 2s (-force_key_frames) so every segment is a
// clean, keyframe-aligned 2.0s chunk. This yields a spec-compliant playlist
// (TARGETDURATION 2, stable 8-segment / 16s window) identical in shape to
// the capture/VOD HLS the rest of the app already plays.
// Fix: force a constant output frame rate (-r 30000/1001 = 29.97 CFR). This
// regenerates uniform PTS and — critically — makes the frame-based GOP land
// on regular time boundaries: at 29.97 fps a 60-frame GOP (-g 60) is exactly
// 2.0s, so every keyframe (and therefore every HLS split at -hls_time 2) is
// a clean 2.0s boundary. The original used -g 60 WITHOUT -r, so "60 frames"
// varied with the channel's irregular feed rate and segments drifted.
//
// NOTE: -force_key_frames does NOT work here — CasparCG's FFMPEG consumer
// routes args to the muxer, not the encoder, and logs it as "Unused option".
// The CFR rate + frame GOP is the combination that actually takes effect.
const out = `${HLS_DIR}/index.m3u8`;
const args = [
`FILE "${out}"`,
@ -182,9 +187,7 @@ export class PlayoutManager {
'-hls_list_size 8',
'-hls_flags delete_segments+append_list+independent_segments',
'-codec:v libx264 -preset:v veryfast -tune:v zerolatency -b:v 800k -maxrate 1M -bufsize 2M',
// 29.97 CFR confidence feed: -r forces constant frame rate (fixes the
// duration-0 PTS), -force_key_frames pins keyframes to 2s media boundaries.
'-r 30000/1001 -force_key_frames expr:gte(t,n_forced*2) -sc_threshold 0',
'-r 30000/1001 -g 60 -keyint_min 60 -sc_threshold 0',
'-codec:a aac -b:a 96k -ar 48000',
'-filter:v format=yuv420p',
].join(' ');