Merge remote-tracking branch 'wilddragon/feat/recorder-codec-bitrate' into integrate
This commit is contained in:
commit
4473427515
2 changed files with 58 additions and 23 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
|||
<div className="field">
|
||||
<label className="field-label">Video codec</label>
|
||||
<select className="field-input" value={recCodec} onChange={e => setRecCodec(e.target.value)} style={{ appearance: 'auto' }}>
|
||||
<option value="prores_4444xq">ProRes 4444 XQ</option>
|
||||
<option value="prores_4444">ProRes 4444</option>
|
||||
<option value="prores_hq">ProRes 422 HQ</option>
|
||||
<option value="hevc_nvenc">All-Intra HEVC (NVENC) — GPU, growing</option>
|
||||
<option value="h264_nvenc">H.264 (NVENC) — GPU</option>
|
||||
<option value="prores_hq">ProRes 422 HQ — 4:2:2 CPU</option>
|
||||
<option value="prores">ProRes 422</option>
|
||||
<option value="prores_lt">ProRes 422 LT</option>
|
||||
<option value="prores_proxy">ProRes 422 Proxy</option>
|
||||
<option value="libx264">H.264 (x264)</option>
|
||||
<option value="libx265">H.265 / HEVC (x265)</option>
|
||||
<option value="dnxhd">DNxHD 185x</option>
|
||||
<option value="dnxhr_hq">DNxHR HQ</option>
|
||||
<option value="xdcam_hd422">XDCAM HD422</option>
|
||||
<option value="libx264">H.264 (x264, CPU)</option>
|
||||
<option value="libx265">H.265 (x265, CPU)</option>
|
||||
</select>
|
||||
</div>
|
||||
<Field label="Resolution" value="Source (native)" select />
|
||||
<Field label="Color space" value="Rec. 709" select />
|
||||
<Field label="Bit depth" value="10-bit" select />
|
||||
{codecUsesBitrate ? (
|
||||
<div className="field">
|
||||
<label className="field-label">Target bitrate (Mbps)</label>
|
||||
<input
|
||||
className="field-input"
|
||||
type="number" min="1" max="400" step="1"
|
||||
value={recBitrate}
|
||||
onChange={e => setRecBitrate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Field label="Bitrate" value="Quality-based (profile)" select />
|
||||
)}
|
||||
<Field label="Resolution" value="Auto — from source" select />
|
||||
<Field label="Framerate" value="Auto — from source" select />
|
||||
{/* #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 (
|
||||
<div style={{ gridColumn: '1 / -1', fontSize: 11.5, color: 'var(--warn, #d9a441)', border: '1px solid var(--warn, #d9a441)', borderRadius: 6, padding: '8px 10px', background: 'rgba(217,164,65,0.08)' }}>
|
||||
⚠ Target {cfg} Mbps exceeds the source stream (~{srcMbps.toFixed(1)} Mbps). Encoding above the source bitrate increases file size without adding quality.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{recTab === 'audio' && (
|
||||
|
|
@ -410,16 +453,8 @@ function NewRecorderModal({ open, onClose }) {
|
|||
)}
|
||||
{recTab === 'container' && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
||||
<div className="field">
|
||||
<label className="field-label">Container</label>
|
||||
<select className="field-input" value={recContainer} onChange={e => setRecContainer(e.target.value)} style={{ appearance: 'auto' }}>
|
||||
<option value="mov">MOV (QuickTime)</option>
|
||||
<option value="mxf">MXF (SMPTE)</option>
|
||||
<option value="mkv">MKV (Matroska)</option>
|
||||
<option value="mp4">MP4</option>
|
||||
</select>
|
||||
</div>
|
||||
<Field label="Segment" value="None (single file)" select />
|
||||
<Field label="Container" value={recContainer === 'mp4' ? 'MP4 (auto)' : 'Fragmented MOV (auto)'} select />
|
||||
<Field label="Growing-file" value={recContainer === 'mov' ? 'Supported (edit-while-record)' : 'No'} select />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue