diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx index d213acc..e62afea 100644 --- a/services/web-ui/public/screens-ingest.jsx +++ b/services/web-ui/public/screens-ingest.jsx @@ -1,29 +1,28 @@ -// screens-ingest.jsx — Upload, Recorders, Capture (SDI), Monitors +// screens-ingest.jsx — Upload, Recorders, Capture, Monitors -const { RECORDERS, NODES, SDI_PORTS_zampp2, PROJECTS } = window.ZAMPP_DATA; - -/* ========== Upload ========== */ +/* ===== Upload ===== */ function Upload({ navigate }) { - const [files, setFiles] = React.useState([ - { id: 1, name: "Drone_Aerial_Lap_4.mov", size: "12.4 GB", progress: 68, status: "uploading" }, - { id: 2, name: "Interview_Director.mxf", size: "2.1 GB", progress: 100, status: "done" }, - { id: 3, name: "Sponsor_Logo_v4.mov", size: "120 MB", progress: 100, status: "done" }, - { id: 4, name: "Pit_Cam_3.mp4", size: "4.8 GB", progress: 24, status: "uploading" }, - { id: 5, name: "Backstage_Audio.wav", size: "920 MB", progress: 0, status: "queued" }, - ]); + const [files, setFiles] = React.useState([]); + const [project, setProject] = React.useState(''); React.useEffect(() => { - const i = setInterval(() => { - setFiles(fs => fs.map(f => { - if (f.status !== "uploading") return f; - const next = f.progress + Math.random() * 4; - if (next >= 100) return { ...f, progress: 100, status: "done" }; - return { ...f, progress: next }; - })); - }, 600); - return () => clearInterval(i); + 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 (
@@ -31,108 +30,139 @@ function Upload({ navigate }) { Drop video, audio, or stills — we proxy and index automatically.
-
+
-
Protour 2026
-
-
- -
Master files
+
+ {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 — up to 5 GB each
+
Video, audio, and image files
- {["MOV", "MP4", "MXF", "ProRes", "DNxHR", "WAV", "AIFF"].map(f => {f})} + {['MOV', 'MP4', 'MXF', 'ProRes', 'DNxHR', 'WAV', 'AIFF'].map(f => {f})}
-
-
- Queue - {files.length} - - - -
-
- {files.map(f => ( -
- -
-
- {f.name} - {f.size} -
-
-
+ {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) + '%'} +
- - {f.status === "done" ? "✓ done" : f.status === "queued" ? "queued" : `${Math.round(f.progress)}%`} - -
- ))} + ))} +
-
+ )}
); } -/* ========== Recorders (live ingest dashboard) ========== */ +/* ===== Recorders ===== */ function Recorders({ navigate, onNew }) { + const [recorders, setRecorders] = React.useState(window.ZAMPP_DATA.RECORDERS); + + // Poll every 10s for recorder status changes + 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 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
-
- - 4 recording · 1 armed · 1 error -
+ {(liveCount > 0 || errCount > 0) && ( +
+ + {liveCount > 0 ? liveCount + ' recording' : ''}{errCount > 0 ? (liveCount > 0 ? ' · ' : '') + errCount + ' error' : ''} +
+ )}
-
- {RECORDERS.map(r => )} -
+ {recorders.length === 0 ? ( +
+ No recorders configured. +
+
+ ) : ( +
+ {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(()=>{}); }} />)} +
+ )}
); } -function RecorderRow({ recorder }) { - const isRec = recorder.status === "recording"; +function RecorderRow({ recorder, onRefresh }) { + const isRec = recorder.status === 'recording'; + + const toggle = () => { + const action = isRec ? 'stop' : 'start'; + window.ZAMPP_API.fetch('/recorders/' + recorder.id + '/' + action, { method: 'POST' }) + .then(onRefresh).catch(() => {}); + }; + return ( -
+
- {recorder.audio ? ( -
- - -
- ) : isRec ? ( - - ) : ( -
- -
- )} + {isRec + ? + :
}
-
+
{recorder.name} - + {recorder.status.toUpperCase()} {recorder.source} @@ -140,8 +170,7 @@ function RecorderRow({ recorder }) {
{recorder.url}
{recorder.codec}· - {recorder.res}· - node: {recorder.node} + {recorder.res}
@@ -150,211 +179,133 @@ function RecorderRow({ recorder }) {
{recorder.elapsed}
-
Bitrate
-
{recorder.bitrate}
-
-
-
Health
-
- -
+
Status
+
- {isRec ? ( - - ) : recorder.status === "error" ? ( - - ) : ( - - )} + {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)"; + const color = value > 80 ? 'var(--success)' : value > 40 ? 'var(--warning)' : 'var(--danger)'; return ( -
-
-
+
+
+
- {value}% + {value}%
); } -function badgeForStatus(s) { - return { recording: "live", armed: "accent", idle: "neutral", error: "danger", offline: "neutral" }[s] || "neutral"; -} - -/* ========== Capture (rich SDI port picker) ========== */ +/* ===== Capture ===== */ function Capture({ navigate }) { - const [activePort, setActivePort] = React.useState(1); - const ports = SDI_PORTS_zampp2; + const [devices, setDevices] = React.useState([]); + const [activeIdx, setActiveIdx] = React.useState(0); + + React.useEffect(() => { + window.ZAMPP_API.fetch('/cluster/devices/blackmagic') + .then(devs => setDevices(devs || [])) + .catch(() => setDevices([])); + }, []); + + 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 — multi-port routing across cluster nodes + DeckLink SDI ingest — {devices.length} device{devices.length > 1 ? 's' : ''} in cluster
- - +
-
- - -
-
-
- ); -} - -function DeckLinkVisual({ ports, activePort, onSelect }) { - return ( -
-
-
-
DeckLink Duo 2
-
zampp2 · 172.18.91.217
-
- ONLINE -
-
-
-
DECKLINK DUO 2
-
-
-
-
-
-
- {ports.map(p => ( - ))}
-
-
- Ports: 4 - Active: {ports.filter(p => p.active).length} - Recording: {ports.filter(p => p.recording).length} -
-
- ); -} - -function CaptureDetail({ port }) { - if (!port.active) { - return ( -
- -
No signal on SDI {port.idx}
-
Connect a source, then click Refresh.
-
- ); - } - return ( -
-
- -
- {port.recording && ( -
- REC +
+
+ +
+
{active.model || active.device || 'DeckLink'}
+
{active.hostname} · {active.ip_address}
+
- )} -
- - +
Connect a source and click Refresh to see port status.
-
- {port.signal} -
-
-
- - - - - - -
-
- {!port.recording ? ( - - ) : ( - - )} - -
); } -function CaptureStat({ label, value }) { - return ( -
-
{label}
-
{value}
-
- ); -} - -/* ========== Monitors (multi-cam grid) ========== */ +/* ===== Monitors ===== */ function Monitors({ navigate }) { + const { RECORDERS } = window.ZAMPP_DATA; const [grid, setGrid] = React.useState(4); - const allFeeds = [ - ...RECORDERS.filter(r => !r.audio).map(r => ({ ...r, kind: "video" })), - { id: "audio1", name: "FOH Mix", kind: "audio" }, - { id: "audio2", name: "Stage Mics", kind: "audio" }, - { id: "audio3", name: "PGM Bus", kind: "audio" }, + + const videoFeeds = RECORDERS.filter(r => !r.audio); + const audioFeeds = [ + { id: '__audio1', name: 'FOH Mix', kind: 'audio' }, + { id: '__audio2', name: 'PGM Bus', kind: 'audio' }, ]; - const feeds = allFeeds.slice(0, grid * grid - (grid === 2 ? 1 : 0)); - const gridSize = grid; + const allFeeds = [ + ...videoFeeds.map(r => ({ ...r, kind: 'video' })), + ...audioFeeds, + ]; + const feeds = allFeeds.slice(0, grid * grid); return (

Monitors

- Multi-cam live monitoring across all active feeds + Multi-cam live monitoring
{[2, 3, 4].map(n => ( - + ))}
-
-
- {feeds.map((f, i) => ( - - ))} +
+ {feeds.map((f, i) => )} + {feeds.length === 0 && ( +
No active feeds. Start a recorder to see live video here.
+ )}
@@ -362,13 +313,13 @@ function Monitors({ navigate }) { } function MonitorTile({ feed, seed }) { - if (feed.kind === "audio") { + if (feed.kind === 'audio') { return (
-
+
-
+
@@ -381,17 +332,15 @@ function MonitorTile({ feed, seed }) { } return (
- -
-
- {feed.status === "recording" && REC} - {feed.status === "armed" && ARMED} - {feed.status === "error" && ERR} - {feed.status === "idle" && IDLE} + +
+ {feed.status === 'recording' && REC} + {feed.status === 'stopped' && IDLE} + {feed.status === 'error' && ERR}
{feed.name} - {feed.elapsed && feed.elapsed !== "00:00:00" && {feed.elapsed}} + {feed.elapsed && feed.elapsed !== '—' && {feed.elapsed}}
);