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:
parent
7aa07c6708
commit
0a5b4d6191
1 changed files with 184 additions and 14 deletions
|
|
@ -192,6 +192,10 @@
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
word-break: break-all;
|
||||||
|
text-align: right;
|
||||||
|
max-width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recorder-duration {
|
.recorder-duration {
|
||||||
|
|
@ -402,6 +406,38 @@
|
||||||
display: flex;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.recorders-grid {
|
.recorders-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
@ -487,7 +523,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Source Config Fields -->
|
<!-- Source Config Fields — rebuilt dynamically per source type -->
|
||||||
<div id="sourceConfigFields" class="conditional-fields"></div>
|
<div id="sourceConfigFields" class="conditional-fields"></div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -657,6 +693,15 @@
|
||||||
updateStatusBar();
|
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) {
|
function createRecorderCard(recorder) {
|
||||||
const isRecording = recorder.status === 'recording';
|
const isRecording = recorder.status === 'recording';
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
|
|
@ -664,6 +709,7 @@
|
||||||
|
|
||||||
const sourceTypeLower = (recorder.source_type || '').toLowerCase();
|
const sourceTypeLower = (recorder.source_type || '').toLowerCase();
|
||||||
const sourceTypeClass = ['sdi', 'srt', 'rtmp'].includes(sourceTypeLower) ? sourceTypeLower : 'sdi';
|
const sourceTypeClass = ['sdi', 'srt', 'rtmp'].includes(sourceTypeLower) ? sourceTypeLower : 'sdi';
|
||||||
|
const sourceDisplay = getSourceDisplay(recorder);
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="recorder-thumbnail ${isRecording ? 'recording' : ''}">
|
<div class="recorder-thumbnail ${isRecording ? 'recording' : ''}">
|
||||||
|
|
@ -682,7 +728,7 @@
|
||||||
<div class="recorder-meta">
|
<div class="recorder-meta">
|
||||||
<div class="recorder-meta-item">
|
<div class="recorder-meta-item">
|
||||||
<span class="recorder-meta-label">Source</span>
|
<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>
|
||||||
<div class="recorder-meta-item">
|
<div class="recorder-meta-item">
|
||||||
<span class="recorder-meta-label">Codec</span>
|
<span class="recorder-meta-label">Codec</span>
|
||||||
|
|
@ -724,6 +770,7 @@
|
||||||
document.getElementById('recordingCodec').value = 'prores_hq';
|
document.getElementById('recordingCodec').value = 'prores_hq';
|
||||||
document.getElementById('resolution').value = 'native';
|
document.getElementById('resolution').value = 'native';
|
||||||
document.getElementById('proxyToggle').classList.remove('on');
|
document.getElementById('proxyToggle').classList.remove('on');
|
||||||
|
document.getElementById('proxyFields').classList.remove('visible');
|
||||||
recorderState.proxyEnabled = false;
|
recorderState.proxyEnabled = false;
|
||||||
updateSourceFields();
|
updateSourceFields();
|
||||||
}
|
}
|
||||||
|
|
@ -732,6 +779,11 @@
|
||||||
document.getElementById('createModal').classList.remove('active');
|
document.getElementById('createModal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SOURCE FIELD BUILDERS
|
||||||
|
// Rebuilt each time the source type selector changes.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
function updateSourceFields() {
|
function updateSourceFields() {
|
||||||
const sourceType = document.getElementById('sourceType').value;
|
const sourceType = document.getElementById('sourceType').value;
|
||||||
const fieldsContainer = document.getElementById('sourceConfigFields');
|
const fieldsContainer = document.getElementById('sourceConfigFields');
|
||||||
|
|
@ -749,34 +801,125 @@
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
fieldsContainer.classList.add('visible');
|
fieldsContainer.classList.add('visible');
|
||||||
|
|
||||||
} else if (sourceType === 'srt') {
|
} else if (sourceType === 'srt') {
|
||||||
fieldsContainer.innerHTML = `
|
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">
|
<div class="form-group">
|
||||||
<label class="form-label">Mode</label>
|
<label class="form-label">Mode</label>
|
||||||
<select id="srtMode" class="form-select">
|
<select id="srtMode" class="form-select" onchange="updateSrtModeFields()">
|
||||||
<option value="listener">Listener</option>
|
<option value="listener">Listener — encoder pushes to this recorder</option>
|
||||||
<option value="caller">Caller</option>
|
<option value="caller">Caller — recorder pulls from source</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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://<server-ip>: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');
|
fieldsContainer.classList.add('visible');
|
||||||
|
|
||||||
} else if (sourceType === 'rtmp') {
|
} else if (sourceType === 'rtmp') {
|
||||||
fieldsContainer.innerHTML = `
|
fieldsContainer.innerHTML = `
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">RTMP URL</label>
|
<label class="form-label">Mode</label>
|
||||||
<input type="text" id="rtmpUrl" class="form-input" placeholder="rtmp://host:port/app/stream">
|
<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://<server-ip>: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>
|
</div>
|
||||||
`;
|
`;
|
||||||
fieldsContainer.classList.add('visible');
|
fieldsContainer.classList.add('visible');
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
fieldsContainer.classList.remove('visible');
|
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() {
|
function toggleProxy() {
|
||||||
const toggle = document.getElementById('proxyToggle');
|
const toggle = document.getElementById('proxyToggle');
|
||||||
const fields = document.getElementById('proxyFields');
|
const fields = document.getElementById('proxyFields');
|
||||||
|
|
@ -810,11 +953,38 @@
|
||||||
|
|
||||||
if (sourceType === 'sdi') {
|
if (sourceType === 'sdi') {
|
||||||
sourceConfig.device = document.getElementById('sdiDevice').value;
|
sourceConfig.device = document.getElementById('sdiDevice').value;
|
||||||
|
|
||||||
} else if (sourceType === 'srt') {
|
} else if (sourceType === 'srt') {
|
||||||
sourceConfig.url = document.getElementById('srtUrl').value;
|
const mode = document.getElementById('srtMode').value;
|
||||||
sourceConfig.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') {
|
} 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 = {
|
const data = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue