From 24a1d57165a1b42afbae3643d1825e51191eefa9 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Fri, 22 May 2026 10:55:19 -0400 Subject: [PATCH] fix: SDI crash, monitors polling, home RAM fields, editor IN DEV splash, timecode, create recorder API: screens-ingest.jsx --- services/web-ui/public/screens-ingest.jsx | 162 +++++++++++++++------- 1 file changed, 114 insertions(+), 48 deletions(-) diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx index e62afea..44ee642 100644 --- a/services/web-ui/public/screens-ingest.jsx +++ b/services/web-ui/public/screens-ingest.jsx @@ -84,30 +84,45 @@ function Upload({ navigate }) { } /* ===== 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); - // Poll every 10s for recorder status changes + 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 refresh = () => { - window.ZAMPP_API.fetch('/recorders') - .then(raw => { - const norm = (raw || []).map(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||r.source_type||'—', codec: r.recording_codec||'—', res: r.recording_resolution||'—', node: r.node_id||'primary', elapsed, bitrate:'—', health:100, audio:false }; - }); - window.ZAMPP_DATA.RECORDERS = norm; - setRecorders(norm); - }) - .catch(() => {}); - }; - const i = setInterval(refresh, 10000); - return () => clearInterval(i); + const id = setInterval(refresh, 10000); + return () => clearInterval(id); }, []); const liveCount = recorders.filter(r => r.status === 'recording').length; @@ -125,6 +140,7 @@ function Recorders({ navigate, onNew }) { {liveCount > 0 ? liveCount + ' recording' : ''}{errCount > 0 ? (liveCount > 0 ? ' · ' : '') + errCount + ' error' : ''} )} +
@@ -135,7 +151,7 @@ function Recorders({ navigate, onNew }) {
) : (
- {recorders.map(r => { window.ZAMPP_API.fetch('/recorders').then(raw => setRecorders((raw||[]).map(x => { const cfg=x.source_config||{}; return {...x,source:x.source_type,url:cfg.url||cfg.address||x.source_type,codec:x.recording_codec,res:x.recording_resolution,node:x.node_id,elapsed:'—',bitrate:'—',health:100,audio:false}; }))).catch(()=>{}); }} />)} + {recorders.map(r => )}
)} @@ -143,13 +159,23 @@ function Recorders({ navigate, onNew }) { ); } -function RecorderRow({ recorder, onRefresh }) { +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(onRefresh).catch(() => {}); + .then(() => { setPending(false); onRefresh(); }) + .catch(e => { setPending(false); setErr(e.message || 'Failed'); setRecorder(initialRecorder); }); }; return ( @@ -172,6 +198,7 @@ function RecorderRow({ recorder, onRefresh }) { {recorder.codec}· {recorder.res} + {err &&
{err}
}
@@ -185,8 +212,12 @@ function RecorderRow({ recorder, onRefresh }) {
{isRec - ? - : } + ? + : }
@@ -214,11 +245,13 @@ function Capture({ navigate }) { const [devices, setDevices] = React.useState([]); const [activeIdx, setActiveIdx] = React.useState(0); - React.useEffect(() => { + const loadDevices = () => { window.ZAMPP_API.fetch('/cluster/devices/blackmagic') - .then(devs => setDevices(devs || [])) + .then(devs => setDevices(Array.isArray(devs) ? devs : [])) .catch(() => setDevices([])); - }, []); + }; + + React.useEffect(() => { loadDevices(); }, []); if (devices.length === 0) { return ( @@ -227,8 +260,7 @@ function Capture({ navigate }) {

Capture

DeckLink SDI ingest
- +
@@ -247,7 +279,7 @@ function Capture({ navigate }) {

Capture

DeckLink SDI ingest — {devices.length} device{devices.length > 1 ? 's' : ''} in cluster
- +
@@ -274,14 +306,25 @@ function Capture({ navigate }) { /* ===== Monitors ===== */ function Monitors({ navigate }) { - const { RECORDERS } = window.ZAMPP_DATA; + const [recorders, setRecorders] = React.useState(window.ZAMPP_DATA.RECORDERS); const [grid, setGrid] = React.useState(4); - const videoFeeds = RECORDERS.filter(r => !r.audio); - const audioFeeds = [ - { id: '__audio1', name: 'FOH Mix', kind: 'audio' }, - { id: '__audio2', name: 'PGM Bus', kind: 'audio' }, - ]; + 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, @@ -301,18 +344,30 @@ function Monitors({ navigate }) {
-
- {feeds.map((f, i) => )} - {feeds.length === 0 && ( -
No active feeds. Start a recorder to see live video here.
- )} -
+ {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 (
@@ -320,8 +375,8 @@ function MonitorTile({ feed, seed }) {
- - + +
LIVE @@ -330,14 +385,25 @@ function MonitorTile({ feed, seed }) {
); } + return (
+ {isLive && ( +
+ )}
- {feed.status === 'recording' && REC} - {feed.status === 'stopped' && IDLE} - {feed.status === 'error' && ERR} + {isLive && REC} + {feed.status === 'stopped' && IDLE} + {feed.status === 'idle' && IDLE} + {feed.status === 'error' && ERR}
+ {isLive && ( +
+ + +
+ )}
{feed.name} {feed.elapsed && feed.elapsed !== '—' && {feed.elapsed}}