From 07eea0210978d4167e5a4b016153e0a46d091597 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Thu, 4 Jun 2026 04:14:59 +0000 Subject: [PATCH] fix(capture): restore audio wallclock (throughput) + remove CPU codec options - restore -use_wallclock_as_timestamps on audio input: without it ffmpeg's raw s16le reader stalled the graph (NVENC idle at 9%, ~half frames dropped). With it + long-GOP HEVC the encoder runs realtime and A/V length stays locked. - remove all CPU codec options (prores*, dnxh*, libx264/265) from recorder UI; GPU NVENC only (hevc_nvenc / h264_nvenc). 3x L4 cluster, no reason for CPU. - GPU codec defaults in env builders + proxy default h264_nvenc. --- services/capture/src/capture-manager.js | 20 ++++++++++--------- services/mam-api/src/routes/recorders.js | 16 +++++++-------- services/web-ui/public/modal-new-recorder.jsx | 10 +--------- services/web-ui/public/screens-ingest.jsx | 9 +-------- 4 files changed, 21 insertions(+), 34 deletions(-) diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index 92220a6..9e77cdc 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -737,18 +737,20 @@ class CaptureManager { '-video_size', fcSize, '-framerate', fcFps, '-i', 'pipe:0', - // Audio FIFO → ffmpeg input 1. The bridge writes EXACTLY the SDI-clock - // paced samples (group 0 is the reference, same slot clock as video), - // so we DERIVE audio PTS from the sample count at 48 kHz — NOT from - // wall-clock arrival. Wall-clock timestamping made the audio stream's - // length equal real elapsed time while video length = frame_count/fps; - // when the encoder ran a hair under realtime the audio ended up ~1% - // longer than video (heard as a pitch-up). Reading the raw stream at - // its natural rate keeps both in the same SDI clock domain; the - // master-output aresample=async=1 still soaks up any micro-jitter. + // Audio FIFO → ffmpeg input 1. Wall-clock timestamps on the audio + // input are REQUIRED for throughput: without them ffmpeg's audio + // reader has no rate reference on the raw s16le FIFO and the demux + // thread stalls the whole graph (NVENC sat idle at 9% while frames + // dropped). With wallclock, audio is paced by arrival and the master + // -af aresample=async=1 resamples it onto the video CFR timeline so + // A/V length stays locked. The residual ~1% drift that wallclock used + // to cause was actually the all-intra HEVC dropping frames (video + // short); that's fixed by long-GOP HEVC for non-growing records, so + // wallclock is safe again and necessary. // The FIFO carries the full 16ch the bridge publishes; channel // SELECTION (keep first N) is applied as an output filter so the // discrete broadcast channels are preserved, not downmixed. + '-use_wallclock_as_timestamps', '1', '-thread_queue_size', '512', '-f', 's16le', '-ar', '48000', diff --git a/services/mam-api/src/routes/recorders.js b/services/mam-api/src/routes/recorders.js index afce8fd..65073e4 100644 --- a/services/mam-api/src/routes/recorders.js +++ b/services/mam-api/src/routes/recorders.js @@ -198,7 +198,7 @@ function validateRecorderConfig(cfg, nodeHasGpu = null) { // NVENC requires a GPU on the target node. Only a hard error when we know the // node lacks one; unknown capability is left as a soft pass. if (GPU_CODECS.includes(codec) && nodeHasGpu === false) { - return `Invalid combo: codec ${cfg.recording_codec} requires an NVIDIA GPU, but the target node reports no GPU. Choose a software codec (e.g. prores_hq, dnxhr_hq, h264) or assign a GPU node.`; + return `Invalid combo: codec ${cfg.recording_codec} requires an NVIDIA GPU, but the target node reports no GPU. Assign this recorder to a GPU node.`; } return null; @@ -253,7 +253,7 @@ function buildStandbyEnv(recorder) { `SOURCE_TYPE=${recorder.source_type}`, `SOURCE_CONFIG=${JSON.stringify(sourceConfig)}`, `DEVICE_INDEX=${deviceIndex}`, - `RECORDING_CODEC=${recorder.recording_codec || 'prores_hq'}`, + `RECORDING_CODEC=${recorder.recording_codec || 'hevc_nvenc'}`, `RECORDING_RESOLUTION=${recorder.recording_resolution || 'native'}`, `RECORDING_VIDEO_BITRATE=${recorder.recording_video_bitrate || ''}`, `RECORDING_FRAMERATE=${recorder.recording_framerate || ''}`, @@ -262,7 +262,7 @@ function buildStandbyEnv(recorder) { `RECORDING_AUDIO_CHANNELS=${recorder.recording_audio_channels ?? 2}`, `RECORDING_CONTAINER=${recorder.recording_container || 'mov'}`, `PROXY_ENABLED=${recorder.proxy_enabled !== false ? 'true' : 'false'}`, - `PROXY_CODEC=${recorder.proxy_codec || 'h264'}`, + `PROXY_CODEC=${recorder.proxy_codec || 'h264_nvenc'}`, `PROXY_RESOLUTION=${recorder.proxy_resolution || '1920x1080'}`, `PROXY_VIDEO_BITRATE=${recorder.proxy_video_bitrate || '2M'}`, `PROXY_FRAMERATE=${recorder.proxy_framerate || ''}`, @@ -468,9 +468,9 @@ router.post('/', async (req, res, next) => { recording_audio_codec: 'pcm_s24le', recording_audio_channels: 2, recording_container: 'mov', - proxy_enabled: true, - proxy_codec: 'h264', - proxy_resolution: '1920x1080', + proxy_enabled: true, + proxy_codec: 'h264_nvenc', + proxy_resolution: '1920x1080', proxy_video_bitrate: '2M', proxy_audio_codec: 'aac', proxy_audio_bitrate: '128k', @@ -793,7 +793,7 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => { `DEVICE_INDEX=${deviceIndex}`, // Recording codec controls - `RECORDING_CODEC=${recorder.recording_codec || 'prores_hq'}`, + `RECORDING_CODEC=${recorder.recording_codec || 'hevc_nvenc'}`, `RECORDING_RESOLUTION=${recorder.recording_resolution || 'native'}`, `RECORDING_VIDEO_BITRATE=${recorder.recording_video_bitrate || ''}`, `RECORDING_FRAMERATE=${recorder.recording_framerate || ''}`, @@ -804,7 +804,7 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => { // Proxy codec controls `PROXY_ENABLED=${recorder.proxy_enabled !== false ? 'true' : 'false'}`, - `PROXY_CODEC=${recorder.proxy_codec || 'h264'}`, + `PROXY_CODEC=${recorder.proxy_codec || 'h264_nvenc'}`, `PROXY_RESOLUTION=${recorder.proxy_resolution || '1920x1080'}`, `PROXY_VIDEO_BITRATE=${recorder.proxy_video_bitrate || '2M'}`, `PROXY_FRAMERATE=${recorder.proxy_framerate || ''}`, diff --git a/services/web-ui/public/modal-new-recorder.jsx b/services/web-ui/public/modal-new-recorder.jsx index e3eeffe..32ad08f 100644 --- a/services/web-ui/public/modal-new-recorder.jsx +++ b/services/web-ui/public/modal-new-recorder.jsx @@ -418,7 +418,6 @@ function NewRecorderModal({ open, onClose }) { {[ { id: 'hevc', label: 'HEVC Master (MOV)', codec: 'hevc_nvenc', bitrate: '25' }, { id: 'h264', label: 'H.264 Proxy-friendly (MP4)', codec: 'h264_nvenc', bitrate: '25' }, - { id: 'dnxhr', label: 'DNxHR HQ (MOV)', codec: 'dnxhr_hq', bitrate: '145' }, ].map(p => (