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++)
|
for (int g = 0; g < GROUPS; g++)
|
||||||
ai.pAudioGroups[g].pAudioChannels[0].DataSize = (ULONG)gbuf_sz;
|
ai.pAudioGroups[g].pAudioChannels[0].DataSize = (ULONG)gbuf_sz;
|
||||||
if (VHD_SlotExtractAudio(slot, &ai) == VHDERR_NOERROR) {
|
if (VHD_SlotExtractAudio(slot, &ai) == VHDERR_NOERROR) {
|
||||||
/* Frames present = bytes from the most-populated group
|
/* Group 0 is the AUTHORITATIVE sample count — it paces the
|
||||||
* divided by one stereo frame (4 bytes). Real SDI audio
|
* audio timeline in lockstep with video (same SDI slot
|
||||||
* is sample-aligned across groups; using the max keeps
|
* clock), exactly as the original 2ch path did. We must
|
||||||
* us robust if a quiet group returns fewer bytes. */
|
* 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;
|
ULONG g0 = ai.pAudioGroups[0].pAudioChannels[0].DataSize;
|
||||||
size_t frames = (size_t)g0 / 4; /* 2ch * s16 = 4 bytes/frame */
|
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) {
|
if (frames > 0) {
|
||||||
size_t need = frames * FRAME_BYTES;
|
size_t need = frames * FRAME_BYTES;
|
||||||
if (need > out_cap) { frames = out_cap / FRAME_BYTES; 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;
|
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) {
|
for (const p of ports) {
|
||||||
// INSERT … ON CONFLICT DO NOTHING: create-once. Never overwrite an existing
|
// INSERT … ON CONFLICT DO NOTHING: create-once. Never overwrite an existing
|
||||||
// row (preserves label, enabled, codec config, status). source_config keeps
|
// row (preserves label, enabled, codec config, status). source_config keeps
|
||||||
|
|
@ -285,8 +292,9 @@ async function reconcileRecordersForNode(node) {
|
||||||
: { device: p.device_index };
|
: { device: p.device_index };
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`INSERT INTO recorders
|
`INSERT INTO recorders
|
||||||
(node_id, device_index, source_type, source_config, name, enabled, auto_provisioned)
|
(node_id, device_index, source_type, source_config, name, enabled, auto_provisioned,
|
||||||
VALUES ($1, $2, $3::source_type, $4, $5, false, true)
|
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
|
ON CONFLICT (node_id, device_index) WHERE node_id IS NOT NULL AND device_index IS NOT NULL
|
||||||
DO NOTHING`,
|
DO NOTHING`,
|
||||||
[
|
[
|
||||||
|
|
@ -296,6 +304,7 @@ async function reconcileRecordersForNode(node) {
|
||||||
JSON.stringify(srcCfg),
|
JSON.stringify(srcCfg),
|
||||||
// Deterministic hardware name; the operator can set a friendly `label`.
|
// Deterministic hardware name; the operator can set a friendly `label`.
|
||||||
`${node.hostname}-${p.source_type === 'deltacast' ? 'dc' : 'bmd'}${p.device_index}`,
|
`${node.hostname}-${p.source_type === 'deltacast' ? 'dc' : 'bmd'}${p.device_index}`,
|
||||||
|
defaultCodec,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue