fix(capture): align A/V at record start (kill leading silence + length drift)
Root cause of 'silent first ~1s then clean' + ~0.5% audio-too-long: in standby the bridge keeps filling the audio FIFO while the idle-preview consumes only video, so when recording starts ffmpeg reads a ~0.5s backlog of stale audio, AND the video-only pre-roll discards video frames the audio never had. Fix: (1) skip the video-only pre-roll in standby (warm slot = no unstable frames), (2) drain the audio FIFO non-blocking immediately before ffmpeg opens it, so audio starts at the live edge aligned with the first real video frame.
This commit is contained in:
parent
fffb6b63b5
commit
b1a2249f36
1 changed files with 36 additions and 5 deletions
|
|
@ -732,7 +732,7 @@ class CaptureManager {
|
|||
],
|
||||
isNetwork: false,
|
||||
bridgeProcess: fcPipeProcess, /* capture-manager pipes this to ffmpeg stdin */
|
||||
audioFifo: null,
|
||||
audioFifo: audioFifoPath, /* flushed just before ffmpeg opens it (A/V align) */
|
||||
interlaced: fcInterlaced,
|
||||
audioInputIndex: 1, /* audio FIFO is ffmpeg input 1 */
|
||||
_fcPipeProcess: fcPipeProcess, /* stored for clean stop */
|
||||
|
|
@ -1030,16 +1030,47 @@ exit "$BMXRC"
|
|||
sourceType, sourceBackend, device, port, board, sourceUrl, listen, listenPort, streamKey,
|
||||
});
|
||||
|
||||
// ── Pre-roll: discard initial unstable frames ────────────────────────────
|
||||
if (bridgeProcess && (sourceType === 'deltacast' || sourceType === 'blackmagic' || sourceType === 'sdi')) {
|
||||
// ── Pre-roll + A/V alignment ─────────────────────────────────────────────
|
||||
// The pre-roll drains the VIDEO pipe (fc_pipe) to discard unstable startup
|
||||
// frames. In STANDBY the framecache slot is already warm, so there are no
|
||||
// unstable frames — skip the video drain (draining only video while audio
|
||||
// keeps buffering is exactly what offset the streams, giving "silent first
|
||||
// second then clean").
|
||||
if (bridgeProcess && !_standbyMode
|
||||
&& (sourceType === 'deltacast' || sourceType === 'blackmagic' || sourceType === 'sdi')) {
|
||||
console.log(`[capture] pre-rolling: discarding ${PRE_ROLL_SECONDS}s of frames`);
|
||||
// Attach temporary drain listener.
|
||||
bridgeProcess.stdout.on('data', () => {});
|
||||
bridgeProcess.stdout.on('data', () => {});
|
||||
await new Promise(r => setTimeout(r, PRE_ROLL_SECONDS * 1000));
|
||||
bridgeProcess.stdout.removeAllListeners('data');
|
||||
console.log(`[capture] pre-roll complete.`);
|
||||
}
|
||||
|
||||
// FLUSH STALE AUDIO immediately before ffmpeg opens the FIFO. During standby
|
||||
// the bridge keeps writing audio into the named FIFO while the idle-preview
|
||||
// consumes only video, so the FIFO holds up to a full pipe buffer (~0.5s) of
|
||||
// stale audio. Draining it here (right before the record ffmpeg attaches)
|
||||
// makes audio start at the live edge, time-aligned with the first video
|
||||
// frame — eliminating the leading silence + the ~0.5% audio-length surplus.
|
||||
if (audioFifo) {
|
||||
try {
|
||||
const fsSync = await import('node:fs');
|
||||
const fd = fsSync.openSync(audioFifo, fsSync.constants.O_RDONLY | fsSync.constants.O_NONBLOCK);
|
||||
const tmp = Buffer.allocUnsafe(1 << 20);
|
||||
let drained = 0;
|
||||
for (;;) {
|
||||
let n = 0;
|
||||
try { n = fsSync.readSync(fd, tmp, 0, tmp.length, null); }
|
||||
catch (e) { if (e.code === 'EAGAIN') break; throw e; }
|
||||
if (n <= 0) break;
|
||||
drained += n;
|
||||
}
|
||||
fsSync.closeSync(fd);
|
||||
console.log(`[capture] flushed ${drained} bytes of stale standby audio before record`);
|
||||
} catch (e) {
|
||||
console.warn(`[capture] audio FIFO pre-flush skipped: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const startedAt = new Date().toISOString();
|
||||
const recordingStartedAt = Date.now();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue