fix(gateway): deliver audio + per-IDR SPS/PPS in GoLive screen share

Screen share showed a single frozen frame: the GoLive pipeline sent video
only (-"-an", h264 muxer cannot carry audio) so the audio SSRC never
transmitted and Discord kept the stream in thumbnail state.

- prepareStream: mux NUT when includeAudio (h264 muxer drops audio) and
  return the actual container format
- Demuxer: support NUT input with a second output pipe (fd3) carrying
  Ogg Opus; parse OGG pages into opus frames (20ms, 48kHz) emitted as
  GoLiveFrames; fix metadata parsing that dropped the audio stream line
  when it arrived in a later stderr chunk (early parsedMeta return)
- playStream: pipe audio.stream into AudioStream → RTP on the audio SSRC
- Encoders: -x264-params repeat-headers=1 → SPS/PPS inline before EVERY
  IDR (NUT remux drops container extradata; also enables PLI recovery)
- screenShareController: includeAudio true
- tests: demuxerNut.test.ts — OGG parser unit test + real ffmpeg NUT
  integration (video access units + parsed opus frames)
This commit is contained in:
asepharyana
2026-08-12 14:20:29 +07:00
parent f70a92880e
commit f1aa08cdf6
7 changed files with 390 additions and 24 deletions
@@ -207,11 +207,13 @@ export class ScreenShareController {
frameRate: 30,
bitrateVideo: 2500,
bitrateVideoMax: 4000,
// Video-only GoLive: the -f h264 output muxer cannot carry audio
// ("h264 muxer does not support any stream of type audio" → header
// write fails → empty stdout → demux 'Invalid data' → black tile).
// Audio is not delivered by the GoLive demux path anyway.
includeAudio: false,
// GoLive with audio: the encoder muxes to NUT (video h264 + opus
// audio) so the audio SSRC carries RTP too. Discord's GoLive
// pipeline expects audio — a video-only stream shows a static
// tile/thumbnail instead of live video. When the source has no
// audio track, the encoder's `-map 0:a:0?` yields no audio stream
// and the demuxer simply reports none (video still flows).
includeAudio: true,
videoCodec: normalizeVideoCodec("H264"),
});
const { command } = prepared;