fix(playout): video-only HLS preview (broken audio time_base was the black-screen cause)

Definitive root cause of the black preview, found via server-side ffmpeg
decode of the live playlist:

  Error while decoding stream #0:1: Invalid data found (x57)
  [abuffer] Value inf for parameter 'time_base' ... time_base to value 1/0

Stream #0:1 is the AAC audio. CasparCG's real-time channel feeds the HLS
consumer an audio stream whose muxed time_base is 1/0 (infinity). ffmpeg
itself cannot decode the playlist, and hls.js silently fails to append the
fragment after demux, so the <video> stays at readyState 0 (black) even
though the video PTS is perfectly continuous and segments serve 200.

Fix: drop audio from the HLS confidence monitor (-an). The video track is
clean h264 and plays in hls.js. Program audio still rides the primary
SRT/RTMP/SDI/NDI output, which is unaffected.

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

View file

@ -169,16 +169,23 @@ 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 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.
// The HLS preview is a VIDEO-ONLY confidence monitor. We deliberately drop
// audio (-an).
//
// 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.
// Why: CasparCG's real-time channel feeds the FFMPEG consumer an audio
// stream whose muxed time_base comes out as 1/0 (infinity). ffmpeg itself
// can't decode the resulting playlist ("Invalid data ... abuffer: Value inf
// for parameter 'time_base'"), and hls.js silently fails to append the
// fragment after demux — the video element sits at readyState 0 and the
// preview stays black. Dropping audio removes the broken stream entirely;
// the remaining video track is clean h264 and plays in hls.js. A confidence
// monitor doesn't need audio — the real program audio rides the primary
// SRT/RTMP/SDI/NDI output, which is unaffected.
//
// NOTE: encoder options like -g / -r / -force_key_frames are NOT honored
// here — CasparCG's FFMPEG consumer applies args to the muxer, not the
// encoder (it logs "Unused option"). Segment cadence follows the channel's
// own keyframes; that's fine for a video-only preview.
const out = `${HLS_DIR}/index.m3u8`;
const args = [
`FILE "${out}"`,
@ -187,8 +194,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',
'-r 30000/1001 -g 60 -keyint_min 60 -sc_threshold 0',
'-codec:a aac -b:a 96k -ar 48000',
'-an',
'-filter:v format=yuv420p',
].join(' ');
await this.amcp.send(`ADD ${CHANNEL} ${args}`);