From c40de38c453732b6b183d5fb57b331a8b8fc761a Mon Sep 17 00:00:00 2001 From: OpenCode Date: Fri, 5 Jun 2026 05:27:27 +0000 Subject: [PATCH] feat(capture): AUDIO_OFFSET_MS knob for fixed A/V alignment trim The deltacast bridge captures audio and video on separate VHD streams; any constant capture-path latency difference shows as a fixed A/V offset (e.g. audio slightly ahead of video) even though stream lengths stay locked (no drift, verified ~1 frame over 461s). AUDIO_OFFSET_MS applies an -itsoffset on the SDI/Deltacast audio input only: positive DELAYS audio (audio-ahead case), negative advances it. Default 0 = no change, fully non-destructive, clamped to +/-1000ms. Lets an operator dial out residual offset with a lipsync loop without a code change. --- services/capture/src/capture-manager.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index e12b2a2..01ada3b 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -197,6 +197,21 @@ const CAPTURE_GPU_INDEX = (() => { // as an encoder option right after -c:v. Returns [] when no pin is configured. const nvencGpuSel = () => (CAPTURE_GPU_INDEX != null ? ['-gpu', String(CAPTURE_GPU_INDEX)] : []); +// Optional fixed A/V alignment trim for the SDI/Deltacast audio input. The +// deltacast bridge captures audio and video on separate VHD streams; any +// constant capture-path latency difference between them shows as a fixed A/V +// offset (e.g. audio slightly ahead of video) even though stream LENGTHS stay +// locked (no drift). AUDIO_OFFSET_MS lets an operator dial that out without a +// rebuild: POSITIVE value DELAYS audio (use when audio is AHEAD of video), +// NEGATIVE advances it. Applied as ffmpeg `-itsoffset` on the audio input only. +// Default 0 = no change (fully non-destructive). Range-clamped to ±1000 ms. +const audioOffsetArgs = () => { + const raw = parseFloat(process.env.AUDIO_OFFSET_MS || '0'); + if (!Number.isFinite(raw) || raw === 0) return []; + const ms = Math.max(-1000, Math.min(1000, raw)); + return ['-itsoffset', (ms / 1000).toFixed(4)]; +}; + function hevcNvencArgs(framerate, growing) { const base = ['-c:v', 'hevc_nvenc', ...nvencGpuSel(), '-preset', 'p4', '-rc', 'vbr', '-profile:v', 'main10']; if (growing) { @@ -821,6 +836,8 @@ class CaptureManager { '-f', 's16le', '-ar', '48000', '-ac', '2', + // Optional fixed A/V trim (env AUDIO_OFFSET_MS); default empty = no shift. + ...audioOffsetArgs(), '-i', audioFifoPath, ], isNetwork: false,