cdf04bd8eb
Lint & Validate / lint (push) Has been cancelled
- src/streaming/ module: camera detection (USB webcam + Pi Camera Module via v4l2/libcamera), GStreamer pipeline builder with V4L2/OMX/x264 h264 encoders, platform presets (YouTube, Twitch, Facebook, custom RTMP), streamer lifecycle manager with auto-reconnect, scene management, stream statistics - Keyboard shortcut controller (evdev + pynput fallback) for stream control: Ctrl+Shift+S (start/stop), Ctrl+Shift+1-9 (scene switch), Ctrl+Shift+R (reconnect) - REST API: /api/v1/stream/* endpoints for start/stop/status, scenes, cameras, platforms, and hotkey listing — integrated into NetworkServer - StreamStateProvider bridging layer connects REST routes to Streamer instance - GStreamer pipeline supports ALSA/JACK/PulseAudio audio capture and V4L2/libcamera video capture with H.264 hardware encoding on RPi4B - Low-latency streaming settings for live performance (zerolatency, no B-frames) - 89 streaming tests (709 total project tests pass)
220 lines
12 KiB
HTML
220 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<meta name="theme-color" content="#1a1a2e">
|
||
<title>RPi Audio Mixer</title>
|
||
<link rel="stylesheet" href="/style.css">
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
|
||
<!-- ═══ Toolbar ═══ -->
|
||
<header id="toolbar">
|
||
<h1>🎚️ RPi Mixer</h1>
|
||
<div id="connection">
|
||
<span id="conn-status" class="status disconnected">● Offline</span>
|
||
<input type="text" id="apikey-input" placeholder="API Key" value="mixer-local" size="14">
|
||
<button id="login-btn" class="btn" onclick="App.login()">Login</button>
|
||
<button id="connect-btn" class="btn" onclick="App.connectWS()" disabled>Connect</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- ═══ Tab bar ═══ -->
|
||
<nav id="tabs">
|
||
<button class="tab active" data-view="mixer" onclick="App.showView('mixer')">
|
||
<span class="tab-icon">🎛️</span> Mixer
|
||
</button>
|
||
<button class="tab" data-view="routing" onclick="App.showView('routing')">
|
||
<span class="tab-icon">🔀</span> Routing
|
||
</button>
|
||
<button class="tab" data-view="plugins" onclick="App.showView('plugins')">
|
||
<span class="tab-icon">🔌</span> Plugins
|
||
</button>
|
||
<button class="tab" data-view="sessions" onclick="App.showView('sessions')">
|
||
<span class="tab-icon">💾</span> Sessions
|
||
</button>
|
||
</nav>
|
||
|
||
<!-- ═══ Transport bar (global) ═══ -->
|
||
<div id="transport-bar">
|
||
<button class="tbtn" id="btn-play" onclick="App.sendTransport('play')" title="Play">▶</button>
|
||
<button class="tbtn" id="btn-stop" onclick="App.sendTransport('stop')" title="Stop">■</button>
|
||
<button class="tbtn" id="btn-record" onclick="App.sendTransport('record')" title="Record">●</button>
|
||
<button class="tbtn" id="btn-loop" onclick="App.sendTransport('loop')" title="Loop">↻</button>
|
||
<button class="tbtn" id="btn-locate" onclick="App.showLocateDialog()" title="Locate">⏱️</button>
|
||
<span class="transport-info" id="tempo-display">120.0 BPM</span>
|
||
<span class="transport-info" id="time-display">00:00:00</span>
|
||
<span class="transport-info dimmed" id="uptime-display"></span>
|
||
</div>
|
||
|
||
<!-- ═══ Main content ═══ -->
|
||
<main id="view-container">
|
||
|
||
<!-- ── Mixer View ── -->
|
||
<section id="view-mixer" class="view active">
|
||
<div id="mixer-scroll">
|
||
<div id="channels"></div>
|
||
<div id="master-section">
|
||
<div class="section-label">Master</div>
|
||
<div class="fader-container" id="master-fader">
|
||
<div class="meter-v" id="master-meter"><div class="meter-fill"></div></div>
|
||
<input type="range" min="-60" max="12" value="0" step="0.5" orient="vertical"
|
||
oninput="App.sendParam('master_volume', this.value, -1)">
|
||
<span class="fader-label">Master</span>
|
||
<span class="fader-db" id="master-vol-val">0.0 dB</span>
|
||
</div>
|
||
<div class="master-buttons">
|
||
<button class="toggle-btn" id="btn-mute-master" onclick="App.toggleParam('master_mute', -1)">M</button>
|
||
<button class="toggle-btn" id="btn-dim" onclick="App.toggleParam('master_dim', -1)">Dim</button>
|
||
<button class="toggle-btn" id="btn-mono" onclick="App.toggleParam('master_mono', -1)">Mono</button>
|
||
</div>
|
||
<div class="master-buses" id="master-buses-info"></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ── Routing View ── -->
|
||
<section id="view-routing" class="view">
|
||
<div class="routing-layout">
|
||
<div class="routing-panel sources-panel">
|
||
<h3>Sources</h3>
|
||
<div id="routing-sources" class="routing-list"></div>
|
||
</div>
|
||
<div class="routing-panel matrix-panel">
|
||
<h3>Connections</h3>
|
||
<div id="routing-matrix" class="routing-grid"></div>
|
||
</div>
|
||
<div class="routing-panel dests-panel">
|
||
<h3>Destinations</h3>
|
||
<div id="routing-dests" class="routing-list"></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ── Plugins View ── -->
|
||
<section id="view-plugins" class="view">
|
||
<div class="plugins-header">
|
||
<h3>Channel Plugins</h3>
|
||
<select id="plugin-channel-select" onchange="App.showPluginsForChannel(this.value)">
|
||
<option value="all">All Channels</option>
|
||
</select>
|
||
</div>
|
||
<div id="plugins-list" class="plugins-grid"></div>
|
||
</section>
|
||
|
||
<!-- ── Sessions View ── -->
|
||
<section id="view-sessions" class="view">
|
||
<div class="sessions-header">
|
||
<h3>Mixer Sessions</h3>
|
||
<div class="sessions-actions">
|
||
<input type="text" id="session-name-input" placeholder="Session name" class="text-input">
|
||
<input type="text" id="session-notes-input" placeholder="Notes (optional)" class="text-input" style="width:160px">
|
||
<button class="btn btn-primary" onclick="App.saveSession()">💾 Save Session</button>
|
||
<button class="btn" onclick="App.refreshSessions()">🔄 Refresh</button>
|
||
<select id="session-filter" onchange="App.refreshSessions()" style="width:100px">
|
||
<option value="all">All</option>
|
||
<option value="sessions">Sessions</option>
|
||
<option value="snapshots">Snapshots</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<!-- Auto-save status -->
|
||
<div id="autosave-status" class="autosave-status"><span id="autosave-indicator">● Auto-save active</span></div>
|
||
|
||
<!-- Sub-tabs for Sessions / Setlists / Snapshots -->
|
||
<div class="sessions-subtabs">
|
||
<button class="subtab active" id="subtab-sessions" onclick="App.showSessionsSubtab('sessions')">📁 Sessions</button>
|
||
<button class="subtab" id="subtab-setlists" onclick="App.showSessionsSubtab('setlists')">📋 Setlists</button>
|
||
<button class="subtab" id="subtab-snapshots" onclick="App.showSessionsSubtab('snapshots')">📸 Snapshots</button>
|
||
</div>
|
||
|
||
<!-- Sessions list -->
|
||
<div id="sessions-list" class="sessions-grid"></div>
|
||
|
||
<!-- Setlist view (hidden by default) -->
|
||
<div id="setlists-view" style="display:none">
|
||
<div class="sessions-actions" style="margin-bottom:0.5rem">
|
||
<input type="text" id="setlist-name-input" placeholder="Setlist name" class="text-input">
|
||
<button class="btn btn-primary" onclick="App.createSetlist()">+ New Setlist</button>
|
||
</div>
|
||
<div id="setlists-list" class="sessions-grid"></div>
|
||
<div id="setlist-editor" style="display:none">
|
||
<h4>Setlist: <span id="setlist-editor-title"></span></h4>
|
||
<div id="setlist-entries"></div>
|
||
<div class="sessions-actions">
|
||
<input type="text" id="setlist-entry-session" placeholder="Session name" class="text-input">
|
||
<select id="setlist-entry-transition"><option value="cut">Cut</option><option value="crossfade">Crossfade</option><option value="wait">Wait</option></select>
|
||
<button class="btn" onclick="App.addSetlistEntry()">+ Add Entry</button>
|
||
<button class="btn btn-primary" onclick="App.saveSetlist()">💾 Save Setlist</button>
|
||
<button class="btn" onclick="App.activateSetlist()">▶ Activate</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Snapshots view (hidden by default) -->
|
||
<div id="snapshots-view" style="display:none">
|
||
<div class="sessions-actions" style="margin-bottom:0.5rem">
|
||
<input type="text" id="snapshot-name-input" placeholder="Snapshot name" class="text-input">
|
||
<select id="snapshot-category-input" style="width:100px">
|
||
<option value="">No Category</option><option value="intro">Intro</option><option value="verse">Verse</option><option value="chorus">Chorus</option><option value="bridge">Bridge</option><option value="outro">Outro</option>
|
||
</select>
|
||
<button class="btn btn-primary" onclick="App.captureSnapshot()">📸 Capture</button>
|
||
<button class="btn" onclick="App.snapshotNext()">▶ Next</button>
|
||
<button class="btn" onclick="App.snapshotPrev()">◀ Prev</button>
|
||
</div>
|
||
<div id="snapshots-list" class="sessions-grid"></div>
|
||
</div>
|
||
|
||
<!-- Import/Export -->
|
||
<div class="sessions-actions" style="margin-top:1rem; border-top:1px solid var(--border-color); padding-top:0.5rem">
|
||
<button class="btn" onclick="App.importSession()">📥 Import</button>
|
||
<button class="btn" onclick="App.exportAllSessions()">📤 Export All</button>
|
||
<input type="file" id="session-import-file" accept=".json" style="display:none" onchange="App.handleImportFile(this)">
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<!-- ═══ Channel EQ modal ═══ -->
|
||
<div id="eq-modal" class="modal hidden">
|
||
<div class="modal-backdrop" onclick="App.closeEQ()"></div>
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3 id="eq-modal-title">Channel EQ</h3>
|
||
<button class="modal-close" onclick="App.closeEQ()">✕</button>
|
||
</div>
|
||
<div id="eq-controls">
|
||
<label class="eq-toggle">
|
||
<input type="checkbox" id="eq-enable" onchange="App.sendParam('eq_enable', this.checked ? 1 : 0, App.eqChannel)">
|
||
Enable EQ
|
||
</label>
|
||
<div class="eq-bands" id="eq-bands"></div>
|
||
</div>
|
||
<div id="dyn-controls">
|
||
<h4>Compressor</h4>
|
||
<div class="param-rows" id="comp-params"></div>
|
||
<h4>Noise Gate</h4>
|
||
<div class="param-rows" id="gate-params"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ Log panel ═══ -->
|
||
<div id="log-panel">
|
||
<div id="log-header">
|
||
<span id="log-toggle" onclick="App.toggleLog()">📋 Log ▾</span>
|
||
</div>
|
||
<div id="log-body" class="collapsed">
|
||
<div id="log"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/app.js"></script>
|
||
</body>
|
||
</html>
|