diff --git a/cameras.json b/cameras.json index 8c7ffbe..f25ceb6 100644 --- a/cameras.json +++ b/cameras.json @@ -1,37 +1,38 @@ { "cameras": [ { - "id": "cam0", - "device": "/dev/video0", - "name": "Kamera 0", + "id": "cam0", + "device": "/dev/video0", + "name": "Kamera 0", "position": "front", - "stream": true, - "hires": true, - "note": "usb-046d_0825_3BB3FE20-video-index0", + "stream": true, + "hires": true, + "note": "usb-046d_0825_3BB3FE20-video-index0", "hiresSize": "1280x960", "liveSize": "320x240" }, { - "id": "cam1", - "device": "/dev/video2", - "name": "Kamera 1", + "id": "cam1", + "device": "/dev/video2", + "name": "Kamera 1", "position": "left", - "stream": true, - "hires": true, - "note": "usb-046d_081b_342D4F40-video-index0", + "stream": true, + "hires": true, + "note": "usb-046d_081b_342D4F40-video-index0", "hiresSize": "1280x960", "liveSize": "320x240" }, { - "id": "cam2", - "device": "/dev/video4", - "name": "Kamera 2", + "id": "cam2", + "device": "/dev/video4", + "name": "Kamera 2", "position": "right", - "stream": true, - "hires": true, - "note": "usb-046d_HD_Pro_Webcam_C920_9C5591DF-video-index0", - "hiresSize": "1920x1080", - "liveSize": "320x240" + "stream": true, + "hires": true, + "note": "usb-046d_HD_Pro_Webcam_C920_9C5591DF-video-index0", + "hiresSize": "1920x1080", + "liveSize": "640x480", + "encode": "h264" } ] } diff --git a/docker-compose.yaml b/docker-compose.yaml index 5c84fbe..096c6ae 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -62,8 +62,12 @@ services: - /dev/v4l/by-id/usb-046d_0825_3BB3FE20-video-index0:/dev/video0 # cam0 – C270 (046d:0825) - /dev/v4l/by-id/usb-046d_081b_342D4F40-video-index0:/dev/video2 # cam1 – C270 (046d:081b) - /dev/v4l/by-id/usb-046d_HD_Pro_Webcam_C920_9C5591DF-video-index0:/dev/video4 # cam2 – C920 + # GPU-Renderknoten für H.264-Encoding (VAAPI Intel/AMD). Nur nötig, wenn eine + # Kamera encode='h264' nutzt. Auf der Intel-Box bestätigt: /dev/dri/renderD128. + - /dev/dri:/dev/dri group_add: - video + - render # Zugriff auf /dev/dri/renderD128 (VAAPI). GID via `getent group render`. environment: - NODE_ENV=production - PORT=8444 @@ -75,8 +79,21 @@ services: # - HIRES_FPS=15 # - ENCODE_MODE=copybsf # copybsf = Bitstream-Copy, niedrige CPU (Default) # # mjpeg = Re-Encode (~50%, Fallback falls copybsf zickt) + # # h264 = GPU-H.264 → MSE (Bandbreite sparen, braucht GPU) # - ON_DEMAND=true # Live nur bei Zuschauern (Default); 'false' = dauerhaft an # - IDLE_GRACE_MS=15000 # Karenz nach letztem Zuschauer vor dem Stop + # + # ── H.264-Hardware-Encoding (nur relevant für encode='h264') ────────────── + # - GPU=intel # intel|amd → VAAPI (gemeinsamer Pfad) · none → libx264 (CPU-Test) + # # → HIER die Maschine wählen: 'intel' (UHD 630) oder 'amd' (680M) + # - HWENC=vaapi # vaapi|qsv|libx264 – Encoder erzwingen (überschreibt GPU) + # - HWENC_DEVICE=/dev/dri/renderD128 # VAAPI/QSV-Renderknoten + # - H264_BITRATE=3M # Zielbitrate + # - H264_GOP= # Keyframe-Abstand (Default ~2×fps); kleiner = schnellerer Einstieg, mehr Bitrate + # - H264_PROFILE=main # constrained_baseline|main|high (muss zum Treiber passen) + # - H264_FRAG_MS=200 # fMP4-Fragmentlänge in ms + # - H264_JPEG_FPS=2 # Bildrate des MJPEG-Nebenausgangs (für /api/snapshot) + # - H264_MSE_CODEC= # MSE-Codec-String überschreiben, falls der Browser meckert (z.B. avc1.640020) # ── Netzwerk ──────────────────────────────────────────────────────────────────── # Externes, bereits existierendes Bridge-Netz (vom Stack "approbot"). Wird hier nur diff --git a/public/config.html b/public/config.html index 19c41bc..d5f337d 100644 --- a/public/config.html +++ b/public/config.html @@ -16,9 +16,9 @@
- + - +
IDNameLive-Auflösungaktuell
IDNameLive-AuflösungKompressionaktuell
lädt…
lädt…
@@ -30,6 +30,8 @@

Auflösungs-Änderung ist sofort aktiv (laufende Streams frieren kurz ein).
„Aus" schaltet in den Snapshot-Modus: kein Video, alle 15 s ein HD-Einzelbild im Viewer.
+ Kompression „H.264" spart Bandbreite (GPU), braucht aber einen MSE-fähigen Browser; + „MJPEG" ist der latenzärmste Standard. Umschalten erst nach Viewer-Reload sichtbar.
⚠ C920 (cam2) braucht bei kleinen 4:3-Auflösungen überdurchschnittlich Bandbreite (siehe doc/12).

diff --git a/public/config.js b/public/config.js index 29ed64d..abec31a 100644 --- a/public/config.js +++ b/public/config.js @@ -7,6 +7,12 @@ const OFF = '__off__'; let liveSizes = []; +// UI bietet nur diese zwei Modi an (mjpeg-Re-Encode bleibt API/Env vorbehalten). +const ENCODE_OPTIONS = [ + { value: 'copybsf', label: 'MJPEG' }, + { value: 'h264', label: 'H.264 (GPU)' }, +]; + async function load() { const r = await fetch('/api/config'); if (!r.ok) throw new Error(`HTTP ${r.status}`); @@ -46,20 +52,35 @@ function render(cameras) { } tdSel.appendChild(sel); + const tdEnc = document.createElement('td'); + const encSel = document.createElement('select'); + encSel.dataset.encId = cam.id; + // copybsf und (unsichtbar) mjpeg gelten beide als „MJPEG" in der UI. + const curEnc = cam.encode === 'h264' ? 'h264' : 'copybsf'; + for (const o of ENCODE_OPTIONS) { + encSel.add(new Option(o.label, o.value, false, curEnc === o.value)); + } + tdEnc.appendChild(encSel); + const tdCur = document.createElement('td'); tdCur.className = 'cur'; - tdCur.textContent = isOff ? 'aus' : (cam.liveSize || '?'); + tdCur.textContent = isOff ? 'aus' : `${cam.liveSize || '?'} · ${curEnc === 'h264' ? 'H.264' : 'MJPEG'}`; - tr.append(tdId, tdName, tdSel, tdCur); + tr.append(tdId, tdName, tdSel, tdEnc, tdCur); tbody.appendChild(tr); } } function collect() { + const encOf = (id) => { + const e = document.querySelector(`select[data-enc-id="${id}"]`); + return e ? e.value : undefined; + }; return Array.from(document.querySelectorAll('select[data-id]')).map((sel) => { const id = sel.dataset.id; - if (sel.value === OFF) return { id, stream: false }; - return { id, stream: true, liveSize: sel.value }; + const encode = encOf(id); + if (sel.value === OFF) return { id, stream: false, ...(encode ? { encode } : {}) }; + return { id, stream: true, liveSize: sel.value, ...(encode ? { encode } : {}) }; }); } diff --git a/public/viewer.js b/public/viewer.js index 95019bd..2a4dd71 100644 --- a/public/viewer.js +++ b/public/viewer.js @@ -38,6 +38,150 @@ function stopStream(cam) { log(cam.id, 'Live aus'); } +// ── H.264-Live über MSE ───────────────────────────────────────────────────── +// Der Server liefert unter /api/stream/ ein fortlaufendes fragmentiertes MP4 +// (Init-Segment zuerst, dann Fragmente). Wir lesen den Body als ReadableStream +// und speisen die Bytes der Reihe nach in einen SourceBuffer →