From 51f939b1fe40a73dee5c4e1befd96f92818cb672 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Thu, 4 Jun 2026 04:01:25 +0000 Subject: [PATCH] fix(deltacast-bridge): use group-0 sample count as authoritative audio length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Taking the MAX sample count across the 4 audio groups could emit more audio frames per slot than group 0 (the SDI-clock reference), drifting the audio stream slightly longer than video — heard as a ~1% pitch-up. Group 0 paces the timeline exactly as the original 2ch path did; shorter groups are silence-padded to its length, never extending it. --- services/capture/deltacast-bridge/main.c | 17 +++++++++-------- services/mam-api/src/routes/cluster.js | 13 +++++++++++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/services/capture/deltacast-bridge/main.c b/services/capture/deltacast-bridge/main.c index 2a4076e..8cd1fc2 100644 --- a/services/capture/deltacast-bridge/main.c +++ b/services/capture/deltacast-bridge/main.c @@ -322,16 +322,17 @@ static void *audio_thread(void *arg) { for (int g = 0; g < GROUPS; g++) ai.pAudioGroups[g].pAudioChannels[0].DataSize = (ULONG)gbuf_sz; if (VHD_SlotExtractAudio(slot, &ai) == VHDERR_NOERROR) { - /* Frames present = bytes from the most-populated group - * divided by one stereo frame (4 bytes). Real SDI audio - * is sample-aligned across groups; using the max keeps - * us robust if a quiet group returns fewer bytes. */ + /* Group 0 is the AUTHORITATIVE sample count — it paces the + * audio timeline in lockstep with video (same SDI slot + * clock), exactly as the original 2ch path did. We must + * emit EXACTLY group 0's frame count per slot; taking a + * max across groups would occasionally emit extra frames + * and make the audio stream drift LONGER than the video + * (heard as a slight pitch-up). Groups 1-3 are sampled at + * the same rate; any that return fewer bytes are padded + * with silence to group 0's length, never extending it. */ ULONG g0 = ai.pAudioGroups[0].pAudioChannels[0].DataSize; size_t frames = (size_t)g0 / 4; /* 2ch * s16 = 4 bytes/frame */ - for (int g = 1; g < GROUPS; g++) { - size_t gf = (size_t)ai.pAudioGroups[g].pAudioChannels[0].DataSize / 4; - if (gf > frames) frames = gf; - } if (frames > 0) { size_t need = frames * FRAME_BYTES; if (need > out_cap) { frames = out_cap / FRAME_BYTES; need = frames * FRAME_BYTES; } diff --git a/services/mam-api/src/routes/cluster.js b/services/mam-api/src/routes/cluster.js index 1e45fb7..17f1ea2 100644 --- a/services/mam-api/src/routes/cluster.js +++ b/services/mam-api/src/routes/cluster.js @@ -276,6 +276,13 @@ async function reconcileRecordersForNode(node) { } if (ports.length === 0) return; + // Default master codec for newly-discovered ports. SDI capture at 1080p59.94 + // CANNOT be encoded in realtime on CPU (ProRes/x264 fall behind → dropped + // frames → short, fast-playing files). Nodes with an NVENC-capable GPU default + // to GPU HEVC; only GPU-less nodes fall back to CPU ProRes. + const hasGpu = Array.isArray(cap.gpus) && cap.gpus.length > 0; + const defaultCodec = hasGpu ? 'hevc_nvenc' : 'prores_hq'; + for (const p of ports) { // INSERT … ON CONFLICT DO NOTHING: create-once. Never overwrite an existing // row (preserves label, enabled, codec config, status). source_config keeps @@ -285,8 +292,9 @@ async function reconcileRecordersForNode(node) { : { device: p.device_index }; await pool.query( `INSERT INTO recorders - (node_id, device_index, source_type, source_config, name, enabled, auto_provisioned) - VALUES ($1, $2, $3::source_type, $4, $5, false, true) + (node_id, device_index, source_type, source_config, name, enabled, auto_provisioned, + recording_codec, recording_container, recording_video_bitrate, recording_audio_channels) + VALUES ($1, $2, $3::source_type, $4, $5, false, true, $6, 'mov', '25M', 2) ON CONFLICT (node_id, device_index) WHERE node_id IS NOT NULL AND device_index IS NOT NULL DO NOTHING`, [ @@ -296,6 +304,7 @@ async function reconcileRecordersForNode(node) { JSON.stringify(srcCfg), // Deterministic hardware name; the operator can set a friendly `label`. `${node.hostname}-${p.source_type === 'deltacast' ? 'dc' : 'bmd'}${p.device_index}`, + defaultCodec, ] ); }