feat: multi-destination SRT in ConfigPanel
This commit is contained in:
parent
e39241d7b1
commit
7cf45ceee8
1 changed files with 115 additions and 15 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { RecorderConfig, CodecType } from '../types';
|
||||
import { RecorderConfig, CodecType, SRTDestination } from '../types';
|
||||
|
||||
interface ConfigPanelProps {
|
||||
portIndex: number;
|
||||
|
|
@ -14,7 +14,7 @@ export function ConfigPanel({ portIndex, currentConfig, onSave, onClose }: Confi
|
|||
const [qualityProfile, setQualityProfile] = useState('hq');
|
||||
const [recordingPath, setRecordingPath] = useState(`/recordings/port_${portIndex}_{timestamp}.mxf`);
|
||||
const [srtEnabled, setSrtEnabled] = useState(false);
|
||||
const [srtDestination, setSrtDestination] = useState('');
|
||||
const [srtDestinations, setSrtDestinations] = useState<SRTDestination[]>([]);
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -24,20 +24,41 @@ export function ConfigPanel({ portIndex, currentConfig, onSave, onClose }: Confi
|
|||
setQualityProfile(currentConfig.quality_profile);
|
||||
setRecordingPath(currentConfig.recording_path);
|
||||
setSrtEnabled(currentConfig.srt_enabled);
|
||||
setSrtDestination(currentConfig.srt_destination);
|
||||
setSrtDestinations(currentConfig.srt_destinations ?? []);
|
||||
setPreviewEnabled(currentConfig.preview_enabled);
|
||||
}
|
||||
}, [currentConfig]);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
port_index: portIndex, codec, bitrate, quality_profile: qualityProfile,
|
||||
recording_path: recordingPath, srt_enabled: srtEnabled,
|
||||
srt_destination: srtDestination, preview_enabled: previewEnabled,
|
||||
port_index: portIndex,
|
||||
codec,
|
||||
bitrate,
|
||||
quality_profile: qualityProfile,
|
||||
recording_path: recordingPath,
|
||||
srt_enabled: srtEnabled,
|
||||
srt_destination: srtDestinations[0]?.url ?? '',
|
||||
srt_destinations: srtDestinations,
|
||||
preview_enabled: previewEnabled,
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
const addDestination = () => {
|
||||
setSrtDestinations(prev => [
|
||||
...prev,
|
||||
{ url: '', label: `Dest ${prev.length + 1}`, enabled: true },
|
||||
]);
|
||||
};
|
||||
|
||||
const removeDestination = (idx: number) => {
|
||||
setSrtDestinations(prev => prev.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
const updateDestination = (idx: number, field: keyof SRTDestination, value: string | boolean) => {
|
||||
setSrtDestinations(prev => prev.map((d, i) => i === idx ? { ...d, [field]: value } : d));
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
background: 'rgba(0,0,0,0.35)', border: '1px solid var(--border)', borderRadius: 3,
|
||||
padding: '9px 12px', fontFamily: 'var(--font-mono)', fontSize: 13,
|
||||
|
|
@ -45,12 +66,10 @@ export function ConfigPanel({ portIndex, currentConfig, onSave, onClose }: Confi
|
|||
};
|
||||
|
||||
const selectStyle: React.CSSProperties = {
|
||||
...inputStyle,
|
||||
cursor: 'pointer', appearance: 'none' as const,
|
||||
...inputStyle, cursor: 'pointer', appearance: 'none' as const,
|
||||
backgroundImage: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='%237a8fa8' d='M0 0l5 6 5-6z'/%3E%3C/svg%3E\")",
|
||||
backgroundRepeat: 'no-repeat', backgroundPosition: 'right 12px center',
|
||||
paddingRight: 32,
|
||||
background: 'rgba(0,0,0,0.35)',
|
||||
paddingRight: 32, background: 'rgba(0,0,0,0.35)',
|
||||
};
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
|
|
@ -80,7 +99,7 @@ export function ConfigPanel({ portIndex, currentConfig, onSave, onClose }: Confi
|
|||
style={{
|
||||
background: 'var(--bg-panel)',
|
||||
border: '1px solid var(--border-bright)',
|
||||
borderRadius: 4, width: '100%', maxWidth: 500,
|
||||
borderRadius: 4, width: '100%', maxWidth: 520,
|
||||
maxHeight: '90vh', overflowY: 'auto',
|
||||
boxShadow: '0 24px 80px rgba(0,0,0,0.8)',
|
||||
animation: 'modalIn 0.2s ease',
|
||||
|
|
@ -157,7 +176,7 @@ export function ConfigPanel({ portIndex, currentConfig, onSave, onClose }: Confi
|
|||
{/* Transport section */}
|
||||
<div>
|
||||
<div style={sectionTitleStyle}>
|
||||
Transport
|
||||
SRT Transport
|
||||
<div style={{ flex: 1, height: 1, background: 'var(--border)' }} />
|
||||
</div>
|
||||
<ToggleRow
|
||||
|
|
@ -166,10 +185,91 @@ export function ConfigPanel({ portIndex, currentConfig, onSave, onClose }: Confi
|
|||
checked={srtEnabled}
|
||||
onChange={setSrtEnabled}
|
||||
/>
|
||||
|
||||
{srtEnabled && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<label style={labelStyle}>SRT Destination</label>
|
||||
<input type="text" value={srtDestination} onChange={e => setSrtDestination(e.target.value)} placeholder="srt://host:9000" style={inputStyle} />
|
||||
<div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{srtDestinations.map((dest, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
background: 'rgba(0,0,0,0.25)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 3,
|
||||
padding: '12px 14px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span style={{ fontFamily: 'var(--font-ui)', fontSize: 10, fontWeight: 700, letterSpacing: '0.25em', textTransform: 'uppercase', color: 'var(--accent-amber)' }}>
|
||||
Destination {idx + 1}
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={dest.enabled}
|
||||
onChange={e => updateDestination(idx, 'enabled', e.target.checked)}
|
||||
style={{ accentColor: 'var(--dragon-blue)', width: 14, height: 14 }}
|
||||
/>
|
||||
<span style={{ fontFamily: 'var(--font-ui)', fontSize: 10, color: 'var(--text-muted)', letterSpacing: '0.1em' }}>Active</span>
|
||||
</label>
|
||||
<button
|
||||
onClick={() => removeDestination(idx)}
|
||||
style={{
|
||||
background: 'transparent', border: 'none',
|
||||
color: 'rgba(255,32,32,0.6)', cursor: 'pointer',
|
||||
fontSize: 14, lineHeight: 1, padding: '2px 4px',
|
||||
}}
|
||||
title="Remove destination"
|
||||
>✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>Label</label>
|
||||
<input
|
||||
type="text"
|
||||
value={dest.label}
|
||||
onChange={e => updateDestination(idx, 'label', e.target.value)}
|
||||
placeholder={`Destination ${idx + 1}`}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>SRT URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={dest.url}
|
||||
onChange={e => updateDestination(idx, 'url', e.target.value)}
|
||||
placeholder="srt://host:9000"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={addDestination}
|
||||
style={{
|
||||
background: 'rgba(26,58,255,0.08)',
|
||||
border: '1px dashed rgba(26,58,255,0.35)',
|
||||
color: 'rgba(122,164,255,0.7)',
|
||||
fontFamily: 'var(--font-ui)', fontSize: 11, fontWeight: 600,
|
||||
letterSpacing: '0.15em', textTransform: 'uppercase',
|
||||
padding: '10px 14px', borderRadius: 3, cursor: 'pointer',
|
||||
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, lineHeight: 1 }}>+</span>
|
||||
Add Destination
|
||||
</button>
|
||||
|
||||
{srtDestinations.length === 0 && (
|
||||
<p style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-muted)', textAlign: 'center' }}>
|
||||
No destinations configured — click Add Destination
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue