diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js
index 5878a24..c14fec1 100644
--- a/services/capture/src/capture-manager.js
+++ b/services/capture/src/capture-manager.js
@@ -720,10 +720,15 @@ class CaptureManager {
'-video_size', fcSize,
'-framerate', fcFps,
'-i', 'pipe:0',
- // Audio FIFO → ffmpeg input 1. The deltacast bridge writes a 2ch s16le
- // 48kHz stream paced by the SDI slot clock (same clock as the video),
- // so wallclock timestamps + master aresample=async=1 keep A/V locked.
- '-use_wallclock_as_timestamps', '1',
+ // Audio FIFO → ffmpeg input 1. The bridge flushes its slot backlog to
+ // the live edge on reader-attach, so the FIFO delivers live audio in
+ // lockstep with the video. We DERIVE audio PTS from the s16le sample
+ // count starting at 0 — the SAME origin as the video's frame-0 PTS —
+ // rather than from wall-clock arrival. Wall-clock stamped the first
+ // audio chunk at a time offset from video frame 0, and the master
+ // aresample then PADDED ~2.5s of leading silence to align them. With
+ // sample-count PTS both streams share one origin → no pad, no leading
+ // silence, length locked.
'-thread_queue_size', '512',
'-f', 's16le',
'-ar', '48000',
diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx
index 6e7ba41..04022d7 100644
--- a/services/web-ui/public/screens-ingest.jsx
+++ b/services/web-ui/public/screens-ingest.jsx
@@ -616,7 +616,6 @@ function RecorderConfigModal({ recorder, onClose, onSaved }) {
const [codec, setCodec] = React.useState(recorder.recording_codec || 'hevc_nvenc');
const [bitrate, setBitrate] = React.useState((recorder.recording_video_bitrate || '25').replace(/M$/i, ''));
const [growing, setGrowing] = React.useState(recorder.growing_enabled === true);
- const [audioCh, setAudioCh] = React.useState(recorder.recording_audio_channels || 2);
const [projectId, setProjectId] = React.useState(recorder.project_id || PROJECTS[0]?.id || '');
const [saving, setSaving] = React.useState(false);
const [err, setErr] = React.useState(null);
@@ -634,7 +633,6 @@ function RecorderConfigModal({ recorder, onClose, onSaved }) {
label: label.trim() || null,
recording_codec: effCodec,
growing_enabled: growing,
- recording_audio_channels: Number(audioCh),
project_id: projectId || null,
};
if (showBitrate && bitrate) body.recording_video_bitrate = String(bitrate).replace(/M$/i, '') + 'M';
@@ -680,41 +678,59 @@ function RecorderConfigModal({ recorder, onClose, onSaved }) {
+ {/* Recording mode — clean segmented control instead of a tickbox. */}
-
-
+
+
+
+
+
+
+ {growing
+ ? 'Writes a growing XDCAM HD422 MXF (OP1a) to the SMB share so editors can cut the clip live in Premiere.'
+ : 'Encodes a GPU master (HEVC/H.264) streamed straight to the library on stop.'}
+
- {showBitrate && (
-
-
- setBitrate(e.target.value)} />
+ {/* Codec + bitrate only apply to Standard mode (growing is fixed XDCAM). */}
+ {!growing && (
+
+
+
+
+
+ {showBitrate && (
+
+
+ setBitrate(e.target.value)} />
+
+ )}
)}
-
-
-
-
- SDI embeds up to 16 channels; the master keeps the first N (discrete, no downmix).
-
-
-
-
-
{err &&
{err}
}
@@ -805,6 +810,10 @@ function Recorders({ navigate, onNew }) {
// Per-recorder config editor (codec / growing / label). Null = closed.
const [configRecorder, setConfigRecorder] = React.useState(null);
+ // Bump when the cluster snapshot updates so the node-grouping re-derives
+ // online/offline state without waiting for the recorder list to change.
+ const [nodesTick, setNodesTick] = React.useState(0);
+
const refresh = React.useCallback(() => {
window.ZAMPP_API.fetch('/recorders')
.then(raw => {
@@ -818,6 +827,18 @@ function Recorders({ navigate, onNew }) {
if (err && err.message && err.message.includes('Unauthenticated')) return;
window.DF_LOG.warn('[recorders] poll error:', err?.message);
});
+ // ALSO refresh the cluster-node snapshot — the recorder page groups by node
+ // and shows each node's online/offline state via ZAMPP_DATA.NODES. Without
+ // this the snapshot goes stale while idling here (nodes wrongly show offline
+ // even though they're heartbeating). Best-effort; failure leaves last-known.
+ window.ZAMPP_API.fetch('/cluster')
+ .then(nodes => {
+ if (Array.isArray(nodes)) {
+ window.ZAMPP_DATA.NODES = nodes;
+ setNodesTick(t => t + 1);
+ }
+ })
+ .catch(() => {});
}, []);
React.useEffect(() => {
@@ -857,7 +878,7 @@ function Recorders({ navigate, onNew }) {
}
out.sort((a, b) => a.meta.hostname.localeCompare(b.meta.hostname));
return out;
- }, [recorders]);
+ }, [recorders, nodesTick]);
return (