969 lines
35 KiB
HTML
969 lines
35 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Wild Dragon - Recorders</title>
|
|
<link rel="stylesheet" href="/css/common.css">
|
|
<style>
|
|
.recorders-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: calc(100vh - 110px);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.recorders-content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: var(--spacing-lg);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-lg);
|
|
}
|
|
|
|
.recorders-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.recorders-header h1 {
|
|
margin: 0;
|
|
}
|
|
|
|
.recorders-controls {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.recorders-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
}
|
|
|
|
.recorder-card {
|
|
background-color: var(--color-bg-tertiary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
transition: all var(--transition-normal);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.recorder-card:hover {
|
|
border-color: var(--color-accent-primary);
|
|
box-shadow: 0 8px 24px rgba(233, 69, 96, 0.15);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.recorder-thumbnail {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 200px;
|
|
background-color: var(--color-bg-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 3rem;
|
|
opacity: 0.3;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.recorder-thumbnail.recording {
|
|
background: linear-gradient(45deg, rgba(239, 68, 68, 0.2), rgba(233, 69, 96, 0.2));
|
|
}
|
|
|
|
.recorder-status-badge {
|
|
position: absolute;
|
|
top: var(--spacing-md);
|
|
left: var(--spacing-md);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
padding: 6px 12px;
|
|
background-color: rgba(0, 0, 0, 0.6);
|
|
border-radius: 20px;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
color: white;
|
|
}
|
|
|
|
.recorder-status-badge.recording {
|
|
background-color: rgba(239, 68, 68, 0.9);
|
|
animation: pulse 1s infinite;
|
|
}
|
|
|
|
.recorder-status-badge.stopped {
|
|
background-color: rgba(75, 85, 99, 0.9);
|
|
}
|
|
|
|
.recorder-status-badge.error {
|
|
background-color: rgba(239, 68, 68, 0.9);
|
|
}
|
|
|
|
.recording-indicator {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background-color: currentColor;
|
|
animation: pulse 1s infinite;
|
|
}
|
|
|
|
.recorder-content {
|
|
padding: var(--spacing-md);
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.recorder-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-md);
|
|
padding-bottom: var(--spacing-md);
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
|
|
.recorder-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--color-text-primary);
|
|
word-break: break-word;
|
|
}
|
|
|
|
.recorder-type-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
padding: 4px 10px;
|
|
background-color: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
color: var(--color-text-secondary);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.recorder-type-badge.sdi {
|
|
border-color: var(--color-info);
|
|
color: var(--color-info);
|
|
}
|
|
|
|
.recorder-type-badge.srt {
|
|
border-color: var(--color-warning);
|
|
color: var(--color-warning);
|
|
}
|
|
|
|
.recorder-type-badge.rtmp {
|
|
border-color: var(--color-success);
|
|
color: var(--color-success);
|
|
}
|
|
|
|
.recorder-meta {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-sm);
|
|
margin-bottom: var(--spacing-md);
|
|
font-size: 0.9rem;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.recorder-meta-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.recorder-meta-label {
|
|
color: var(--color-text-tertiary);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.recorder-meta-value {
|
|
color: var(--color-text-primary);
|
|
font-weight: 500;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
|
|
.recorder-duration {
|
|
font-size: 0.9rem;
|
|
color: var(--color-text-secondary);
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
|
|
.recorder-actions {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
margin-top: auto;
|
|
}
|
|
|
|
.recorder-actions button {
|
|
flex: 1;
|
|
}
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 400px;
|
|
text-align: center;
|
|
color: var(--color-text-tertiary);
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 3rem;
|
|
margin-bottom: var(--spacing-md);
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.empty-state-text {
|
|
color: var(--color-text-tertiary);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
/* Modal Styles */
|
|
.modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.6);
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-overlay.active {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal {
|
|
background-color: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-lg);
|
|
max-width: 600px;
|
|
width: 90%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--spacing-lg);
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.modal-close {
|
|
background: none;
|
|
border: none;
|
|
color: var(--color-text-secondary);
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal-close:hover {
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.modal-body {
|
|
padding: var(--spacing-lg);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: var(--spacing-md);
|
|
padding: var(--spacing-lg);
|
|
border-top: 1px solid var(--color-border);
|
|
background-color: var(--color-bg-tertiary);
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.form-label {
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
color: var(--color-text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.3px;
|
|
}
|
|
|
|
.form-input,
|
|
.form-select {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
background-color: var(--color-bg-primary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
color: var(--color-text-primary);
|
|
font-family: var(--font-family);
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.form-input:focus,
|
|
.form-select:focus {
|
|
outline: none;
|
|
border-color: var(--color-accent-primary);
|
|
background-color: var(--color-bg-secondary);
|
|
}
|
|
|
|
.form-select {
|
|
cursor: pointer;
|
|
appearance: none;
|
|
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%23b0b0b0' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
|
|
background-position: right var(--spacing-sm) center;
|
|
background-repeat: no-repeat;
|
|
background-size: 1.5em 1.5em;
|
|
padding-right: 2.5rem;
|
|
}
|
|
|
|
.form-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.toggle-switch {
|
|
position: relative;
|
|
width: 50px;
|
|
height: 28px;
|
|
background-color: var(--color-bg-primary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 20px;
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.toggle-switch.on {
|
|
background-color: var(--color-accent-primary);
|
|
border-color: var(--color-accent-primary);
|
|
}
|
|
|
|
.toggle-switch::after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 22px;
|
|
height: 22px;
|
|
background-color: white;
|
|
border-radius: 50%;
|
|
top: 2px;
|
|
left: 2px;
|
|
transition: left var(--transition-fast);
|
|
}
|
|
|
|
.toggle-switch.on::after {
|
|
left: 26px;
|
|
}
|
|
|
|
.conditional-fields {
|
|
display: none;
|
|
padding: var(--spacing-md);
|
|
background-color: var(--color-bg-tertiary);
|
|
border-radius: var(--radius-md);
|
|
border: 1px solid var(--color-border);
|
|
gap: var(--spacing-md);
|
|
flex-direction: column;
|
|
}
|
|
|
|
.conditional-fields.visible {
|
|
display: flex;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.recorders-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-container">
|
|
<!-- Header -->
|
|
<header class="header">
|
|
<div class="header-logo">
|
|
<div class="header-logo-icon">D</div>
|
|
<span>WILD DRAGON</span>
|
|
</div>
|
|
<nav class="header-nav">
|
|
<div class="nav-item" data-page="assets">Assets</div>
|
|
<div class="nav-item" data-page="capture">Capture</div>
|
|
<div class="nav-item" data-page="upload">Upload</div>
|
|
<div class="nav-item active" data-page="recorders">Recorders</div>
|
|
</nav>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<div class="main-content">
|
|
<div class="content-area">
|
|
<div class="recorders-container">
|
|
<div class="recorders-content">
|
|
<!-- Header -->
|
|
<div class="recorders-header">
|
|
<h1>Recorder Management</h1>
|
|
<div class="recorders-controls">
|
|
<button class="btn btn-primary" onclick="openCreateModal()">+ New Recorder</button>
|
|
<button class="btn btn-secondary" id="backBtn" onclick="navigateTo('assets')">← Back</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recorders Grid -->
|
|
<div class="recorders-grid" id="recordersGrid">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🎬</div>
|
|
<div class="empty-state-text">No recorders configured</div>
|
|
<p style="color: var(--color-text-tertiary); font-size: 0.9rem;">Click "New Recorder" to create your first recorder instance</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Bar -->
|
|
<footer class="status-bar">
|
|
<div class="status-item">
|
|
<span class="status-indicator" id="statusIndicator"></span>
|
|
<span id="statusText">Loading recorders...</span>
|
|
</div>
|
|
<div class="status-item" id="recorderCountItem" style="display: none;">
|
|
<span id="recorderCount">0</span> recorder(s)
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
|
|
<!-- Create Recorder Modal -->
|
|
<div class="modal-overlay" id="createModal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<div class="modal-title">New Recorder</div>
|
|
<button class="modal-close" onclick="closeCreateModal()">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label class="form-label">Recorder Name</label>
|
|
<input type="text" id="recorderName" class="form-input" placeholder="e.g., Studio A SDI">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Source Type</label>
|
|
<select id="sourceType" class="form-select" onchange="updateSourceFields()">
|
|
<option value="">Select source type...</option>
|
|
<option value="sdi">SDI (DeckLink)</option>
|
|
<option value="srt">SRT (Streaming)</option>
|
|
<option value="rtmp">RTMP (Stream)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Source Config Fields -->
|
|
<div id="sourceConfigFields" class="conditional-fields"></div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Recording Codec</label>
|
|
<select id="recordingCodec" class="form-select">
|
|
<option value="prores_hq">ProRes HQ</option>
|
|
<option value="prores_422">ProRes 422</option>
|
|
<option value="dnxhd_185">DNxHD 185</option>
|
|
<option value="dnxhr_hq">DNxHR HQ</option>
|
|
<option value="h264">H.264</option>
|
|
<option value="h265">H.265</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Resolution</label>
|
|
<select id="resolution" class="form-select">
|
|
<option value="native">Native</option>
|
|
<option value="4k">3840x2160 (4K)</option>
|
|
<option value="1080p">1920x1080 (Full HD)</option>
|
|
<option value="720p">1280x720 (HD)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-toggle">
|
|
<div>
|
|
<div class="form-label">Proxy Generation</div>
|
|
<div style="font-size: 0.8rem; color: var(--color-text-tertiary);">Generate lower-res proxy for faster editing</div>
|
|
</div>
|
|
<div class="toggle-switch" id="proxyToggle" onclick="toggleProxy()"></div>
|
|
</div>
|
|
|
|
<div id="proxyFields" class="conditional-fields">
|
|
<div class="form-group">
|
|
<label class="form-label">Proxy Codec</label>
|
|
<select id="proxyCodec" class="form-select">
|
|
<option value="h264">H.264</option>
|
|
<option value="h265">H.265</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Proxy Resolution</label>
|
|
<select id="proxyResolution" class="form-select">
|
|
<option value="1080p">1920x1080 (Full HD)</option>
|
|
<option value="720p">1280x720 (HD)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Project</label>
|
|
<select id="projectSelect" class="form-select"></select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="closeCreateModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="createRecorder()">Create</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/js/api.js"></script>
|
|
<script>
|
|
// ============================================================
|
|
// STATE MANAGEMENT
|
|
// ============================================================
|
|
|
|
let recorderState = {
|
|
recorders: [],
|
|
projects: [],
|
|
selectedProject: null,
|
|
proxyEnabled: false,
|
|
statusPollInterval: null,
|
|
};
|
|
|
|
// ============================================================
|
|
// INITIALIZATION
|
|
// ============================================================
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
setupEventListeners();
|
|
await loadRecorders();
|
|
await loadProjects();
|
|
startStatusPolling();
|
|
updateStatusBar();
|
|
});
|
|
|
|
function setupEventListeners() {
|
|
document.querySelectorAll('[data-page]').forEach(el => {
|
|
el.addEventListener('click', (e) => navigateTo(e.target.dataset.page));
|
|
});
|
|
|
|
document.getElementById('createModal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'createModal') closeCreateModal();
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// LOAD DATA
|
|
// ============================================================
|
|
|
|
async function loadRecorders() {
|
|
try {
|
|
const result = await getRecorders();
|
|
if (result.success) {
|
|
recorderState.recorders = result.data || [];
|
|
renderRecorders();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load recorders:', error);
|
|
}
|
|
}
|
|
|
|
async function loadProjects() {
|
|
try {
|
|
const result = await getProjects();
|
|
if (result.success) {
|
|
recorderState.projects = result.data || [];
|
|
renderProjectSelect();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load projects:', error);
|
|
}
|
|
}
|
|
|
|
function renderProjectSelect() {
|
|
const select = document.getElementById('projectSelect');
|
|
select.innerHTML = '';
|
|
|
|
recorderState.projects.forEach(project => {
|
|
const option = document.createElement('option');
|
|
option.value = project.id;
|
|
option.textContent = project.name;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
if (recorderState.projects.length > 0) {
|
|
recorderState.selectedProject = recorderState.projects[0].id;
|
|
select.value = recorderState.selectedProject;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// RENDERING
|
|
// ============================================================
|
|
|
|
function renderRecorders() {
|
|
const grid = document.getElementById('recordersGrid');
|
|
|
|
if (recorderState.recorders.length === 0) {
|
|
grid.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🎬</div>
|
|
<div class="empty-state-text">No recorders configured</div>
|
|
<p style="color: var(--color-text-tertiary); font-size: 0.9rem;">Click "New Recorder" to create your first recorder instance</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = '';
|
|
recorderState.recorders.forEach(recorder => {
|
|
const card = createRecorderCard(recorder);
|
|
grid.appendChild(card);
|
|
});
|
|
|
|
updateStatusBar();
|
|
}
|
|
|
|
function createRecorderCard(recorder) {
|
|
const isRecording = recorder.status === 'recording';
|
|
const card = document.createElement('div');
|
|
card.className = 'recorder-card';
|
|
|
|
const sourceTypeLower = (recorder.source_type || '').toLowerCase();
|
|
const sourceTypeClass = ['sdi', 'srt', 'rtmp'].includes(sourceTypeLower) ? sourceTypeLower : 'sdi';
|
|
|
|
card.innerHTML = `
|
|
<div class="recorder-thumbnail ${isRecording ? 'recording' : ''}">
|
|
🎥
|
|
<div class="recorder-status-badge ${recorder.status || 'stopped'}">
|
|
${isRecording ? '<span class="recording-indicator"></span>' : ''}
|
|
${recorder.status === 'recording' ? 'Recording' :
|
|
recorder.status === 'stopped' ? 'Stopped' : 'Error'}
|
|
</div>
|
|
</div>
|
|
<div class="recorder-content">
|
|
<div class="recorder-header">
|
|
<div class="recorder-title">${recorder.name || 'Untitled Recorder'}</div>
|
|
<div class="recorder-type-badge ${sourceTypeClass}">${recorder.source_type || 'Unknown'}</div>
|
|
</div>
|
|
<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>
|
|
</div>
|
|
<div class="recorder-meta-item">
|
|
<span class="recorder-meta-label">Codec</span>
|
|
<span class="recorder-meta-value">${recorder.recording_codec || '—'}</span>
|
|
</div>
|
|
<div class="recorder-meta-item">
|
|
<span class="recorder-meta-label">Resolution</span>
|
|
<span class="recorder-meta-value">${recorder.resolution || 'Native'}</span>
|
|
</div>
|
|
${isRecording ? `
|
|
<div class="recorder-meta-item">
|
|
<span class="recorder-meta-label">Duration</span>
|
|
<span class="recorder-duration" id="duration-${recorder.id}">00:00:00</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
<div class="recorder-actions">
|
|
${isRecording ? `
|
|
<button class="btn btn-sm btn-secondary" onclick="stopRecorder('${recorder.id}')">⏹ Stop</button>
|
|
` : `
|
|
<button class="btn btn-sm btn-primary" onclick="startRecorder('${recorder.id}')">⏺ Start</button>
|
|
`}
|
|
<button class="btn btn-sm btn-secondary" onclick="deleteRecorder('${recorder.id}')">🗑 Delete</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return card;
|
|
}
|
|
|
|
// ============================================================
|
|
// MODAL OPERATIONS
|
|
// ============================================================
|
|
|
|
function openCreateModal() {
|
|
document.getElementById('createModal').classList.add('active');
|
|
document.getElementById('recorderName').value = '';
|
|
document.getElementById('sourceType').value = '';
|
|
document.getElementById('recordingCodec').value = 'prores_hq';
|
|
document.getElementById('resolution').value = 'native';
|
|
document.getElementById('proxyToggle').classList.remove('on');
|
|
recorderState.proxyEnabled = false;
|
|
updateSourceFields();
|
|
}
|
|
|
|
function closeCreateModal() {
|
|
document.getElementById('createModal').classList.remove('active');
|
|
}
|
|
|
|
function updateSourceFields() {
|
|
const sourceType = document.getElementById('sourceType').value;
|
|
const fieldsContainer = document.getElementById('sourceConfigFields');
|
|
fieldsContainer.innerHTML = '';
|
|
|
|
if (sourceType === 'sdi') {
|
|
fieldsContainer.innerHTML = `
|
|
<div class="form-group">
|
|
<label class="form-label">Device</label>
|
|
<select id="sdiDevice" class="form-select">
|
|
<option value="">Select DeckLink device...</option>
|
|
<option value="decklink_0">DeckLink Card 1</option>
|
|
<option value="decklink_1">DeckLink Card 2</option>
|
|
</select>
|
|
</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>
|
|
</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">
|
|
</div>
|
|
`;
|
|
fieldsContainer.classList.add('visible');
|
|
} else {
|
|
fieldsContainer.classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
function toggleProxy() {
|
|
const toggle = document.getElementById('proxyToggle');
|
|
const fields = document.getElementById('proxyFields');
|
|
recorderState.proxyEnabled = !recorderState.proxyEnabled;
|
|
|
|
toggle.classList.toggle('on');
|
|
if (recorderState.proxyEnabled) {
|
|
fields.classList.add('visible');
|
|
} else {
|
|
fields.classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// RECORDER OPERATIONS
|
|
// ============================================================
|
|
|
|
async function createRecorder() {
|
|
const name = document.getElementById('recorderName').value.trim();
|
|
const sourceType = document.getElementById('sourceType').value;
|
|
|
|
if (!name || !sourceType) {
|
|
alert('Please fill in all required fields');
|
|
return;
|
|
}
|
|
|
|
let sourceConfig = {};
|
|
|
|
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;
|
|
} else if (sourceType === 'rtmp') {
|
|
sourceConfig.url = document.getElementById('rtmpUrl').value;
|
|
}
|
|
|
|
const data = {
|
|
name,
|
|
source_type: sourceType,
|
|
source_config: sourceConfig,
|
|
recording_codec: document.getElementById('recordingCodec').value,
|
|
resolution: document.getElementById('resolution').value,
|
|
project_id: document.getElementById('projectSelect').value,
|
|
};
|
|
|
|
if (recorderState.proxyEnabled) {
|
|
data.proxy_enabled = true;
|
|
data.proxy_codec = document.getElementById('proxyCodec').value;
|
|
data.proxy_resolution = document.getElementById('proxyResolution').value;
|
|
}
|
|
|
|
try {
|
|
const result = await createRecorder(data);
|
|
if (result.success) {
|
|
closeCreateModal();
|
|
await loadRecorders();
|
|
updateStatusBar('Recorder created');
|
|
} else {
|
|
alert('Failed to create recorder');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to create recorder:', error);
|
|
alert('Error creating recorder');
|
|
}
|
|
}
|
|
|
|
async function startRecorder(id) {
|
|
try {
|
|
const result = await startRecorder(id);
|
|
if (result.success) {
|
|
const recorder = recorderState.recorders.find(r => r.id === id);
|
|
if (recorder) {
|
|
recorder.status = 'recording';
|
|
recorder.start_time = Date.now();
|
|
renderRecorders();
|
|
updateStatusBar('Recorder started');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to start recorder:', error);
|
|
}
|
|
}
|
|
|
|
async function stopRecorder(id) {
|
|
try {
|
|
const result = await stopRecorder(id);
|
|
if (result.success) {
|
|
const recorder = recorderState.recorders.find(r => r.id === id);
|
|
if (recorder) {
|
|
recorder.status = 'stopped';
|
|
renderRecorders();
|
|
updateStatusBar('Recorder stopped');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to stop recorder:', error);
|
|
}
|
|
}
|
|
|
|
async function deleteRecorder(id) {
|
|
if (!confirm('Are you sure you want to delete this recorder?')) return;
|
|
|
|
try {
|
|
const result = await deleteRecorder(id);
|
|
if (result.success) {
|
|
recorderState.recorders = recorderState.recorders.filter(r => r.id !== id);
|
|
renderRecorders();
|
|
updateStatusBar('Recorder deleted');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to delete recorder:', error);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// STATUS POLLING
|
|
// ============================================================
|
|
|
|
function startStatusPolling() {
|
|
if (recorderState.statusPollInterval) {
|
|
clearInterval(recorderState.statusPollInterval);
|
|
}
|
|
|
|
recorderState.statusPollInterval = setInterval(async () => {
|
|
for (const recorder of recorderState.recorders) {
|
|
if (recorder.status === 'recording') {
|
|
const result = await getRecorderStatus(recorder.id);
|
|
if (result.success) {
|
|
recorder.status = result.data.status;
|
|
if (result.data.duration) {
|
|
const durationEl = document.getElementById(`duration-${recorder.id}`);
|
|
if (durationEl) {
|
|
durationEl.textContent = formatDuration(result.data.duration);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, 2000);
|
|
}
|
|
|
|
// ============================================================
|
|
// NAVIGATION
|
|
// ============================================================
|
|
|
|
function navigateTo(page) {
|
|
if (page === 'assets') {
|
|
window.location.href = '/index.html';
|
|
} else if (page === 'capture') {
|
|
window.location.href = '/capture.html';
|
|
} else if (page === 'upload') {
|
|
window.location.href = '/upload.html';
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// STATUS BAR
|
|
// ============================================================
|
|
|
|
function updateStatusBar() {
|
|
const indicator = document.getElementById('statusIndicator');
|
|
const text = document.getElementById('statusText');
|
|
const count = document.getElementById('recorderCountItem');
|
|
|
|
indicator.className = 'status-indicator';
|
|
text.textContent = 'Connected';
|
|
|
|
const recordingCount = recorderState.recorders.filter(r => r.status === 'recording').length;
|
|
if (recordingCount > 0) {
|
|
text.textContent = `${recordingCount} recorder(s) recording`;
|
|
}
|
|
|
|
if (recorderState.recorders.length > 0) {
|
|
count.style.display = 'flex';
|
|
document.getElementById('recorderCount').textContent = recorderState.recorders.length;
|
|
} else {
|
|
count.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
if (recorderState.statusPollInterval) {
|
|
clearInterval(recorderState.statusPollInterval);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|