Compare commits

..

2 Commits

Author SHA1 Message Date
chk
6d558fd5a6 Draft compression 2026-06-16 21:39:19 +02:00
chk
c6711de922 Draft Focus 2026-06-16 20:59:30 +02:00
2 changed files with 453 additions and 0 deletions

205
doc/Focus.md Normal file
View File

@@ -0,0 +1,205 @@
## Entscheidungsgrundlage Fokus-Steuerung (C920/C922)
**Status:** Nicht umgesetzt. Reine Entscheidungsgrundlage, um abzuwägen ob/wie sich das
lohnt, bevor Aufwand investiert wird.
**Betrifft nur cam2** (laut `cameras.json` aktuell als C920 dokumentiert, laut Nutzer real
eine C922 beide haben denselben UVC-Autofokus-Motor, der Ansatz unten gilt für beide
identisch). **cam0/cam1 (C270) haben keinen Fokusmotor** Fixed-Focus-Linse, keine
UVC-Focus-Controls vorhanden. Jeder UI-/API-Entwurf muss das abbilden (Capability-Check,
nicht Kamera-ID-Whitelist falls mal eine C270 gegen eine weitere C920/C922 ausgetauscht
wird, soll es automatisch funktionieren).
---
## Kontext: Was steuert man da eigentlich?
Fokus ist ein **UVC-Geräte-Control**, kein FFmpeg-Stream-Parameter. Er liegt auf derselben
Ebene wie Belichtung/Weißabgleich gesetzt per `ioctl` auf `/dev/videoN`, unabhängig vom
Capture-Pfad. Zwei relevante Controls (Linux-Namen, via `v4l2-ctl --list-ctrls` abfragbar):
| Control | Bedeutung |
|---|---|
| `focus_automatic_continuous` | Autofokus an (1) / aus (0) |
| `focus_absolute` | manueller Fokuswert, Bereich/Step kameraabhängig (C922 typ. 0255, Step 5) |
`focus_absolute` greift nur, wenn `focus_automatic_continuous=0` ist sonst überschreibt
der Autofokus-Regler den Wert laufend wieder.
**Wichtiger Unterschied zu allem bisher im Projekt:** Bisher hat ausschließlich
`CameraSwitch` das Gerät geöffnet (FFmpeg, exklusiv gedacht laut Architektur-Doku). Eine
Fokus-Steuerung braucht einen **zweiten, kurzlebigen Öffner** desselben Devices, während
FFmpeg ggf. gerade live streamt. Ob das auf dieser Hardware klappt, ohne den Live-Stream zu
stören, ist unklar und **zwingend zuerst auf dem Host zu prüfen** (Memory-Regel: auf Host
messen statt vorhersagen).
---
## Werkzeug-Wahl: `v4l2-ctl` per `spawn`, kein neues npm-Package
Konsistent mit dem Rest des Projekts (FFmpeg wird ebenfalls per `child_process.spawn`
aufgerufen, `package.json` hat bewusst nur `express` als Dependency):
```bash
v4l2-ctl -d /dev/video4 --list-ctrls # Capability-Check + Wertebereich
v4l2-ctl -d /dev/video4 --set-ctrl=focus_automatic_continuous=0
v4l2-ctl -d /dev/video4 --set-ctrl=focus_absolute=30
```
`v4l2-ctl` ist im Image bereits vorhanden (laut Architektur-Doku, dort bisher nur
diagnostisch genutzt). Keine zusätzliche Library, kein zusätzliches Docker-Image-Gewicht.
Alternative wäre eine native Node-V4L2-Bindung (z. B. `v4l2-ctrls`-artige Pakete) unnötig,
da `v4l2-ctl` exakt das tut und schon im Container ist.
---
## Geplanter Aufbau
### Neues Modul `src/focusControl.js`
Trennung **reine Logik** (Jest-testbar, kein Hardware-Zugriff) von **Ausführung**, exakt
das Muster aus `src/configService.js`:
```js
// Reine Logik kein spawn, kein fs:
function parseListCtrls(stdout) { /* → { focus_automatic_continuous: {min,max,default}, focus_absolute: {min,max,step,default} } | {} wenn keine Focus-Controls */ }
function buildSetArgs(device, { auto, value }) { /* → Array von v4l2-ctl-Aufrufen/Args, auto IMMER vor value gesetzt */ }
function clampFocus(value, caps) { /* auf min/max/step runden */ }
// Ausführung spawn, Promise-basiert (analog _captureAt-Stil in cameraSwitch.js):
async function probeFocusCaps(device) { /* spawn v4l2-ctl --list-ctrls, parseListCtrls */ }
async function applyFocus(device, { auto, value }) { /* spawn v4l2-ctl --set-ctrl=..., sequenziell */ }
```
`probeFocusCaps` liefert `{}` (kein Fehler) für Geräte ohne Focus-Control → das ist der
Capability-Check, der C270 automatisch ausblendet, ohne Kamera-Modell-Strings zu vergleichen.
### Wann proben?
Beim Server-Start, einmal pro Kamera, parallel zu den `CameraSwitch`-Instanzen (analog
`loadCalibrations()` in `server.js`). Ergebnis in `camsMeta[i].focusCaps` ablegen. Kein
Re-Probe zur Laufzeit nötig (Hardware-Capabilities ändern sich nicht).
**Offene Frage, auf dem Host zu klären:** Funktioniert `--list-ctrls`/`--set-ctrl`
zuverlässig, während `CameraSwitch` für dieselbe Kamera gerade per FFmpeg streamt (`state
=== 'live'`)? Falls nicht (z. B. `VIDIOC_S_CTRL: Device or resource busy`):
- Fallback A: Fokus-Änderungen kurz den Live-Stream pausieren (`_killCurrentAndWait` +
`_spawnLive`, exakt der bestehende HD-Grab-Mechanismus, nur ohne Auflösungswechsel).
Kostet ein kurzes Stream-Einfrieren (wie bei jedem `reconfigure()`/HD-Grab schon heute).
- Fallback B: Falls selbst das nicht reicht (Gerät wirklich exklusiv), müsste Fokus-Setzen
als neue `CameraSwitch`-Methode laufen, die den Lock nutzt statt eines externen Prozesses.
→ Das ist der **einzige echte Unsicherheitsfaktor** in diesem Plan. Vor Implementierung mit
einem 5-Minuten-Test auf dem Host klären (`v4l2-ctl --set-ctrl=focus_absolute=30` während
der Viewer offen ist und cam2 streamt).
### Persistenz: `cameras.json` erweitern
Analog zu `liveSize`/`stream` (siehe `12_cameraConfig_roadmap.md`) neue optionale Felder,
nur bei Kameras mit Fokus-Support gesetzt:
```json
{
"id": "cam2",
"...": "...",
"focusAuto": false,
"focusAbsolute": 30
}
```
Fehlen die Felder (wie aktuell bei allen drei Kameras) → Kamera bleibt im
Werks-/Treiber-Default (meist `focus_automatic_continuous=1`). Beim Server-Start wird ein
persistierter Wert einmalig angewendet (`applyFocus()` direkt nach `probeFocusCaps()`,
bevor `CameraSwitch.start()` o. On-Demand-Live anläuft) dann ist der Fokus von Anfang an
korrekt, kein Nachjustieren beim ersten Frame nötig.
### API: `src/focusService.js` (neuer Router, Muster wie `configService.js`)
```
GET /api/focus
→ { cameras: [
{ id:"cam0", supported:false },
{ id:"cam1", supported:false },
{ id:"cam2", supported:true, auto:false, value:30, min:0, max:255, step:5 }
] }
POST /api/focus/:id Body: { auto: boolean, value?: number }
→ validiert (id bekannt, supported, value im Bereich) → v4l2-ctl ausführen
(auto zuerst, dann value falls auto:false) → persistiert in cameras.json
(gleiches atomares tmp+rename-Muster wie configService) → 200 mit Ist-Zustand
→ 400 bei unbekannter id / nicht unterstützt / Wert außerhalb Bereich
→ 409 falls Gerät kurzfristig busy (siehe offene Frage oben) kein Server-500,
Client kann anzeigen „bitte erneut versuchen"
```
### UI: Erweiterung von `config.html` (kein neues Formular)
Pro Kamera-Zeile, **nur wenn `supported:true`**, zusätzliche Spalte:
```
┌──────────┬──────────────┬────────────────┬───────────────────────────┐
│ cam2 │ Kamera 2 │ [320×240 ▼] │ Fokus: ☑ Auto [────●────] │
└──────────┴──────────────┴────────────────┴───────────────────────────┘
```
- Checkbox „Auto" (`focus_automatic_continuous`) an = Slider deaktiviert/ausgegraut.
- Slider (`focus_absolute`, `min`/`max`/`step` aus `GET /api/focus`) nur aktiv wenn Auto
aus. Live-Vorschau (Slider-Drag → Debounce ~300 ms → `POST /api/focus/:id`), kein
separater „Anwenden"-Button nötig, da Fokus anders als Auflösung keinen Stream-Restart
auslösen sollte (siehe offene Frage falls Fallback A nötig wird, kurzes Einfrieren pro
Drag-Schritt einplanen, dann doch debounce auf ~800 ms hochsetzen).
- Für cam0/cam1: Spalte entweder ausgeblendet oder Platzhaltertext „kein Autofokus (C270)".
---
## Test-Strategie
**Jest (ohne Hardware), analog `configValidate.test.js`/`configMerge.test.js`:**
- `parseListCtrls`: echte `v4l2-ctl --list-ctrls`-Textbeispiele (mit und ohne
Focus-Controls) → korrektes Capability-Objekt bzw. `{}`.
- `clampFocus`: Werte außerhalb Bereich, Werte nicht auf `step`-Raster.
- `buildSetArgs`: Reihenfolge garantiert auto-vor-value; `auto:true` setzt KEIN
`focus_absolute` mit.
- Validierung im Router: unbekannte id, `supported:false`, Wert außerhalb Bereich.
**Host-Test (zwingend vor jeder UI-Arbeit):**
1. `v4l2-ctl -d /dev/video4 --list-ctrls` → Bestätigen, dass C922 (cam2) Focus-Controls
meldet und C270s (cam0/cam1) keine.
2. **Die offene Frage von oben:** `--set-ctrl` während cam2 live streamt testen.
3. Sichtprüfung: Schärfentest-Chart oder Textseite vor die Kamera halten, `focus_absolute`
über den gesamten Bereich durchfahren, im Viewer beobachten ob sich die Schärfe ändert
(bestätigt, dass die Werte überhaupt etwas bewirken manche UVC-Geräte melden
Controls, die der Treiber dann ignoriert).
---
## Abgrenzung (bewusst NICHT Teil dieses Plans)
- **C270:** keine Fokus-Funktion Hardware kann es nicht, kein Workaround sinnvoll
(digitales Nachschärfen ist kein echter Fokus, nur ein Filter auf bereits unscharfem Bild).
- **Spot-/Touch-Autofokus** (auf einen Bildbereich tippen): UVC kennt das nicht, das ist
proprietäre Logitech-Software-Logik (Logi Capture u. ä.), nicht über v4l2 erreichbar.
- **Automatisches Nachfokussieren bei Auflösungswechsel:** Fokus ist ein reiner
Geräte-/Linsenzustand, unabhängig von `liveSize`/`hiresSize` ändert sich durch
`reconfigure()` oder HD-Grab nicht, daher keine Interaktion mit dem bestehenden
Live/Grab-State-Machine-Code nötig (außer ggf. Fallback A oben).
- **Authentifizierung** auf dem neuen Endpoint: gleiche Linie wie `/api/config` erst bei
geplantem Internet-Zugang.
---
## Aufwandsschätzung (grob, zur Priorisierung)
| Teil | Aufwand |
|---|---|
| Host-Test der offenen Frage (busy-Verhalten) | 15 Min |
| `src/focusControl.js` (Logik + Jest) | klein (~12h) |
| `src/focusService.js` (Router + Jest) | klein (~1h) |
| `server.js`-Integration (Probe beim Start, Apply persistierter Werte) | klein |
| `config.html`/`config.js`-UI-Erweiterung | kleinmittel (~12h) |
| **Gesamt** | **ein halber bis ein Tag**, abhängig vom Ergebnis des Host-Tests |
**Empfehlung:** Lohnt sich kleiner, klar abgegrenzter Scope, nutzt bestehende Muster
1:1 (configService als Vorlage). Einziges Risiko ist der Busy-Test; der ist in 15 Minuten
geklärt und entscheidet nur zwischen Plan A (einfach) und Fallback A (geringfügig
komplexer, aber bereits vorhandener Mechanismus).

248
doc/streamCompression.md Normal file
View File

@@ -0,0 +1,248 @@
# Stream-Komprimierung (MJPEG → H.264) — Abarbeitungsliste
> **Status (2026-06-16):** Der H.264-Pfad ist **im Code vollständig vorhanden und
> unit-getestet**, aber **noch nie auf dem Host scharf geschaltet oder gemessen**.
> Diese Datei ist die ausführbare ToDo-Liste, um ihn in Betrieb zu nehmen — destilliert
> aus [14_ReRender_roadmap.md](14_ReRender_roadmap.md) (Hintergrund/Entwurf),
> [02_HardwareEncoding.md](02_HardwareEncoding.md), [03_Protocoll_roadmap.md](03_Protocoll_roadmap.md)
> und [04_Delay_roadmap.md](04_Delay_roadmap.md).
>
> **Es ist also kein „von Null bauen".** Die offene Arbeit ist: *scharf schalten → messen →
> tunen → ausrollen* — und zwar **auf dem Host gemessen, nicht vorhergesagt**
> (Memory-Regel; alle Zahlen unten ohne Messung sind ausdrücklich **Hypothesen**).
---
## Ausgangslage in einem Satz
Der Live-Stream geht heute als **MJPEG** raus (jedes Frame ein vollständiges JPEG). Das ist
zwar pro Frame komprimiert, aber ohne Inter-Frame-Kompression → hohe Bitrate, und der Client
muss jedes Frame einzeln dekodieren und in ein `<img>` schieben. Bei mehreren Kameras bringt
das schwache Laptops an die Grenze. Mehr Kameras kommen → das Problem wächst.
## Warum H.264 dem Laptop hilft (Motivation + eine Korrektur)
„Unkomprimiert" trifft es nicht ganz — MJPEG **ist** komprimiert, nur eben **intra-frame**.
Der Gewinn von H.264 ist trotzdem real und doppelt:
1. **Inter-Frame-Kompression** (nur Bildänderungen übertragen) → deutlich weniger Bitrate
(Hypothese: ~25 MBit/s statt MJPEG-Bitrate; vor dem Umbau messen, siehe ToDo 0).
2. **Hardware-Decode im Browser** — H.264 dekodiert der Client in der GPU; MJPEG dekodiert
er pro Frame auf der CPU/im Main-Thread und tauscht das `<img>`. Genau **das** ist die
Last, die mehrere Streams auf dem Laptop erzeugen. → H.264 entlastet primär den Client.
> ⚠️ **Realität prüfen, nicht annehmen:** Alle Kameras laufen aktuell auf `liveSize`
> **320×240** ([../cameras.json](../cameras.json)). `1920x1080` dort ist die `hiresSize`
> (Einzelbild beim HD-Knopf), **kein** Dauerstream. Bei 320×240 ist die MJPEG-Bitrate schon
> klein → der Bandbreiten-Gewinn könnte gering sein, der **Client-Decode-Gewinn** aber
> trotzdem zählen. Das entscheidet ToDo 0.
---
## Was bereits im Code steckt (Datei-Pointer)
| Baustein | Datei | Zustand |
|---|---|---|
| Encoder-Wahl (VAAPI/QSV/libx264) + MSE-Codec-String + FFmpeg-Args | [../src/hwencode.js](../src/hwencode.js) | ✅ + Unit-Test [../test/hwencode.test.js](../test/hwencode.test.js) |
| fMP4-Box-Parser (Init-Segment + Fragmente) | [../src/fmp4Parser.js](../src/fmp4Parser.js) | ✅ + Unit-Test [../test/fmp4Parser.test.js](../test/fmp4Parser.test.js) |
| `encode='h264'`-Zweig: Init-Cache, Fan-out, MJPEG-Nebenausgang (fd 3) für Snapshots | [../src/cameraSwitch.js](../src/cameraSwitch.js) | ✅ |
| `video/mp4`-Route (Init-first Fan-out), `encode`/`mseCodec` in `/api/snapshot`+`/api/cameras` | [../src/snapshotService.js](../src/snapshotService.js) | ✅ |
| MSE-`<video>`-Player + Feature-Detection + Snapshot-Fallback + Live-Edge-Tuning | [../public/viewer.js](../public/viewer.js) | ✅ |
| Per-Kamera-Umschaltung „MJPEG / H.264 (GPU)" in der Config-UI | [../public/config.js](../public/config.js), [../src/configService.js](../src/configService.js) | ✅ |
| `/dev/dri`-Passthrough, VA-Treiber-Install beim Start, `LIBVA_DRIVER_NAME=i965`, alle H264-Env | [../docker-compose.yaml](../docker-compose.yaml) | ✅ |
| Verdrahtung (`resolveHwenc`, `H264`-Tuning, `mseCodec`) | [../server.js](../server.js) | ✅ |
**Transport-Entscheidung (steht):** MSE-fMP4, **nicht** WebRTC. WebRTC würde die bewusst
entfernte go2rtc-/Signaling-Maschinerie zurückholen. MSE erhält „Node besitzt die Kameras"
(Node → ffmpeg → Byte-Stream → Browser). Details: [14_ReRender_roadmap.md](14_ReRender_roadmap.md).
**Umschalten ist pro Kamera:** `encode` in [../cameras.json](../cameras.json) oder per UI
(`config.html`). Default bleibt MJPEG → für den LAN-Fall ändert sich nichts.
---
## Eiserne Regeln (gelten weiter — aus [04](04_Delay_roadmap.md)/[09](09_Bug_reports.md))
1. **Der Live-Stream hat absolute Priorität.** Im Zweifel kein Feature statt wackliger Stream.
2. **Auf dem Host messen, nicht vorhersagen.** Jede Bitrate/CPU/Latenz-Zahl ohne Messung ist Hypothese.
3. **Eine USB-Kamera = ein Öffner.** Der `CameraSwitch` bleibt einziger Geräte-Öffner; nie ein zweiter FFmpeg parallel.
4. **Config-Änderung + Rollback statt riskanter Laufzeit-Mutation.** Encode pro Kamera umschalten ist erlaubt, wenn die *andere* Kamera auf bekanntem gutem Stand bleibt und eine Rollback-Zeile existiert.
5. **Smoke-Test mutiert `cameras.json`** ([Memory](../memory/MEMORY.md)) — `POST /api/config` überschreibt die Datei. Nicht gegen die echte Config testen, ohne sie vorher zu sichern.
---
## ToDo-Liste
Legende: ⬜ offen · 🧪 nur auf dem Host verifizierbar (echte Kamera + GPU). Reihenfolge ist
bewusst: erst messen ob es sich lohnt, dann eine Kamera scharf schalten, dann tunen, dann ausrollen.
---
### Phase 0 — Lohnt es sich, und läuft die Basis?
#### ⬜ 0.1 🧪 Ist-Bandbreite & Client-Last des heutigen MJPEG-Streams messen
**Aktion:** Auf dem Host die Bitrate eines Live-Streams bei realer `liveSize` (320×240) messen,
bei 1 und bei n Clients; parallel die CPU-Last des empfangenden Laptops (Browser/OS-Taskmanager)
bei 1, 2, 3 … Streams notieren.
```bash
# Bitrate grob: 10 s Stream ziehen, Bytes messen
curl -s -o /dev/null -w '%{size_download} bytes in %{time_total}s\n' \
--max-time 10 http://<host>:8444/api/stream/cam0
# oder Netz-I/O des Containers:
docker stats --no-stream AppRobotWebcam
```
**Risiken:** Ohne Zahl baut man eventuell viel um für wenig Gewinn (bei 320×240 evtl. marginal).
Der Client-Last-Test ist der eigentliche Entscheider (das ist das Nutzer-Problem), nicht die Bitrate allein.
**Test/Entscheidung:** Tabelle MJPEG vs. (später) H.264 — Bitrate **und** Laptop-CPU pro Stream.
Lohnt sich nur, wenn die Client-CPU mit der Stream-Zahl klar hochläuft. **Gate:** nur weiter, wenn der Gewinn plausibel ist.
#### ⬜ 0.2 🧪 GPU/VAAPI im Container verifizieren (H.264-Encode überhaupt verfügbar?)
**Aktion:** Bestätigen, dass der Container den VA-Treiber laden und H.264 encoden kann.
```bash
docker exec AppRobotWebcam vainfo # erwartet: VAProfileH264* mit VAEntrypointEncSlice
docker exec AppRobotWebcam ls -l /dev/dri/ # renderD128 vorhanden?
docker logs AppRobotWebcam 2>&1 | grep -i -E "VA-Treiber|vainfo|H.264-GPU"
```
**Risiken:**
- VA-Treiber-Install beim Start schlägt **still** fehl (kein Netz/Paketquelle) → App läuft, aber H.264 ist tot (Compose loggt nur `WARN`).
- `i965` vs `iHD`: UHD 630 ist mit `LIBVA_DRIVER_NAME=i965` verdrahtet ([../docker-compose.yaml](../docker-compose.yaml)). Andere/AMD-Box braucht `radeonsi` + `mesa-va-drivers`.
- Auf Synology DSM existiert die `render`-Gruppe nicht (bewusst entfernt); Zugriff läuft über root. Auf der Ziel-Box (Lenovo i5/UHD 630) prüfen, ob das `video`-Group-Mapping + `/dev/dri` für den Node-User reicht.
**Test:** `vainfo` listet H264-Encode-Entrypoint → grünes Licht. Sonst Treiber/Env fixen, **bevor** eine Kamera auf h264 geht.
---
### Phase 1 — Eine Kamera scharf schalten
#### ⬜ 1.1 🧪 Genau eine Kamera auf `encode='h264'` und Bild im Browser prüfen
**Aktion:** **Eine** Kamera umschalten — am sichersten über die Config-UI (`http://<host>:8444/config.html`
→ Spalte Encode → „H.264 (GPU)" → speichern), die anderen auf MJPEG lassen. Alternativ `encode: "h264"`
am Eintrag in [../cameras.json](../cameras.json) + Redeploy. Vorher `cameras.json` sichern (Regel 5).
Erwartete Wirkung im Code: `reconfigure()` killt den Live-FFmpeg und startet ihn als H.264 neu (Hot-Reload);
der Viewer baut für diese Kamera ein `<video>`+MSE statt `<img>`.
**Risiken:**
- **Schwarzes/leeres `<video>`**: MSE-Codec-String passt nicht zu dem, was FFmpeg liefert (Profil/Level). Stellschraube `H264_MSE_CODEC` / `H264_PROFILE` (siehe 2.2). Der Viewer fällt bei nicht unterstütztem Codec automatisch auf den Snapshot-Modus zurück (kein schwarzes Bild, aber auch kein Video).
- **FFmpeg startet nicht** (`encode=h264 ohne hwenc-Konfig` oder VAAPI-Init-Fehler) → Auto-Restart-Schleife. Im Log sichtbar.
- **Andere Kamera nicht anfassen** — Live-Priorität (Regel 1/4). Rollback = Encode der Test-Kamera zurück auf MJPEG.
**Test:** Viewer zeigt flüssiges Live-Bild für die Test-Kamera; Statuszeile „H.264 · live".
Codec im Log prüfen:
```bash
docker logs AppRobotWebcam 2>&1 | grep -i -E "live gestartet|h264_vaapi|encode=h264"
```
#### ⬜ 1.2 🧪 Server-Last messen: encodet wirklich die GPU — und was kostet der MJPEG-Decode?
**Aktion:** `docker stats` für die Test-Kamera im H.264-Modus vs. MJPEG-Modus vergleichen.
**Wichtig (aus [14](14_ReRender_roadmap.md)):** Im h264-Modus muss FFmpeg das USB-**MJPEG erst dekodieren**
(CPU), bevor die GPU H.264 encodet (`format=nv12,hwupload`). Im copybsf-Modus gibt es **gar keinen Decode**.
H.264 kann also die Server-CPU **erhöhen**, obwohl die GPU encodet — das ist zu messen, nicht zu raten.
```bash
docker stats --no-stream AppRobotWebcam
# optional, falls verfügbar, GPU-Auslastung:
docker exec AppRobotWebcam sh -c 'command -v intel_gpu_top && intel_gpu_top -s 1000 || echo "kein intel_gpu_top"'
```
**Risiken:** „GPU-Encode = wenig CPU" ist eine **Hypothese**; der zusätzliche MJPEG-Decode kann sie kippen.
Bei höheren `liveSize` steigt die Decode-Last überproportional.
**Test:** CPU-Delta MJPEG↔H.264 dokumentieren; im FFmpeg-Log `h264_vaapi` bestätigen (nicht `libx264`).
Fällt der Encoder heimlich auf `libx264` zurück → CPU explodiert → Treiber/`HWENC` prüfen.
---
### Phase 2 — Latenz & Qualität tunen
#### ⬜ 2.1 🧪 Latenz H.264/MSE vs. MJPEG messen
**Aktion:** Stoppuhr-Foto-Methode aus [03_Protocoll_roadmap.md](03_Protocoll_roadmap.md): Handy-Stoppuhr (ms)
vor die Kamera, MJPEG- und H.264-Kamera nebeneinander, ein Foto von Monitor + Stoppuhr → Differenz ablesen.
Referenz heute: MJPEG ~139 ms (Kamera→Browser).
**Risiken:** MSE puffert (Init-Segment + Fragment-Dauer + Browser-Jitter-Buffer). Erwartung: H.264 hat **mehr**
Latenz als der MJPEG-Schalter. Der Viewer hält die Latenz klein, indem er an die „Live-Kante" springt
(`H264_MAX_LAG_S` in [../public/viewer.js](../public/viewer.js)) — aggressiver = niedrigere Latenz, mehr Ruckler-Risiko.
**Test:** Gemessene ms in eine Tabelle MJPEG vs. H.264. Entscheiden, ob die Mehrlatenz für die
Roboter-Überwachung vertretbar ist (bei reiner Überwachung meist ja).
#### ⬜ 2.2 🧪 Bitrate / GOP / Profil / Fragmentlänge nachjustieren
**Aktion:** Über Env in [../docker-compose.yaml](../docker-compose.yaml) tunen (Defaults in [../server.js](../server.js)):
| Env | Default | Wirkung |
|---|---|---|
| `H264_BITRATE` | `3M` | Zielbitrate ↓ = weniger Bandbreite, mehr Artefakte |
| `H264_GOP` | ~2×fps | Keyframe-Abstand; kleiner = schnellerer Einstieg/Reconnect, mehr Bitrate |
| `H264_PROFILE` | `main` | `constrained_baseline`/`main`/`high` — muss zum Treiber **und** zum MSE-Codec passen |
| `H264_FRAG_MS` | `200` | fMP4-Fragmentlänge; kleiner = niedrigere Latenz, mehr Overhead |
| `H264_MSE_CODEC` | aus Profil/Level abgeleitet | nur setzen, wenn der Browser den abgeleiteten String ablehnt (z. B. `avc1.640020`) |
**Risiken:** Profil/Level (Server) und MSE-Codec-String (Browser) müssen zusammenpassen, sonst schwarzes
Video / `addSourceBuffer`-Fehler → Snapshot-Fallback. Zu kleine GOP/Fragmente erhöhen Bitrate/CPU wieder.
**Test:** Nach jeder Änderung Bild + Latenz + Bitrate gegenprüfen (ToDo 0.1/2.1 wiederholen). Eine Stellschraube pro Durchgang.
---
### Phase 3 — Snapshot & HD-Grab im H.264-Modus verifizieren
#### ⬜ 3.1 🧪 `/api/snapshot/:id` liefert weiter JPEG, während die Kamera H.264 streamt
**Aktion:** Während die Test-Kamera live H.264 läuft, `GET /api/snapshot/<id>` abrufen.
Hintergrund: Der h264-FFmpeg hat einen **MJPEG-Nebenausgang** (fd 3, gedrosselt auf `H264_JPEG_FPS=2`),
der `latest` für Snapshots füllt ([../src/cameraSwitch.js](../src/cameraSwitch.js), [../src/hwencode.js](../src/hwencode.js)).
**Wichtig fürs Homing-Projekt:** dessen Snapshot-Abruf muss unverändert weiterlaufen.
**Risiken:** Snapshot ist nur ~2 fps frisch (Nebenausgang gedrosselt) — für ein Standbild ok, für „live"-Polling
nicht. Der `split=2`-Filter kostet etwas zusätzlichen CPU (zweiter, billiger MJPEG-Encode).
**Test:** `curl -o snap.jpg http://<host>:8444/api/snapshot/<id>` → valides JPEG in `liveSize`. Homing-Abruf gegenprüfen.
#### ⬜ 3.2 🧪 HD-Grab (`/hires`) im H.264-Modus: Blackout + sauberer Reconnect
**Aktion:** HD-Knopf bzw. `GET /api/snapshot/<id>/hires` auf der H.264-Kamera auslösen.
Ablauf im Code: `grabHires` killt den live-H.264-FFmpeg (`close` = FD frei) → greift HD-JPEG (`hiresEncode`
fällt für h264 automatisch auf `copybsf` → reines Kamera-JPEG, keine H.264-Artefakte) → startet H.264 neu.
Der Neustart erzeugt ein **neues Init-Segment** → die Route beendet bestehende MSE-Verbindungen (`onReinit`),
der Browser verbindet automatisch neu.
**Risiken:** Sichtbarer Blackout + MSE-Reconnect (~23 s) für Zuschauer dieser einen Kamera — wie der
bekannte HD-Blackout, nur mit zusätzlichem Player-Neuaufbau. Reconnect-Logik (`onReinit` + Viewer-Retry)
muss greifen, sonst bleibt das `<video>` stehen.
**Test:** HD-Bild wird heruntergeladen (volle `hiresSize`, scharf, keine H.264-Artefakte); das `<video>`
läuft nach dem Grab von selbst weiter. Andere Kameras unbeeinflusst.
---
### Phase 4 — Mehr Kameras / Rollout
#### ⬜ 4.1 🧪 Mehrere Kameras gleichzeitig auf H.264 — GPU- und USB-Kapazität
**Aktion:** Schrittweise eine zweite, dann dritte Kamera auf H.264 schalten, jeweils CPU/GPU/Bild messen.
**Risiken:** „Schwache OnBoard-Graphik" (UHD 630): Quick Sync schafft i. d. R. mehrere parallele H.264-Encodes,
aber die **CPU-MJPEG-Decodes summieren sich** (jede h264-Kamera dekodiert ihr USB-MJPEG auf der CPU).
USB-Bandbreite ist eine separate Grenze (siehe [07_multipleCam_roadmap.md](07_multipleCam_roadmap.md), `lsusb -t`).
**Test:** Mit jeder zugeschalteten H.264-Kamera CPU/GPU + Bild prüfen. Die Zahl finden, ab der es kippt → dokumentieren.
#### ⬜ 4.2 🧪 Der eigentliche Beweis: Client-Last sinkt
**Aktion:** Auf dem Ziel-Laptop denselben Mehr-Kamera-View einmal in MJPEG, einmal in H.264 öffnen und die
Browser-/OS-CPU vergleichen (das war der Auslöser des ganzen Umbaus).
**Risiken:** Wenn die Client-CPU **nicht** sinkt, lohnt der Server-Mehraufwand (Phase 1.2/4.1) nicht — dann
neu abwägen (z. B. nur Bitrate senken statt H.264). Ältere Browser ohne MSE → Snapshot-Fallback (deutlich schlechter).
**Test:** Laptop-CPU-Tabelle MJPEG vs. H.264 bei n Kameras. Erfolg = klar niedrigere Client-Last bei akzeptabler Latenz/Qualität.
#### ⬜ 4.3 Rollout-Strategie + Rollback festhalten
**Aktion:** Festlegen, welche Kameras dauerhaft H.264 fahren (z. B. die mit Dauer-Zuschauern), welche MJPEG
bleiben (LAN/niedrige Latenz). Default in [../cameras.json](../cameras.json) entsprechend setzen.
**Risiken:** Inkonsistenz zwischen UI-Umschaltung (persistiert in `cameras.json`) und Env-Defaults; Verwechslung
`encode` (Live) ↔ `hiresEncode` (Grab bleibt JPEG).
**Test:** Nach Redeploy `GET /api/cameras` prüfen (`encode`/`mseCodec` pro Kamera korrekt). Rollback = `encode`
zurück auf `copybsf` (UI oder Datei) — der MJPEG-Pfad ist unverändert und immer verfügbar.
---
### Phase 5 — Optional / später
-**AMD-Box gegenprüfen**, falls sie Zielhardware wird: `GPU=amd`, `LIBVA_DRIVER_NAME=radeonsi`, `mesa-va-drivers` in der Compose-`command`-Zeile ergänzen, `vainfo` gegenprüfen.
-**MSE-Watchdog**: eingefrorenes `<video>` ohne Fehler-Event erkennen und neu verbinden (Analogon zum offenen MJPEG-Freeze-Watchdog in [09_Bug_reports.md](09_Bug_reports.md)).
-**Bitrate-Re-Messung nach Tuning** → endgültige Bandbreiten-Zahl für die Doku (ersetzt die Hypothesen oben).
-**`H264_JPEG_FPS` anheben**, falls das Homing-Projekt frischere Snapshots braucht (kostet etwas CPU).
---
## Schnell-Rollback (jederzeit)
1. **Eine Kamera:** Encode in `config.html` zurück auf „MJPEG" (oder `encode` aus dem `cameras.json`-Eintrag entfernen) → Hot-Reload, MJPEG-`<img>`-Pfad sofort zurück.
2. **Komplett:** `cameras.json` aus der Sicherung zurückspielen + Redeploy. Der MJPEG-Schalter ist der unveränderte, bekannte gute Stand.
3. **GPU/Treiber kaputt:** H.264 startet nicht → App läuft trotzdem auf MJPEG weiter (Compose-`command` bricht bei Treiberfehler nicht ab). Kein Live-Ausfall.
## Offene Entscheidungen (vor Phase 1 klären, falls relevant)
1. **Lohnt es sich** bei der realen `liveSize`? → Gate in ToDo 0.1/4.2 (Client-Last ist der Maßstab).
2. **Zielhardware** wirklich die Lenovo-i5/UHD-630-Box (i965)? Falls AMD → Phase 5.
3. **Welche `liveSize`** für H.264-Kameras? Höher als 320×240 wird mit H.264 erst sinnvoll/erschwinglich — gemeinsam mit 2.2 entscheiden.