From 87d988810fc5d5fa29b8659f258f25b518b8bb67 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 31 May 2026 13:38:21 -0400 Subject: [PATCH] fix(playout): use CFR rate + frame GOP for uniform HLS segments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- services/playout/src/playout-manager.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/services/playout/src/playout-manager.js b/services/playout/src/playout-manager.js index e1562c4..35f1f2f 100644 --- a/services/playout/src/playout-manager.js +++ b/services/playout/src/playout-manager.js @@ -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(' ');