P6-R1: Audio/video streaming pipeline
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)
This commit is contained in:
2026-05-19 22:13:29 -04:00
parent d6480a35ed
commit cdf04bd8eb
21 changed files with 7119 additions and 34 deletions
+62 -3
View File
@@ -109,13 +109,72 @@
<!-- ── Sessions View ── -->
<section id="view-sessions" class="view">
<div class="sessions-header">
<h3>Saved Sessions</h3>
<h3>Mixer Sessions</h3>
<div class="sessions-actions">
<input type="text" id="scene-name-input" placeholder="Scene name" class="text-input">
<button class="btn btn-primary" onclick="App.saveScene()">💾 Save Current</button>
<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>