feat(ui): SRT/RTMP listener/caller mode UI in recorders

- SRT: mode selector (Listener / Caller)
  - Listener: listen_port field + live connection info banner
  - Caller: source URL field
- RTMP: mode selector (Listener / Caller)
  - Listener: listen_port + stream_key fields + live connection info banner
  - Caller: source URL field
- Connection info banners update live as port/key fields change
- handleCreateRecorder builds correct source_config per mode
- Card meta display handles listener config (shows port, not url)
- updateSrtModeFields / updateRtmpModeFields helpers for dynamic show/hide
This commit is contained in:
Zac Gaetano 2026-05-16 08:23:24 -04:00
parent 7aa07c6708
commit 0a5b4d6191

View file

@ -192,6 +192,10 @@
color: var(--color-text-primary);
font-weight: 500;
font-family: 'Courier New', monospace;
font-size: 0.8rem;
word-break: break-all;
text-align: right;
max-width: 60%;
}
.recorder-duration {
@ -402,6 +406,38 @@
display: flex;
}
.connection-info {
background-color: var(--color-bg-primary);
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.connection-info-label {
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.connection-info-label.srt {
color: var(--color-warning);
}
.connection-info-label.rtmp {
color: var(--color-success);
}
.connection-info-url {
font-family: 'Courier New', monospace;
font-size: 0.85rem;
color: var(--color-text-secondary);
word-break: break-all;
}
@media (max-width: 768px) {
.recorders-grid {
grid-template-columns: 1fr;
@ -487,7 +523,7 @@
</select>
</div>
<!-- Source Config Fields -->
<!-- Source Config Fields — rebuilt dynamically per source type -->
<div id="sourceConfigFields" class="conditional-fields"></div>
<div class="form-group">
@ -657,6 +693,15 @@
updateStatusBar();
}
function getSourceDisplay(recorder) {
const cfg = recorder.source_config || {};
if (cfg.mode === 'listener') {
const port = cfg.listen_port || (recorder.source_type === 'srt' ? 9000 : 1935);
return `Listen :${port}`;
}
return cfg.url || cfg.device || '—';
}
function createRecorderCard(recorder) {
const isRecording = recorder.status === 'recording';
const card = document.createElement('div');
@ -664,6 +709,7 @@
const sourceTypeLower = (recorder.source_type || '').toLowerCase();
const sourceTypeClass = ['sdi', 'srt', 'rtmp'].includes(sourceTypeLower) ? sourceTypeLower : 'sdi';
const sourceDisplay = getSourceDisplay(recorder);
card.innerHTML = `
<div class="recorder-thumbnail ${isRecording ? 'recording' : ''}">
@ -682,7 +728,7 @@
<div class="recorder-meta">
<div class="recorder-meta-item">
<span class="recorder-meta-label">Source</span>
<span class="recorder-meta-value">${recorder.source_config?.url || recorder.source_config?.device || '—'}</span>
<span class="recorder-meta-value">${sourceDisplay}</span>
</div>
<div class="recorder-meta-item">
<span class="recorder-meta-label">Codec</span>
@ -724,6 +770,7 @@
document.getElementById('recordingCodec').value = 'prores_hq';
document.getElementById('resolution').value = 'native';
document.getElementById('proxyToggle').classList.remove('on');
document.getElementById('proxyFields').classList.remove('visible');
recorderState.proxyEnabled = false;
updateSourceFields();
}
@ -732,6 +779,11 @@
document.getElementById('createModal').classList.remove('active');
}
// ============================================================
// SOURCE FIELD BUILDERS
// Rebuilt each time the source type selector changes.
// ============================================================
function updateSourceFields() {
const sourceType = document.getElementById('sourceType').value;
const fieldsContainer = document.getElementById('sourceConfigFields');
@ -749,34 +801,125 @@
</div>
`;
fieldsContainer.classList.add('visible');
} else if (sourceType === 'srt') {
fieldsContainer.innerHTML = `
<div class="form-group">
<label class="form-label">SRT URL</label>
<input type="text" id="srtUrl" class="form-input" placeholder="srt://host:port">
</div>
<div class="form-group">
<label class="form-label">Mode</label>
<select id="srtMode" class="form-select">
<option value="listener">Listener</option>
<option value="caller">Caller</option>
<select id="srtMode" class="form-select" onchange="updateSrtModeFields()">
<option value="listener">Listener — encoder pushes to this recorder</option>
<option value="caller">Caller — recorder pulls from source</option>
</select>
</div>
<!-- Listener fields (default) -->
<div id="srtListenerFields" style="display:flex;flex-direction:column;gap:var(--spacing-md);">
<div class="form-group">
<label class="form-label">Listen Port</label>
<input type="number" id="srtListenPort" class="form-input"
value="9000" min="1024" max="65535"
oninput="refreshSrtInfo()">
</div>
<div class="connection-info">
<div class="connection-info-label srt">📡 Push SRT to this address</div>
<div class="connection-info-url" id="srtConnectionInfo">
srt://&lt;server-ip&gt;:9000?mode=caller
</div>
</div>
</div>
<!-- Caller fields (hidden initially) -->
<div id="srtCallerFields" style="display:none;flex-direction:column;gap:var(--spacing-md);">
<div class="form-group">
<label class="form-label">SRT Source URL</label>
<input type="text" id="srtUrl" class="form-input"
placeholder="srt://host:port">
</div>
</div>
`;
fieldsContainer.classList.add('visible');
} else if (sourceType === 'rtmp') {
fieldsContainer.innerHTML = `
<div class="form-group">
<label class="form-label">RTMP URL</label>
<input type="text" id="rtmpUrl" class="form-input" placeholder="rtmp://host:port/app/stream">
<label class="form-label">Mode</label>
<select id="rtmpMode" class="form-select" onchange="updateRtmpModeFields()">
<option value="listener">Listener — encoder pushes to this recorder</option>
<option value="caller">Caller — recorder pulls from source</option>
</select>
</div>
<!-- Listener fields (default) -->
<div id="rtmpListenerFields" style="display:flex;flex-direction:column;gap:var(--spacing-md);">
<div class="form-group">
<label class="form-label">Listen Port</label>
<input type="number" id="rtmpListenPort" class="form-input"
value="1935" min="1024" max="65535"
oninput="refreshRtmpInfo()">
</div>
<div class="form-group">
<label class="form-label">Stream Key</label>
<input type="text" id="rtmpStreamKey" class="form-input"
value="stream" placeholder="e.g., live"
oninput="refreshRtmpInfo()">
</div>
<div class="connection-info">
<div class="connection-info-label rtmp">📡 Push RTMP to this address</div>
<div class="connection-info-url" id="rtmpConnectionInfo">
rtmp://&lt;server-ip&gt;:1935/live/stream
</div>
</div>
</div>
<!-- Caller fields (hidden initially) -->
<div id="rtmpCallerFields" style="display:none;flex-direction:column;gap:var(--spacing-md);">
<div class="form-group">
<label class="form-label">RTMP Source URL</label>
<input type="text" id="rtmpUrl" class="form-input"
placeholder="rtmp://host:port/app/stream">
</div>
</div>
`;
fieldsContainer.classList.add('visible');
} else {
fieldsContainer.classList.remove('visible');
}
}
// Update SRT fields when mode selector changes
function updateSrtModeFields() {
const mode = document.getElementById('srtMode').value;
document.getElementById('srtListenerFields').style.display =
mode === 'listener' ? 'flex' : 'none';
document.getElementById('srtCallerFields').style.display =
mode === 'caller' ? 'flex' : 'none';
}
// Refresh SRT connection info as port field changes
function refreshSrtInfo() {
const port = document.getElementById('srtListenPort').value || '9000';
const el = document.getElementById('srtConnectionInfo');
if (el) el.textContent = `srt://<server-ip>:${port}?mode=caller`;
}
// Update RTMP fields when mode selector changes
function updateRtmpModeFields() {
const mode = document.getElementById('rtmpMode').value;
document.getElementById('rtmpListenerFields').style.display =
mode === 'listener' ? 'flex' : 'none';
document.getElementById('rtmpCallerFields').style.display =
mode === 'caller' ? 'flex' : 'none';
}
// Refresh RTMP connection info as port/key fields change
function refreshRtmpInfo() {
const port = document.getElementById('rtmpListenPort').value || '1935';
const key = document.getElementById('rtmpStreamKey').value || 'stream';
const el = document.getElementById('rtmpConnectionInfo');
if (el) el.textContent = `rtmp://<server-ip>:${port}/live/${key}`;
}
function toggleProxy() {
const toggle = document.getElementById('proxyToggle');
const fields = document.getElementById('proxyFields');
@ -810,11 +953,38 @@
if (sourceType === 'sdi') {
sourceConfig.device = document.getElementById('sdiDevice').value;
} else if (sourceType === 'srt') {
sourceConfig.url = document.getElementById('srtUrl').value;
sourceConfig.mode = document.getElementById('srtMode').value;
const mode = document.getElementById('srtMode').value;
sourceConfig.mode = mode;
if (mode === 'listener') {
sourceConfig.listen_port =
parseInt(document.getElementById('srtListenPort').value, 10) || 9000;
} else {
const url = document.getElementById('srtUrl').value.trim();
if (!url) {
alert('SRT caller mode requires a source URL');
return;
}
sourceConfig.url = url;
}
} else if (sourceType === 'rtmp') {
sourceConfig.url = document.getElementById('rtmpUrl').value;
const mode = document.getElementById('rtmpMode').value;
sourceConfig.mode = mode;
if (mode === 'listener') {
sourceConfig.listen_port =
parseInt(document.getElementById('rtmpListenPort').value, 10) || 1935;
sourceConfig.stream_key =
document.getElementById('rtmpStreamKey').value.trim() || 'stream';
} else {
const url = document.getElementById('rtmpUrl').value.trim();
if (!url) {
alert('RTMP caller mode requires a source URL');
return;
}
sourceConfig.url = url;
}
}
const data = {