From 9b4725038864078aba87d5560137cba9f1d90d29 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Fri, 29 May 2026 17:04:00 -0400 Subject: [PATCH] feat(recorder): default All-Intra HEVC (NVENC) + custom bitrate, auto fps/res, source-bitrate warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2 Recorder codec/bitrate: - Default recorder codec → hevc_nvenc (All-Intra HEVC NVENC); ProRes/H.264/DNxHR still selectable. recorders.js default flips prores_hq → hevc_nvenc. - Custom target bitrate (Mbps) input, shown only for bitrate-controlled codecs (NVENC/x264/x265/DNxHD); ProRes shows quality-based (no bitrate). - Framerate + resolution are auto-detected from source (manual fields removed). - Container derived from codec (HEVC/ProRes/DNxHR → fragmented MOV, H.264 → MP4); drops the stub container picker (closes #150 direction). #3 SRT/RTMP customization + bitrate warning: - Same codec/bitrate/auto controls apply to network recorders (shared form). - Warns in the modal when the configured target bitrate exceeds the probed source stream bitrate (via /recorders/probe) — re-encoding above source adds storage, not quality. Co-Authored-By: Claude Opus 4.8 --- services/mam-api/src/routes/recorders.js | 2 +- services/web-ui/public/modal-new-recorder.jsx | 79 +++++++++++++------ 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/services/mam-api/src/routes/recorders.js b/services/mam-api/src/routes/recorders.js index cf6bff3..316d39e 100644 --- a/services/mam-api/src/routes/recorders.js +++ b/services/mam-api/src/routes/recorders.js @@ -197,7 +197,7 @@ router.post('/', async (req, res, next) => { // Defaults — written on insert so the DB row is always self-contained. const defaults = { source_config: {}, - recording_codec: 'prores_hq', + recording_codec: 'hevc_nvenc', recording_resolution: 'native', recording_audio_codec: 'pcm_s24le', recording_audio_channels: 2, diff --git a/services/web-ui/public/modal-new-recorder.jsx b/services/web-ui/public/modal-new-recorder.jsx index ba4041a..fa341c7 100644 --- a/services/web-ui/public/modal-new-recorder.jsx +++ b/services/web-ui/public/modal-new-recorder.jsx @@ -148,8 +148,18 @@ function NewRecorderModal({ open, onClose }) { }); const [dcDevices, setDcDevices] = React.useState(null); const [recTab, setRecTab] = React.useState('video'); - const [recCodec, setRecCodec] = React.useState('prores_hq'); - const [recContainer, setRecContainer] = React.useState('mov'); + // All-Intra HEVC (NVENC) is the default master — GPU-encoded, growing-file + // capable in fragmented MOV. ProRes stays selectable for 4:2:2 mezzanine. + const [recCodec, setRecCodec] = React.useState('hevc_nvenc'); + // Custom target bitrate in Mbps for bitrate-controlled codecs (NVENC / x264 / + // x265 / DNxHD). ProRes ignores bitrate (quality is profile-driven). + const [recBitrate, setRecBitrate] = React.useState('60'); + // Container is derived from the codec, not manually chosen: HEVC/ProRes/DNxHR + // → MOV (fragmented, growing-capable); H.264 → MP4. + const recContainer = (recCodec === 'h264' || recCodec === 'h264_nvenc' || recCodec === 'libx264') ? 'mp4' : 'mov'; + // Codecs whose bitrate is operator-controlled (everything except ProRes). + const BITRATE_CODECS = new Set(['hevc_nvenc', 'h264_nvenc', 'libx264', 'libx265', 'dnxhd', 'dnxhr_hq']); + const codecUsesBitrate = BITRATE_CODECS.has(recCodec); const [proxyOn, setProxyOn] = React.useState(true); const [projectId, setProjectId] = React.useState(PROJECTS[0]?.id || ''); const [submitting, setSubmitting] = React.useState(false); @@ -198,7 +208,14 @@ function NewRecorderModal({ open, onClose }) { generate_proxy: proxyOn, recording_codec: recCodec, recording_container: recContainer, + // Framerate + resolution are auto-detected from the source signal/stream. + recording_framerate: '', // empty = match source + recording_resolution: 'native', }; + // Custom bitrate only applies to bitrate-controlled codecs (ProRes ignores it). + if (codecUsesBitrate && recBitrate) { + body.recording_video_bitrate = `${recBitrate}M`; + } if (sourceType === 'SRT') { body.source_config = { url: srtUrl }; @@ -382,22 +399,48 @@ function NewRecorderModal({ open, onClose }) {
- - - + {codecUsesBitrate ? ( +
+ + setRecBitrate(e.target.value)} + /> +
+ ) : ( + + )} + + + {/* #3: warn when the configured bitrate exceeds the probed source + bitrate — re-encoding above source adds storage, not quality. */} + {codecUsesBitrate && (() => { + const d = probeResult && probeResult.ok ? (probeResult.data || {}) : null; + const raw = d && (d.bitrate ?? d.bit_rate ?? d.video_bitrate ?? (d.video && d.video.bit_rate)); + const srcMbps = raw ? (Number(raw) > 100000 ? Number(raw) / 1e6 : Number(raw)) : null; + const cfg = parseFloat(recBitrate); + if (srcMbps && cfg && cfg > srcMbps * 1.05) { + return ( +
+ ⚠ Target {cfg} Mbps exceeds the source stream (~{srcMbps.toFixed(1)} Mbps). Encoding above the source bitrate increases file size without adding quality. +
+ ); + } + return null; + })()} )} {recTab === 'audio' && ( @@ -410,16 +453,8 @@ function NewRecorderModal({ open, onClose }) { )} {recTab === 'container' && (
-
- - -
- + +
)}