fix(gateway): output NUT (not raw h264) so Demuxer re-splits video+audio correctly
Revert392bc35: streaming raw h264 video + opus on separate pipes broke because prepareStream.output (pipe:1) feeds the Demuxer, but the opus pipe:3 was never attached to the Demuxer's input — so for audio-capable streams the Demuxer saw format=h264 (video-only) and emitted -an, dropping audio RTP. Correct design (fromf1aa08c): prepareStream muxes video+audio into NUT on a SINGLE pipe:1. The Demuxer then spawns a child ffmpeg that demuxes NUT → -f h264 pipe:1 (pure AnnexB, start-code scan sees real IDR type 5) + -f opus pipe:3 (Ogg Opus via createOggOpusDemux). The start-code parser never touches NUT framing — it runs on the child ffmpeg's clean h264 stdout.
This commit is contained in:
@@ -28,8 +28,8 @@ export interface PrepareStreamResult {
|
|||||||
height: number;
|
height: number;
|
||||||
frameRate?: number;
|
frameRate?: number;
|
||||||
includeAudio: boolean;
|
includeAudio: boolean;
|
||||||
/** Output container — always raw H264 AnnexB (audio demuxed to pipe:3). */
|
/** Container the encoder muxes to: "nut" (audio-capable) or "h264" (raw). */
|
||||||
format: "h264";
|
format: "nut" | "h264";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFiniteNonZero(n: unknown): n is number {
|
function isFiniteNonZero(n: unknown): n is number {
|
||||||
@@ -183,18 +183,13 @@ export function prepareStream(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio (transcode to libopus for encoding). Video output is ALWAYS raw
|
// Audio: transcode to libopus. NUT muxer on stdout carries video (H264
|
||||||
// H264 AnnexB on pipe:1 — NOT NUT. NUT container framing on stdout corrupts
|
// AnnexB) + audio (Ogg Opus) as ONE stream into the Demuxer, which re-splits
|
||||||
// the AnnexB start-code scanner in Demuxer (NUT headers misread as NAL type 0
|
// them via a child ffmpeg -f nut -i pipe:0 -c:v copy -f h264 pipe:1 ... . The
|
||||||
// → every frame classified non-keyframe → static tile). NUT is only needed
|
// Demuxer's start-code scan runs on THAT child ffmpeg's stdout (pure H264),
|
||||||
// for the *input* (one pipe carries both streams); output demuxes each stream
|
// NOT on NUT — so NAL type 5 (IDR) is parsed correctly. (Outputting raw
|
||||||
// to its own raw format.
|
// h264+opus on two pipes directly was tried and broke: the audio pipe was
|
||||||
// Video output: raw H264 AnnexB on pipe:1 — demux() scans start codes.
|
// never attached to the demuxer's input, so audio RTP never flowed.)
|
||||||
args.push("-f", "h264", "pipe:1");
|
|
||||||
// Audio output: Ogg Opus on fd3 (pipe:3), separate from video stdout so it
|
|
||||||
// never pollutes the AnnexB start-code stream on pipe:1. Map the audio
|
|
||||||
// stream here (after the video -map 0:v:0 above) so it targets only this
|
|
||||||
// output.
|
|
||||||
if (mergedOptions.includeAudio) {
|
if (mergedOptions.includeAudio) {
|
||||||
args.push(
|
args.push(
|
||||||
"-map",
|
"-map",
|
||||||
@@ -207,14 +202,17 @@ export function prepareStream(
|
|||||||
"48000",
|
"48000",
|
||||||
"-ac",
|
"-ac",
|
||||||
"2",
|
"2",
|
||||||
"-f",
|
|
||||||
"opus",
|
|
||||||
"pipe:3",
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
args.push("-an");
|
args.push("-an");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NUT muxer carries video+audio; the raw h264 muxer cannot ("h264 muxer
|
||||||
|
// does not support any stream of type audio" -> header write fails ->
|
||||||
|
// empty stdout -> black tile). Audio delivery requires NUT.
|
||||||
|
const outFormat = mergedOptions.includeAudio ? "nut" : "h264";
|
||||||
|
args.push("-f", outFormat, "pipe:1");
|
||||||
|
|
||||||
const isUrl = typeof input === "string";
|
const isUrl = typeof input === "string";
|
||||||
const proc: ChildProcess = isUrl
|
const proc: ChildProcess = isUrl
|
||||||
? spawn(FFMPEG_BIN, args, { stdio: ["ignore", "pipe", "pipe"] })
|
? spawn(FFMPEG_BIN, args, { stdio: ["ignore", "pipe", "pipe"] })
|
||||||
@@ -263,7 +261,7 @@ export function prepareStream(
|
|||||||
height: mergedOptions.height,
|
height: mergedOptions.height,
|
||||||
frameRate: mergedOptions.frameRate,
|
frameRate: mergedOptions.frameRate,
|
||||||
includeAudio: !!mergedOptions.includeAudio,
|
includeAudio: !!mergedOptions.includeAudio,
|
||||||
format: "h264",
|
format: outFormat,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user