fix(deltacast-bridge): use group-0 sample count as authoritative audio length
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.
This commit is contained in:
parent
095306d9cf
commit
51f939b1fe
2 changed files with 20 additions and 10 deletions
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue