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,
|
isNetwork: false,
|
||||||
bridgeProcess: fcPipeProcess, /* capture-manager pipes this to ffmpeg stdin */
|
bridgeProcess: fcPipeProcess, /* capture-manager pipes this to ffmpeg stdin */
|
||||||
audioFifo: null,
|
audioFifo: audioFifoPath, /* flushed just before ffmpeg opens it (A/V align) */
|
||||||
interlaced: fcInterlaced,
|
interlaced: fcInterlaced,
|
||||||
audioInputIndex: 1, /* audio FIFO is ffmpeg input 1 */
|
audioInputIndex: 1, /* audio FIFO is ffmpeg input 1 */
|
||||||
_fcPipeProcess: fcPipeProcess, /* stored for clean stop */
|
_fcPipeProcess: fcPipeProcess, /* stored for clean stop */
|
||||||
|
|
@ -1030,16 +1030,47 @@ exit "$BMXRC"
|
||||||
sourceType, sourceBackend, device, port, board, sourceUrl, listen, listenPort, streamKey,
|
sourceType, sourceBackend, device, port, board, sourceUrl, listen, listenPort, streamKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Pre-roll: discard initial unstable frames ────────────────────────────
|
// ── Pre-roll + A/V alignment ─────────────────────────────────────────────
|
||||||
if (bridgeProcess && (sourceType === 'deltacast' || sourceType === 'blackmagic' || sourceType === 'sdi')) {
|
// 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`);
|
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));
|
await new Promise(r => setTimeout(r, PRE_ROLL_SECONDS * 1000));
|
||||||
bridgeProcess.stdout.removeAllListeners('data');
|
bridgeProcess.stdout.removeAllListeners('data');
|
||||||
console.log(`[capture] pre-roll complete.`);
|
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 startedAt = new Date().toISOString();
|
||||||
const recordingStartedAt = Date.now();
|
const recordingStartedAt = Date.now();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue