// modal-new-recorder.jsx — New Recorder dialog (SRT / RTMP / SDI) function ProbeResult({ result }) { if (!result.ok) { return (
Probe failed: {result.error}
); } const d = result.data || {}; const entries = Object.entries(d).filter(([, v]) => v !== null && typeof v !== 'object'); if (entries.length === 0) { return (
✓ Source reachable
); } return (
{entries.map(([k, v]) => (
{k} {String(v)}
))}
); } function NewRecorderModal({ open, onClose }) { const PROJECTS = window.ZAMPP_DATA?.PROJECTS || []; const NODES = window.ZAMPP_DATA?.NODES || []; const [name, setName] = React.useState(''); const [sourceType, setSourceType] = React.useState('SRT'); const [srtUrl, setSrtUrl] = React.useState('srt://10.0.4.18:4200'); const [rtmpUrl, setRtmpUrl] = React.useState('rtmp://stream.local/live/cam_a'); const [sdiDeviceIdx, setSdiDeviceIdx] = React.useState(0); const [sdiNodeId, setSdiNodeId] = React.useState(() => { const n = NODES[0]; return n ? (n.id || n.hostname || '') : ''; }); const [sdiDevices, setSdiDevices] = React.useState(null); const [recTab, setRecTab] = React.useState('video'); const [recCodec, setRecCodec] = React.useState('prores_hq'); const [recContainer, setRecContainer] = React.useState('mov'); const [proxyOn, setProxyOn] = React.useState(true); const [projectId, setProjectId] = React.useState(PROJECTS[0]?.id || ''); const [submitting, setSubmitting] = React.useState(false); const [submitErr, setSubmitErr] = React.useState(null); const [probing, setProbing] = React.useState(false); const [probeResult, setProbeResult] = React.useState(null); React.useEffect(() => { if (sourceType !== 'SDI' || sdiDevices !== null) return; window.ZAMPP_API.fetch('/cluster/devices/blackmagic') .then(d => setSdiDevices(Array.isArray(d) ? d : [])) .catch(() => setSdiDevices([])); }, [sourceType]); React.useEffect(() => { setProbeResult(null); }, [sourceType, srtUrl, rtmpUrl]); const handleProbe = () => { setProbing(true); setProbeResult(null); const body = sourceType === 'SRT' ? { source_type: 'srt', url: srtUrl } : { source_type: 'rtmp', url: rtmpUrl }; window.ZAMPP_API.fetch('/recorders/probe', { method: 'POST', body: JSON.stringify(body) }) .then(r => { setProbing(false); setProbeResult({ ok: true, data: r }); }) .catch(e => { setProbing(false); setProbeResult({ ok: false, error: e.message }); }); }; const handleCreate = () => { if (!name.trim()) { setSubmitErr('Recorder name is required.'); return; } if (sourceType === 'SDI' && !sdiNodeId) { setSubmitErr('Select a capture node for SDI.'); return; } setSubmitting(true); setSubmitErr(null); const body = { name: name.trim(), source_type: sourceType.toLowerCase(), project_id: projectId || undefined, generate_proxy: proxyOn, recording_codec: recCodec, recording_container: recContainer, }; if (sourceType === 'SRT') { body.source_config = { url: srtUrl }; } else if (sourceType === 'RTMP') { body.source_config = { url: rtmpUrl }; } else { // SDI: device_index and node_id are top-level fields body.source_config = {}; body.device_index = sdiDeviceIdx; body.node_id = sdiNodeId || undefined; } window.ZAMPP_API.fetch('/recorders', { method: 'POST', body: JSON.stringify(body) }) .then(() => { setSubmitting(false); // Recorders list listens for this and re-fetches; otherwise the // operator has to wait for the next 10s poll tick to see the new row. window.dispatchEvent(new CustomEvent('df:recorders-changed')); onClose(); }) .catch(e => { setSubmitting(false); setSubmitErr(e.message || 'Failed to create recorder'); }); }; if (!open) return null; return (
e.stopPropagation()}>
New recorder
Configure source, codec, and destination
setName(e.target.value)} />
{[ { id: 'SRT', label: 'SRT', desc: 'Secure Reliable Transport — pull caller', icon: 'signal' }, { id: 'RTMP', label: 'RTMP', desc: 'Real-Time Messaging Protocol', icon: 'globe' }, { id: 'SDI', label: 'SDI', desc: 'Blackmagic DeckLink hardware', icon: 'video' }, ].map(t => ( ))}
{sourceType === 'SRT' && (
setSrtUrl(e.target.value)} style={{ flex: 1 }} />
Recorder connects out to this URL (caller mode).
{probeResult && }
)} {sourceType === 'RTMP' && (
setRtmpUrl(e.target.value)} style={{ flex: 1 }} />
Recorder pulls this RTMP stream.
{probeResult && }
)} {sourceType === 'SDI' && (
{sdiDevices === null && (
Detecting DeckLink devices…
)} {sdiDevices !== null && sdiDevices.length > 0 && (
{sdiDevices.map((dev, di) => (
{(dev.model || dev.device || 'DeckLink').toUpperCase()} · {dev.hostname}
{Array.from({ length: dev.port_count || 4 }, (_, i) => i).map(idx => ( ))}
))}
)} {sdiDevices !== null && sdiDevices.length === 0 && (
No DeckLink devices auto-detected. Configure manually:
)}
)}
Master recording
{['video', 'audio', 'container'].map(t => ( ))}
{recTab === 'video' && (
)} {recTab === 'audio' && (
)} {recTab === 'container' && (
)}
Generate proxy
SDI sources record proxy in parallel. Network sources generate proxy after stop.
{proxyOn && (
Proxy
{['H.264', '2 Mbps', 'MP4', '1920×1080', 'AAC 128 kbps'].map(tag => ( {tag} ))}
Fixed proxy profile — not configurable.
)}
Destination
{submitErr && (
{submitErr}
)}
); } window.NewRecorderModal = NewRecorderModal;