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 React, { useState, useEffect } from 'react';
|
||||||
import { RecorderConfig, CodecType } from '../types';
|
import { RecorderConfig, CodecType, SRTDestination } from '../types';
|
||||||
|
|
||||||
interface ConfigPanelProps {
|
interface ConfigPanelProps {
|
||||||
portIndex: number;
|
portIndex: number;
|
||||||
|
|
@ -14,7 +14,7 @@ export function ConfigPanel({ portIndex, currentConfig, onSave, onClose }: Confi
|
||||||
const [qualityProfile, setQualityProfile] = useState('hq');
|
const [qualityProfile, setQualityProfile] = useState('hq');
|
||||||
const [recordingPath, setRecordingPath] = useState(`/recordings/port_${portIndex}_{timestamp}.mxf`);
|
const [recordingPath, setRecordingPath] = useState(`/recordings/port_${portIndex}_{timestamp}.mxf`);
|
||||||
const [srtEnabled, setSrtEnabled] = useState(false);
|
const [srtEnabled, setSrtEnabled] = useState(false);
|
||||||
const [srtDestination, setSrtDestination] = useState('');
|
const [srtDestinations, setSrtDestinations] = useState<SRTDestination[]>([]);
|
||||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -24,20 +24,41 @@ export function ConfigPanel({ portIndex, currentConfig, onSave, onClose }: Confi
|
||||||
setQualityProfile(currentConfig.quality_profile);
|
setQualityProfile(currentConfig.quality_profile);
|
||||||
setRecordingPath(currentConfig.recording_path);
|
setRecordingPath(currentConfig.recording_path);
|
||||||
setSrtEnabled(currentConfig.srt_enabled);
|
setSrtEnabled(currentConfig.srt_enabled);
|
||||||
setSrtDestination(currentConfig.srt_destination);
|
setSrtDestinations(currentConfig.srt_destinations ?? []);
|
||||||
setPreviewEnabled(currentConfig.preview_enabled);
|
setPreviewEnabled(currentConfig.preview_enabled);
|
||||||
}
|
}
|
||||||
}, [currentConfig]);
|
}, [currentConfig]);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
onSave({
|
onSave({
|
||||||
port_index: portIndex, codec, bitrate, quality_profile: qualityProfile,
|
port_index: portIndex,
|
||||||
recording_path: recordingPath, srt_enabled: srtEnabled,
|
codec,
|
||||||
srt_destination: srtDestination, preview_enabled: previewEnabled,
|
bitrate,
|
||||||
|
quality_profile: qualityProfile,
|
||||||
|
recording_path: recordingPath,
|
||||||
|
srt_enabled: srtEnabled,
|
||||||
|
srt_destination: srtDestinations[0]?.url ?? '',
|
||||||
|
srt_destinations: srtDestinations,
|
||||||
|
preview_enabled: previewEnabled,
|
||||||
});
|
});
|
||||||
onClose();
|
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 = {
|
const inputStyle: React.CSSProperties = {
|
||||||
background: 'rgba(0,0,0,0.35)', border: '1px solid var(--border)', borderRadius: 3,
|
background: 'rgba(0,0,0,0.35)', border: '1px solid var(--border)', borderRadius: 3,
|
||||||
padding: '9px 12px', fontFamily: 'var(--font-mono)', fontSize: 13,
|
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 = {
|
const selectStyle: React.CSSProperties = {
|
||||||
...inputStyle,
|
...inputStyle, cursor: 'pointer', appearance: 'none' as const,
|
||||||
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\")",
|
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',
|
backgroundRepeat: 'no-repeat', backgroundPosition: 'right 12px center',
|
||||||
paddingRight: 32,
|
paddingRight: 32, background: 'rgba(0,0,0,0.35)',
|
||||||
background: 'rgba(0,0,0,0.35)',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const labelStyle: React.CSSProperties = {
|
const labelStyle: React.CSSProperties = {
|
||||||
|
|
@ -80,7 +99,7 @@ export function ConfigPanel({ portIndex, currentConfig, onSave, onClose }: Confi
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--bg-panel)',
|
background: 'var(--bg-panel)',
|
||||||
border: '1px solid var(--border-bright)',
|
border: '1px solid var(--border-bright)',
|
||||||
borderRadius: 4, width: '100%', maxWidth: 500,
|
borderRadius: 4, width: '100%', maxWidth: 520,
|
||||||
maxHeight: '90vh', overflowY: 'auto',
|
maxHeight: '90vh', overflowY: 'auto',
|
||||||
boxShadow: '0 24px 80px rgba(0,0,0,0.8)',
|
boxShadow: '0 24px 80px rgba(0,0,0,0.8)',
|
||||||
animation: 'modalIn 0.2s ease',
|
animation: 'modalIn 0.2s ease',
|
||||||
|
|
@ -157,7 +176,7 @@ export function ConfigPanel({ portIndex, currentConfig, onSave, onClose }: Confi
|
||||||
{/* Transport section */}
|
{/* Transport section */}
|
||||||
<div>
|
<div>
|
||||||
<div style={sectionTitleStyle}>
|
<div style={sectionTitleStyle}>
|
||||||
Transport
|
SRT Transport
|
||||||
<div style={{ flex: 1, height: 1, background: 'var(--border)' }} />
|
<div style={{ flex: 1, height: 1, background: 'var(--border)' }} />
|
||||||
</div>
|
</div>
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
|
|
@ -166,10 +185,91 @@ export function ConfigPanel({ portIndex, currentConfig, onSave, onClose }: Confi
|
||||||
checked={srtEnabled}
|
checked={srtEnabled}
|
||||||
onChange={setSrtEnabled}
|
onChange={setSrtEnabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{srtEnabled && (
|
{srtEnabled && (
|
||||||
<div style={{ marginTop: 10 }}>
|
<div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||||
<label style={labelStyle}>SRT Destination</label>
|
{srtDestinations.map((dest, idx) => (
|
||||||
<input type="text" value={srtDestination} onChange={e => setSrtDestination(e.target.value)} placeholder="srt://host:9000" style={inputStyle} />
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue