diff --git a/services/playout/src/playout-manager.js b/services/playout/src/playout-manager.js index 5609e09..e1562c4 100644 --- a/services/playout/src/playout-manager.js +++ b/services/playout/src/playout-manager.js @@ -159,24 +159,33 @@ export class PlayoutManager { } // Low-bitrate HLS for the web UI preview. Segments land in the shared media - // volume; the mam-api serves /live//* from there. + // volume; the mam-api serves /media/live//* from there. async _addHlsConsumer() { - // mkdir is done by the entrypoint; CasparCG's ffmpeg consumer creates the - // playlist on first segment. 2s segments / 6-window list keeps lag low - // without thrashing disk. - // FILE keyword (alias of the FFMPEG consumer) writing a segmented HLS - // playlist. Same arg rules as the STREAM consumer: -param:stream form and a - // format=yuv420p filter ahead of libx264 (channel output is RGBA). + // The CasparCG channel feeds this consumer in real time, and its frame + // timestamps are irregular ("packet with pts X has duration 0" warnings). + // With frame-count GOPs (-g 60) the HLS muxer split points drift, producing + // erratic segment durations (0.4s–4.2s) and TARGETDURATION violations. The + // result is a live playlist hls.js parses but can never sync to — it + // 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. const out = `${HLS_DIR}/index.m3u8`; const args = [ `FILE "${out}"`, '-format hls', '-hls_time 2', - '-hls_list_size 6', - '-hls_flags delete_segments+append_list', + '-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', - '-g 60 -keyint_min 60 -sc_threshold 0', - '-codec:a aac -b:a 96k', + // 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', + '-codec:a aac -b:a 96k -ar 48000', '-filter:v format=yuv420p', ].join(' '); await this.amcp.send(`ADD ${CHANNEL} ${args}`);