feat: add probe button to SRT/RTMP sources, fix node labels
This commit is contained in:
parent
994fd799d0
commit
bb508d3256
1 changed files with 64 additions and 5 deletions
|
|
@ -1,5 +1,34 @@
|
||||||
// modal-new-recorder.jsx — New Recorder dialog (SRT / RTMP / SDI)
|
// modal-new-recorder.jsx — New Recorder dialog (SRT / RTMP / SDI)
|
||||||
|
|
||||||
|
function ProbeResult({ result }) {
|
||||||
|
if (!result.ok) {
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 6, padding: '8px 10px', background: 'var(--danger-soft)', border: '1px solid var(--danger)', borderRadius: 5, fontSize: 11.5, color: 'var(--danger)' }}>
|
||||||
|
Probe failed: {result.error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const d = result.data || {};
|
||||||
|
const entries = Object.entries(d).filter(([, v]) => v !== null && typeof v !== 'object');
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 6, padding: '8px 10px', background: 'var(--success-soft)', border: '1px solid var(--success)', borderRadius: 5, fontSize: 11.5, color: 'var(--success)' }}>
|
||||||
|
✓ Source reachable
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 6, padding: '10px 12px', background: 'var(--bg-2)', border: '1px solid var(--success)', borderRadius: 5, fontSize: 11.5 }}>
|
||||||
|
{entries.map(([k, v]) => (
|
||||||
|
<div key={k} style={{ display: 'flex', gap: 8, padding: '2px 0' }}>
|
||||||
|
<span style={{ color: 'var(--text-3)', minWidth: 100, flexShrink: 0 }}>{k}</span>
|
||||||
|
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11 }}>{String(v)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function NewRecorderModal({ open, onClose }) {
|
function NewRecorderModal({ open, onClose }) {
|
||||||
const { PROJECTS, NODES } = window.ZAMPP_DATA;
|
const { PROJECTS, NODES } = window.ZAMPP_DATA;
|
||||||
const [name, setName] = React.useState('');
|
const [name, setName] = React.useState('');
|
||||||
|
|
@ -18,6 +47,8 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
const [projectId, setProjectId] = React.useState(PROJECTS[0]?.id || '');
|
const [projectId, setProjectId] = React.useState(PROJECTS[0]?.id || '');
|
||||||
const [submitting, setSubmitting] = React.useState(false);
|
const [submitting, setSubmitting] = React.useState(false);
|
||||||
const [submitErr, setSubmitErr] = React.useState(null);
|
const [submitErr, setSubmitErr] = React.useState(null);
|
||||||
|
const [probing, setProbing] = React.useState(false);
|
||||||
|
const [probeResult, setProbeResult] = React.useState(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (sourceType !== 'SDI' || sdiDevices !== null) return;
|
if (sourceType !== 'SDI' || sdiDevices !== null) return;
|
||||||
|
|
@ -26,6 +57,19 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
.catch(() => setSdiDevices([]));
|
.catch(() => setSdiDevices([]));
|
||||||
}, [sourceType]);
|
}, [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 = () => {
|
const handleCreate = () => {
|
||||||
if (!name.trim()) { setSubmitErr('Recorder name is required.'); return; }
|
if (!name.trim()) { setSubmitErr('Recorder name is required.'); return; }
|
||||||
if (sourceType === 'SDI' && !sdiNodeId) { setSubmitErr('Select a capture node for SDI.'); return; }
|
if (sourceType === 'SDI' && !sdiNodeId) { setSubmitErr('Select a capture node for SDI.'); return; }
|
||||||
|
|
@ -99,22 +143,36 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
{sourceType === 'SRT' && (
|
{sourceType === 'SRT' && (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="field-label">Source URL</label>
|
<label className="field-label">Source URL</label>
|
||||||
<input className="field-input mono" placeholder="srt://192.168.1.100:4200"
|
<div style={{ display: 'flex', gap: 6 }}>
|
||||||
value={srtUrl} onChange={e => setSrtUrl(e.target.value)} />
|
<input className="field-input mono" placeholder="srt://192.168.1.100:4200"
|
||||||
|
value={srtUrl} onChange={e => setSrtUrl(e.target.value)} style={{ flex: 1 }} />
|
||||||
|
<button className="btn ghost sm" onClick={handleProbe} disabled={probing}
|
||||||
|
style={{ flexShrink: 0, minWidth: 64 }}>
|
||||||
|
{probing ? '…' : 'Probe'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 4 }}>
|
<div style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 4 }}>
|
||||||
Recorder connects out to this URL (caller mode).
|
Recorder connects out to this URL (caller mode).
|
||||||
</div>
|
</div>
|
||||||
|
{probeResult && <ProbeResult result={probeResult} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sourceType === 'RTMP' && (
|
{sourceType === 'RTMP' && (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="field-label">Source URL</label>
|
<label className="field-label">Source URL</label>
|
||||||
<input className="field-input mono" placeholder="rtmp://server/live/streamkey"
|
<div style={{ display: 'flex', gap: 6 }}>
|
||||||
value={rtmpUrl} onChange={e => setRtmpUrl(e.target.value)} />
|
<input className="field-input mono" placeholder="rtmp://server/live/streamkey"
|
||||||
|
value={rtmpUrl} onChange={e => setRtmpUrl(e.target.value)} style={{ flex: 1 }} />
|
||||||
|
<button className="btn ghost sm" onClick={handleProbe} disabled={probing}
|
||||||
|
style={{ flexShrink: 0, minWidth: 64 }}>
|
||||||
|
{probing ? '…' : 'Probe'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 4 }}>
|
<div style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 4 }}>
|
||||||
Recorder pulls this RTMP stream.
|
Recorder pulls this RTMP stream.
|
||||||
</div>
|
</div>
|
||||||
|
{probeResult && <ProbeResult result={probeResult} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -158,7 +216,8 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
? <option value="">No cluster nodes</option>
|
? <option value="">No cluster nodes</option>
|
||||||
: NODES.map(n => {
|
: NODES.map(n => {
|
||||||
const id = n.id || n.hostname || n.name || '';
|
const id = n.id || n.hostname || n.name || '';
|
||||||
return <option key={id} value={id}>{id}</option>;
|
const label = n.hostname || n.name || id;
|
||||||
|
return <option key={id} value={id}>{label}</option>;
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue