dragonflight/services/web-ui/public/modal-new-recorder.jsx

223 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// modal-new-recorder.jsx — New Recorder dialog (SRT / RTMP / SDI)
const { SDI_PORTS_zampp2, PROJECTS: ALL_PROJECTS } = window.ZAMPP_DATA;
function NewRecorderModal({ open, onClose }) {
const [name, setName] = React.useState("");
const [sourceType, setSourceType] = React.useState("SRT");
const [sdiPort, setSdiPort] = React.useState(1);
const [recTab, setRecTab] = React.useState("video");
const [proxyTab, setProxyTab] = React.useState("video");
const [proxyOn, setProxyOn] = React.useState(true);
if (!open) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" onClick={e => e.stopPropagation()}>
<div className="modal-head">
<div>
<div style={{ fontSize: 15, fontWeight: 600 }}>New recorder</div>
<div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 2 }}>Configure source, codec, and destination</div>
</div>
<button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
</div>
<div className="modal-body">
<div className="field">
<label className="field-label">Recorder name</label>
<input className="field-input" placeholder="e.g. Studio A Stage Cam" value={name} onChange={e => setName(e.target.value)} />
</div>
<div className="field">
<label className="field-label">Source type</label>
<div className="source-type-grid">
{[
{ id: "SRT", label: "SRT", desc: "Secure Reliable Transport — pull caller" },
{ id: "RTMP", label: "RTMP", desc: "Real-Time Messaging Protocol" },
{ id: "SDI", label: "SDI", desc: "Blackmagic DeckLink hardware" },
].map(t => (
<button
key={t.id}
className={`source-type-card ${sourceType === t.id ? "active" : ""}`}
onClick={() => setSourceType(t.id)}
>
<div className="source-type-icon">
<Icon name={t.id === "SDI" ? "video" : t.id === "RTMP" ? "globe" : "signal"} size={16} />
</div>
<div>
<div style={{ fontWeight: 600, fontSize: 13 }}>{t.label}</div>
<div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 2 }}>{t.desc}</div>
</div>
</button>
))}
</div>
</div>
{sourceType === "SRT" && (
<div className="field">
<label className="field-label">Source URL</label>
<input className="field-input mono" placeholder="srt://192.168.1.100:4200" defaultValue="srt://10.0.4.18:4200" />
<div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 4 }}>
The recorder connects out to this URL (caller mode). <span className="mono">?mode=caller</span> is appended automatically.
</div>
</div>
)}
{sourceType === "RTMP" && (
<div className="field">
<label className="field-label">Source URL</label>
<input className="field-input mono" placeholder="rtmp://server/live/streamkey" defaultValue="rtmp://stream.local/live/cam_a" />
<div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 4 }}>
The recorder will pull this RTMP stream. Must be an existing published stream.
</div>
</div>
)}
{sourceType === "SDI" && (
<>
<div className="field">
<label className="field-label">Capture node</label>
<div className="field-input select">
<span>zampp2 · DeckLink Duo 2 · 4 ports</span>
<Icon name="chevronDown" size={12} style={{ color: "var(--text-3)" }} />
</div>
</div>
<div className="field">
<label className="field-label">Port</label>
<SDIPortMini ports={SDI_PORTS_zampp2} selected={sdiPort} onSelect={setSdiPort} />
</div>
</>
)}
<div className="modal-section">
<div className="modal-section-head">
<span>Master recording</span>
<span style={{ flex: 1 }} />
<div className="tab-group">
{["video", "audio", "container"].map(t => (
<button key={t} className={recTab === t ? "active" : ""} onClick={() => setRecTab(t)}>
{t[0].toUpperCase() + t.slice(1)}
</button>
))}
</div>
</div>
<div className="modal-section-body">
{recTab === "video" && (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
<Field label="Video codec" value="ProRes 422 HQ" select />
<Field label="Resolution" value="2160p59.94 (source)" select />
<Field label="Color space" value="Rec. 709" select />
<Field label="Bit depth" value="10-bit" select />
</div>
)}
{recTab === "audio" && (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
<Field label="Audio codec" value="PCM" select />
<Field label="Sample rate" value="48 kHz" select />
<Field label="Channels" value="2.0 stereo" select />
<Field label="Bit depth" value="24-bit" select />
</div>
)}
{recTab === "container" && (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
<Field label="Container" value="MOV (QuickTime)" select />
<Field label="Segment" value="None (single file)" select />
</div>
)}
</div>
</div>
<div className="modal-toggle-row">
<label className="switch">
<input type="checkbox" checked={proxyOn} onChange={e => setProxyOn(e.target.checked)} />
<span className="switch-track"><span className="switch-knob" /></span>
</label>
<div>
<div style={{ fontWeight: 500, fontSize: 13 }}>Generate proxy</div>
<div style={{ fontSize: 11, color: "var(--text-3)" }}>
SDI sources record proxy in parallel. Network sources (SRT/RTMP) generate proxy after stop.
</div>
</div>
</div>
{proxyOn && (
<div className="modal-section">
<div className="modal-section-head">
<span>Proxy</span>
<span style={{ flex: 1 }} />
<div className="tab-group">
{["video", "audio", "container"].map(t => (
<button key={t} className={proxyTab === t ? "active" : ""} onClick={() => setProxyTab(t)}>
{t[0].toUpperCase() + t.slice(1)}
</button>
))}
</div>
</div>
<div className="modal-section-body">
{proxyTab === "video" && (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
<Field label="Video codec" value="H.264 (NVENC)" select />
<Field label="Resolution" value="1280×720" select />
<Field label="Bitrate" value="2 Mbps" select />
<Field label="GOP" value="2 s" select />
</div>
)}
{proxyTab === "audio" && (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
<Field label="Audio codec" value="AAC" select />
<Field label="Bitrate" value="128 kbps" select />
</div>
)}
{proxyTab === "container" && (
<Field label="Container" value="fMP4 (HLS-ready)" select />
)}
</div>
</div>
)}
<div className="modal-section">
<div className="modal-section-head"><span>Destination</span></div>
<div className="modal-section-body">
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
<Field label="Project" value="Protour 2026" select />
<Field label="Bin" value="Live captures" select />
</div>
</div>
</div>
</div>
<div className="modal-foot">
<button className="btn ghost" onClick={onClose}>Cancel</button>
<span style={{ flex: 1 }} />
<button className="btn subtle"><Icon name="signal" />Probe source</button>
<button className="btn primary">Create recorder</button>
</div>
</div>
</div>
);
}
function SDIPortMini({ ports, selected, onSelect }) {
return (
<div className="sdi-port-mini">
<div className="sdi-mini-card">
<div className="sdi-mini-label">DECKLINK DUO 2</div>
</div>
<div className="sdi-mini-ports">
{ports.map(p => (
<button key={p.idx} className={`sdi-mini-port ${selected === p.idx ? "active" : ""} ${p.active ? "live" : ""}`} onClick={() => onSelect(p.idx)}>
<span className="sdi-mini-radio">
<span className="sdi-mini-radio-dot" />
</span>
<span style={{ fontSize: 12, fontWeight: 600 }}>SDI {p.idx}</span>
<span style={{ fontSize: 11, color: "var(--text-3)", marginLeft: 6 }}>{p.label}</span>
{p.active && <span className="badge success" style={{ marginLeft: "auto" }}>{p.signal}</span>}
</button>
))}
</div>
</div>
);
}
window.NewRecorderModal = NewRecorderModal;