// screens-ingest.jsx — Upload, Recorders, Capture, Monitors /* ===== Upload ===== */ function Upload({ navigate }) { const [files, setFiles] = React.useState([]); const [project, setProject] = React.useState(''); React.useEffect(() => { const { PROJECTS } = window.ZAMPP_DATA; if (PROJECTS.length > 0 && !project) setProject(PROJECTS[0].id); }, []); const handleDrop = (e) => { e.preventDefault(); const dropped = Array.from(e.dataTransfer ? e.dataTransfer.files : e.target.files); const newFiles = dropped.map((f, i) => ({ id: Date.now() + i, name: f.name, size: window.ZAMPP_API.fmtSize(f.size), file: f, progress: 0, status: 'queued', })); setFiles(prev => [...prev, ...newFiles]); }; const { PROJECTS } = window.ZAMPP_DATA; return (

Upload

Drop video, audio, or stills — we proxy and index automatically.
{PROJECTS.find(p => p.id === project)?.name || (PROJECTS.length ? PROJECTS[0].name : 'No projects')}
e.preventDefault()} onClick={() => { const i = document.createElement('input'); i.type='file'; i.multiple=true; i.onchange=handleDrop; i.click(); }}>
Drop files here or click to browse
Video, audio, and image files
{['MOV', 'MP4', 'MXF', 'ProRes', 'DNxHR', 'WAV', 'AIFF'].map(f => {f})}
{files.length > 0 && (
Queue {files.length}
{files.map(f => (
{f.name} {f.size}
{f.status === 'done' ? '✓ done' : f.status === 'queued' ? 'queued' : Math.round(f.progress) + '%'}
))}
)}
); } /* ===== Recorders ===== */ function _normRecorder(r) { let elapsed = '—'; if (r.status === 'recording' && r.started_at) { const s = Math.floor((Date.now() - new Date(r.started_at)) / 1000); elapsed = String(Math.floor(s / 3600)).padStart(2, '0') + ':' + String(Math.floor((s % 3600) / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0'); } const cfg = r.source_config || {}; return { ...r, source: r.source_type || '—', url: cfg.url || cfg.address || cfg.srt_url || cfg.rtmp_url || r.source_type || '—', codec: r.recording_codec || '—', res: r.recording_resolution || '—', node: r.node_id ? r.node_id.slice(0, 8) : 'primary', elapsed, bitrate: '—', health: 100, audio: false, }; } function Recorders({ navigate, onNew }) { const [recorders, setRecorders] = React.useState(window.ZAMPP_DATA.RECORDERS); const refresh = React.useCallback(() => { window.ZAMPP_API.fetch('/recorders') .then(raw => { const norm = (raw || []).map(_normRecorder); window.ZAMPP_DATA.RECORDERS = norm; setRecorders(norm); }) .catch(() => {}); }, []); React.useEffect(() => { const id = setInterval(refresh, 10000); return () => clearInterval(id); }, []); const liveCount = recorders.filter(r => r.status === 'recording').length; const errCount = recorders.filter(r => r.status === 'error').length; return (

Recorders

Live ingest from SRT, RTMP, and SDI sources
{(liveCount > 0 || errCount > 0) && (
{liveCount > 0 ? liveCount + ' recording' : ''}{errCount > 0 ? (liveCount > 0 ? ' · ' : '') + errCount + ' error' : ''}
)}
{recorders.length === 0 ? (
No recorders configured.
) : (
{recorders.map(r => )}
)}
); } function RecorderRow({ recorder: initialRecorder, onRefresh }) { const [recorder, setRecorder] = React.useState(initialRecorder); const [pending, setPending] = React.useState(false); const [err, setErr] = React.useState(null); const isRec = recorder.status === 'recording'; React.useEffect(() => { setRecorder(initialRecorder); }, [initialRecorder.id, initialRecorder.status]); const toggle = () => { if (pending) return; const action = isRec ? 'stop' : 'start'; setPending(true); setErr(null); setRecorder(r => ({ ...r, status: isRec ? 'idle' : 'recording' })); window.ZAMPP_API.fetch('/recorders/' + recorder.id + '/' + action, { method: 'POST' }) .then(() => { setPending(false); onRefresh(); }) .catch(e => { setPending(false); setErr(e.message || 'Failed'); setRecorder(initialRecorder); }); }; return (
{isRec ? :
}
{recorder.name} {recorder.status.toUpperCase()} {recorder.source}
{recorder.url}
{recorder.codec}· {recorder.res}
{err &&
{err}
}
Elapsed
{recorder.elapsed}
Status
{isRec ? : }
); } function badgeForStatus(s) { return { recording: 'live', armed: 'accent', idle: 'neutral', error: 'danger', offline: 'neutral', stopped: 'neutral' }[s] || 'neutral'; } function HealthBar({ value }) { const color = value > 80 ? 'var(--success)' : value > 40 ? 'var(--warning)' : 'var(--danger)'; return (
{value}%
); } /* ===== Capture ===== */ function Capture({ navigate }) { const [devices, setDevices] = React.useState([]); const [activeIdx, setActiveIdx] = React.useState(0); const loadDevices = () => { window.ZAMPP_API.fetch('/cluster/devices/blackmagic') .then(devs => setDevices(Array.isArray(devs) ? devs : [])) .catch(() => setDevices([])); }; React.useEffect(() => { loadDevices(); }, []); if (devices.length === 0) { return (

Capture

DeckLink SDI ingest
No DeckLink devices found in cluster.
); } const active = devices[activeIdx] || devices[0]; return (

Capture

DeckLink SDI ingest — {devices.length} device{devices.length > 1 ? 's' : ''} in cluster
{devices.map((d, i) => ( ))}
{active.model || active.device || 'DeckLink'}
{active.hostname} · {active.ip_address}
Connect a source and click Refresh to see port status.
); } /* ===== Monitors ===== */ function Monitors({ navigate }) { const [recorders, setRecorders] = React.useState(window.ZAMPP_DATA.RECORDERS); const [grid, setGrid] = React.useState(4); React.useEffect(() => { const refresh = () => { window.ZAMPP_API.fetch('/recorders') .then(raw => { const norm = (raw || []).map(_normRecorder); window.ZAMPP_DATA.RECORDERS = norm; setRecorders(norm); }) .catch(() => {}); }; const id = setInterval(refresh, 5000); return () => clearInterval(id); }, []); const videoFeeds = recorders.filter(r => !r.audio); const audioFeeds = recorders.filter(r => r.audio).map(r => ({ ...r, kind: 'audio' })); const allFeeds = [ ...videoFeeds.map(r => ({ ...r, kind: 'video' })), ...audioFeeds, ]; const feeds = allFeeds.slice(0, grid * grid); return (

Monitors

Multi-cam live monitoring
{[2, 3, 4].map(n => ( ))}
{feeds.length === 0 ? (
No active feeds. Start a recorder to see live video here.
) : (
{feeds.map((f, i) => )}
)}
); } function MonitorTile({ feed, seed }) { const [levels, setLevels] = React.useState([0.65, 0.78]); const isLive = feed.status === 'recording'; React.useEffect(() => { if (!isLive) return; const id = setInterval(() => { setLevels([0.3 + Math.random() * 0.55, 0.3 + Math.random() * 0.55]); }, 180); return () => clearInterval(id); }, [isLive]); if (feed.kind === 'audio') { return (
LIVE {feed.name}
); } return (
{isLive && (
)}
{isLive && REC} {feed.status === 'stopped' && IDLE} {feed.status === 'idle' && IDLE} {feed.status === 'error' && ERR}
{isLive && (
)}
{feed.name} {feed.elapsed && feed.elapsed !== '—' && {feed.elapsed}}
); } Object.assign(window, { Upload, Recorders, Capture, Monitors });