import React, { useState, useEffect } from 'react'; import { useWebSocket } from './hooks/useWebSocket'; import { useRecorder } from './hooks/useRecorder'; import { PortCard } from './components/PortCard'; import { VideoPreview } from './components/VideoPreview'; import { ConfigPanel } from './components/ConfigPanel'; import { SCTE35Trigger } from './components/SCTE35Trigger'; import { PortStatus, RecorderConfig } from './types'; export default function App() { const [ports, setPorts] = useState([]); const [selectedPort, setSelectedPort] = useState(null); const [configPort, setConfigPort] = useState(null); const [configs, setConfigs] = useState>({}); const [clock, setClock] = useState(''); const wsUrl = `ws://${window.location.host}/ws`; const { isConnected, lastMessage } = useWebSocket(wsUrl); const { startRecording, stopRecording, injectSCTE35, injectSCTE35ForPort } = useRecorder(); // Clock useEffect(() => { const tick = () => { const now = new Date(); const pad = (n: number) => String(n).padStart(2, '0'); setClock(`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`); }; tick(); const id = setInterval(tick, 1000); return () => clearInterval(id); }, []); // WebSocket port updates useEffect(() => { if (lastMessage?.type === 'port_status' && lastMessage.ports) { setPorts(lastMessage.ports); } }, [lastMessage]); // Initial fetch useEffect(() => { fetch('/api/ports') .then(r => r.json()) .then(setPorts) .catch(console.error); }, []); const handleStartRecording = async (portIndex: number) => { const config = configs[portIndex] || defaultConfig(portIndex); await startRecording(portIndex, config); }; const handleSaveConfig = (config: RecorderConfig) => { setConfigs(prev => ({ ...prev, [config.port_index]: config })); }; const handleInjectSCTE35 = async (eventId: number, durationSeconds: number) => { await injectSCTE35(eventId, durationSeconds); }; const handleInjectSCTE35ForPort = async ( portIndex: number, eventId: number, durationSeconds: number, srtDestinationUrl?: string ) => { await injectSCTE35ForPort(portIndex, eventId, durationSeconds, srtDestinationUrl); }; const handleSelectPort = (portIndex: number) => { setSelectedPort(prev => (prev === portIndex ? null : portIndex)); }; const selectedPortData = ports.find(p => p.port_index === selectedPort) ?? null; return (
{/* ── HEADER ── */}
Dragon Encode
Dragon Encode SDI Recorder Suite
{isConnected ? 'LIVE' : 'DISCONNECTED'}
{clock}
{/* ── SECTION LABEL ── */}
SDI Inputs
{ports.length} PORT{ports.length !== 1 ? 'S' : ''}
{/* ── PORT CARDS (8-port grid: 4x2) ── */}
{ports.length === 0 ? (
No Ports Available Waiting for backend connection...
) : ( ports.map(port => ( )) )}
{/* ── DETAIL PANEL ── */} {selectedPort !== null && (
{/* Preview */}
HLS Preview PORT {selectedPort}
{/* Global SCTE-35 panel */}
Ad Break — All Ports SCTE-35 / 104
)}
{/* ── CONFIG MODAL ── */} {configPort !== null && ( setConfigPort(null)} /> )}
); } function defaultConfig(portIndex: number): RecorderConfig { return { port_index: portIndex, codec: 'PRORES', bitrate: '185M', quality_profile: 'hq', recording_path: `/recordings/port_${portIndex}_{timestamp}.mxf`, srt_enabled: false, srt_destination: '', srt_destinations: [], preview_enabled: true, }; }