fix(goLive): kill 4.3s backlog — HWM2 pipes + wire A/V sync (dank-faithful)

Lag root cause: vPipe/aPipe were objectMode PassThrough HWM 128 → the pipe
held up to 128 frames ≈ 4.3s of video before backpressure reached the encoder.
The viewer was watching a 4+ second stale backlog.

Fixes (both faithful to @dank074/discord-video-stream):
1. vPipe/aPipe HWM 2 — at most ~1-2 frames in flight (~66ms @ 30fps), so the
   writeFrame() backpressure pauses ffmpeg stdout almost immediately and the
   whole chain (encoder → NUT → demuxer → vPipe → BaseMediaStream → WebRTC)
   runs at the sender's real pace, exactly like dank's 'resume &&= vPipe.write'.
2. Wire vStream.syncStream = aStream — audio is the master clock; video
   sleeps/wakes on ptsDelta like upstream newApi.js. Prevents A/V drift under
   variable encoder throughput.
This commit is contained in:
asepharyana
2026-08-13 18:34:28 +07:00
parent 6e188f81d6
commit 11f2ad5f23
2 changed files with 12 additions and 2 deletions
@@ -155,8 +155,14 @@ export async function demux(
audio: DemuxedStream | undefined;
close: () => void;
}> {
const vPipe = new PassThrough({ objectMode: true, highWaterMark: 128 });
const aPipe = new PassThrough({ objectMode: true, highWaterMark: 128 });
// objectMode pipes carrying one frame per item. HWM 2 keeps backpressure
// near-instant: at most ~1-2 frames in flight (~66ms @ 30fps) before the
// encoder is throttled, so the viewer sees near-live video instead of a
// multi-second backlog (the old HWM 128 held 128 frames ≈ 4.3s of lag).
// BaseMediaStream below has HWM 0, so the chain is tightly coupled to the
// WebRTC sender's real pace — faithful to @dank074/discord-video-stream.
const vPipe = new PassThrough({ objectMode: true, highWaterMark: 2 });
const aPipe = new PassThrough({ objectMode: true, highWaterMark: 2 });
const isStream = typeof input !== "string";
// NUT/matroska input (prepareStream with includeAudio) carries audio; the
@@ -369,6 +369,10 @@ export async function playStream(
console.log(
`[goLive:playStream] audio stream attached (${audio.codecName})`,
);
// A/V sync (faithful to @dank074 newApi.js): audio is the master clock.
// Video sleeps/wakes based on ptsDelta(video - audio) so they can't drift
// apart under variable encoder throughput.
vStream.syncStream = aStream;
}
const cleanup = () => {