fix(goLive): stream demux directly instead of spool-to-file (empty screen share)
Root cause of 'tile appears but content empty': demux() spooled the live NUT/H264 input to a temp file and awaited stream 'finish' — but the merge ffmpeg output never ends during playback, so demux deadlocked, no probe, no transcode, 0 frames sent. - Demuxer: pipe input straight into ffmpeg stdin (-i pipe:0), parse NAL frames live from stdout; parse video metadata from ffmpeg stderr with a 1.5s race (fall back to H264 defaults). No spool, no await-end. - screenShareController: pass width/height/frameRate (1280x720@30) to playStream — matches the prepareStream encode settings, so setVideoAttributes gets real dimensions even when ffmpeg can't report metadata on an open pipe. - Add tests/golive-demux-live-e2e.ts: proves frames flow while input is still open (regression test for the deadlock).
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// Regression test: demux must emit frames from a LIVE stream that never
|
||||
// ends (the NUT/H264 merge output during playback). The old implementation
|
||||
// spooled the whole stream to a file first → deadlocked forever → 0 frames.
|
||||
// Run: npx tsx tests/golive-demux-live-e2e.ts [ffmpeg-path]
|
||||
import { spawn } from "node:child_process";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { demux } from "../src/goLive/Demuxer.js";
|
||||
|
||||
const FFMPEG = process.argv[2] ?? "ffmpeg";
|
||||
|
||||
// 1) Generate a 2s H264 test clip to a temp file
|
||||
const clip = "/tmp/golive-live-test.h264";
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const p = spawn(
|
||||
FFMPEG,
|
||||
[
|
||||
"-hide_banner", "-loglevel", "error",
|
||||
"-f", "lavfi", "-i", "testsrc=size=640x360:rate=30:duration=2",
|
||||
"-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p",
|
||||
"-f", "h264", clip,
|
||||
],
|
||||
{ stdio: ["ignore", "ignore", "pipe"] },
|
||||
);
|
||||
let err = "";
|
||||
p.stderr?.on("data", (d: Buffer) => (err += d.toString()));
|
||||
p.on("close", (code) => (code === 0 ? resolve() : reject(new Error(err))));
|
||||
});
|
||||
|
||||
// 2) Feed the clip through a PassThrough but DON'T end it (live semantics),
|
||||
// with a small pause after the first chunk so demux has time to emit.
|
||||
const input = new PassThrough();
|
||||
const demuxPromise = demux(input, { format: "h264" });
|
||||
const { video, close } = await demuxPromise;
|
||||
|
||||
let frames = 0;
|
||||
let bytes = 0;
|
||||
video.stream.on("data", (frame: { data: Buffer }) => {
|
||||
frames++;
|
||||
bytes += frame.data.length;
|
||||
});
|
||||
|
||||
const fs = await import("node:fs");
|
||||
const buf = fs.readFileSync(clip);
|
||||
const chunkSize = 16384;
|
||||
for (let i = 0; i < buf.length; i += chunkSize) {
|
||||
input.write(buf.subarray(i, i + chunkSize));
|
||||
if (i === 0) await new Promise((r) => setTimeout(r, 1500));
|
||||
}
|
||||
// Stream still open — if the old spool logic was here we'd never emit.
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
|
||||
console.log(`metadata: ${video.codecName} ${video.width}x${video.height} fps=${video.framerate_num}/${video.framerate_den}`);
|
||||
console.log(`frames while stream OPEN (not ended): ${frames}, bytes: ${bytes}`);
|
||||
if (frames === 0) {
|
||||
console.error("FAIL: no frames emitted while input still open (deadlock)");
|
||||
close();
|
||||
process.exit(1);
|
||||
}
|
||||
input.end();
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
close();
|
||||
console.log("PASS: live stream demux works");
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user