diff --git a/services/web-ui/public/modal-new-recorder.jsx b/services/web-ui/public/modal-new-recorder.jsx
index 3304ddf..95a37ef 100644
--- a/services/web-ui/public/modal-new-recorder.jsx
+++ b/services/web-ui/public/modal-new-recorder.jsx
@@ -1,14 +1,44 @@
// modal-new-recorder.jsx — New Recorder dialog (SRT / RTMP / SDI)
-const { SDI_PORTS_zampp2, PROJECTS: ALL_PROJECTS } = window.ZAMPP_DATA;
-
function NewRecorderModal({ open, onClose }) {
- const [name, setName] = React.useState("");
- const [sourceType, setSourceType] = React.useState("SRT");
+ const { PROJECTS } = window.ZAMPP_DATA;
+ 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 [sdiPort, setSdiPort] = React.useState(1);
- const [recTab, setRecTab] = React.useState("video");
- const [proxyTab, setProxyTab] = React.useState("video");
+ const [sdiDevices, setSdiDevices] = React.useState(null);
+ const [recTab, setRecTab] = React.useState('video');
+ const [proxyTab, setProxyTab] = React.useState('video');
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);
+
+ 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]);
+
+ const handleCreate = () => {
+ if (!name.trim()) { setSubmitErr('Recorder name is required.'); return; }
+ setSubmitting(true);
+ setSubmitErr(null);
+ const body = {
+ name: name.trim(),
+ source_type: sourceType.toLowerCase(),
+ source_config: sourceType === 'SRT' ? { url: srtUrl }
+ : sourceType === 'RTMP' ? { url: rtmpUrl }
+ : { port: sdiPort },
+ project_id: projectId || undefined,
+ generate_proxy: proxyOn,
+ };
+ window.ZAMPP_API.fetch('/recorders', { method: 'POST', body: JSON.stringify(body) })
+ .then(() => { setSubmitting(false); onClose(); })
+ .catch(e => { setSubmitting(false); setSubmitErr(e.message || 'Failed to create recorder'); });
+ };
if (!open) return null;
@@ -18,7 +48,7 @@ function NewRecorderModal({ open, onClose }) {
New recorder
-
Configure source, codec, and destination
+
Configure source, codec, and destination
@@ -33,61 +63,78 @@ function NewRecorderModal({ open, onClose }) {
{[
- { id: "SRT", label: "SRT", desc: "Secure Reliable Transport — pull caller" },
- { id: "RTMP", label: "RTMP", desc: "Real-Time Messaging Protocol" },
- { id: "SDI", label: "SDI", desc: "Blackmagic DeckLink hardware" },
+ { 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" && (
+ {sourceType === 'SRT' && (
-
-
- The recorder connects out to this URL (caller mode).
?mode=caller is appended automatically.
+
setSrtUrl(e.target.value)} />
+
+ The recorder connects out to this URL (caller mode).
)}
- {sourceType === "RTMP" && (
+ {sourceType === 'RTMP' && (
-
-
- The recorder will pull this RTMP stream. Must be an existing published stream.
+
setRtmpUrl(e.target.value)} />
+
+ The recorder will pull this RTMP stream.
)}
- {sourceType === "SDI" && (
- <>
-
-
-
-
zampp2 · DeckLink Duo 2 · 4 ports
-
+ {sourceType === 'SDI' && (
+
+
+ {sdiDevices === null && (
+
Loading DeckLink devices…
+ )}
+ {sdiDevices !== null && sdiDevices.length === 0 && (
+
+ No DeckLink devices found in cluster. Ensure the capture node is online.
-
-
-
-
-
- >
+ )}
+ {sdiDevices !== null && sdiDevices.length > 0 && (
+
+ {sdiDevices.map((dev, di) => (
+
+
{(dev.model || dev.device || 'DECKLINK').toUpperCase()} · {dev.hostname}
+ {(dev.ports || Array.from({ length: dev.port_count || 2 }, (_, i) => ({ idx: i + 1, label: 'SDI ' + (i + 1) }))).map(p => (
+
+ ))}
+
+ ))}
+
+ )}
+
)}
@@ -95,32 +142,32 @@ function NewRecorderModal({ open, onClose }) {
Master recording
- {["video", "audio", "container"].map(t => (
-
- {recTab === "video" && (
-
+ {recTab === 'video' && (
+
)}
- {recTab === "audio" && (
-
+ {recTab === 'audio' && (
+
)}
- {recTab === "container" && (
-
+ {recTab === 'container' && (
+
@@ -135,8 +182,8 @@ function NewRecorderModal({ open, onClose }) {
Generate proxy
-
- SDI sources record proxy in parallel. Network sources (SRT/RTMP) generate proxy after stop.
+
+ SDI sources record proxy in parallel. Network sources generate proxy after stop.
@@ -147,29 +194,29 @@ function NewRecorderModal({ open, onClose }) {
Proxy
- {["video", "audio", "container"].map(t => (
- setProxyTab(t)}>
+ {['video', 'audio', 'container'].map(t => (
+ setProxyTab(t)}>
{t[0].toUpperCase() + t.slice(1)}
))}
- {proxyTab === "video" && (
-
+ {proxyTab === 'video' && (
+
)}
- {proxyTab === "audio" && (
-
+ {proxyTab === 'audio' && (
+
)}
- {proxyTab === "container" && (
+ {proxyTab === 'container' && (
)}
@@ -179,42 +226,36 @@ function NewRecorderModal({ open, onClose }) {
Destination
-
-
-
+
+
+
+
+ {submitErr && (
+
+ {submitErr}
+
+ )}
Cancel
- Probe source
- Create recorder
-
-
-
- );
-}
-
-function SDIPortMini({ ports, selected, onSelect }) {
- return (
-
-
-
- {ports.map(p => (
- onSelect(p.idx)}>
-
-
-
- SDI {p.idx}
- {p.label}
- {p.active && {p.signal}}
+ {
+ const url = sourceType === 'SRT' ? srtUrl : sourceType === 'RTMP' ? rtmpUrl : null;
+ if (url) window.ZAMPP_API.fetch('/recorders/probe', { method: 'POST', body: JSON.stringify({ url, source_type: sourceType.toLowerCase() }) }).catch(() => {});
+ }}>Probe source
+
+ {submitting ? 'Creating…' : 'Create recorder'}
- ))}
+
);