Umbau mit cameraSwitch Dokumentation

This commit is contained in:
chk
2026-06-05 07:32:05 +02:00
parent 39898e3a15
commit 45d9837f4f
4 changed files with 131 additions and 50 deletions

View File

@@ -4,6 +4,18 @@
> 2026-06-05 **nicht mehr über go2rtc**, sondern über einen Node-eigenen MJPEG-Schalter
> (`src/cameraSwitch.js`). Grund: das 106%-Race beim HD-Snapshot. Maßgeblich:
> `05_screenshot_roadmap.md` (Abschnitt „Node-MJPEG-Schalter") und `09_Bug_reports.md`.
>
> ## 🏁 Endergebnis Delay (gemessen 2026-06-05)
> | Variante | Latenz | CPU | Freezes |
> |----------|--------|-----|---------|
> | go2rtc H.264 (WebRTC) | ~130 ms | ~100 % | ja |
> | go2rtc MJPEG | ~200 ms | ~50 % | nein |
> | **Node-MJPEG-Schalter** | **139 ms** | **~5 % idle · ~35 %/Kam aktiv** | nein |
> >
> > Der Schalter unterbietet die go2rtc-MJPEG-Latenz (139 vs. 200 ms) und kommt nahe an
> > H.264 (139 vs. 130 ms) — **ohne** dessen CPU-Last und Freezes. Stellschrauben, die das
> > brachten: **`-fflags nobuffer` + `-flush_packets 1`** (FFmpeg) und **`socket.setNoDelay(true)`
> > + `cork/uncork`** (Node-Stream, ein TCP-Segment pro Frame). On-Demand drückt Idle auf ~5 %.
# AppRobotWebcam Delay / Ruckler-Analyse

View File

@@ -442,8 +442,16 @@ Reconnect innerhalb der Karenz hält Live (kein Thrashing). Abschaltbar: `ON_DEM
(acquire/release/idle-Stop/Reconnect-Karenz/getFrame/always-on) per Mock-Unittest.
- **FFmpeg Default `copybsf`** = `-c:v copy -bsf:v mjpeg2jpeg` (Bitstream-Copy, kein
Transcode; auf dem Host getestet). Fallback `ENCODE_MODE=mjpeg` (~50%, Re-Encode).
- **Auf der Hardware:** CPU **69 % für 2 Kameras bestätigt** (User, copybsf). Latenz nach
den Flags oben + Bug-Reproweg noch gegenzumessen.
- **Auf der Hardware bestätigt (User, 2026-06-05):**
- **Latenz 139 ms** Kamera→Browser (vorher 340 ms) — nach `nobuffer`/`flush_packets`/`setNoDelay`/`cork`.
- **CPU ~5 % idle** (On-Demand, keine Clients), ~35 %/Kamera beim aktiven Streamen (copybsf).
- **HD-Grab beider Kameras parallel:** je echtes 1280×960-JPEG (~133 KB) in ~2,3 s. Live kehrt sauber zurück.
- **Login/Logout + Screenshot+Reconnect:** kein 106%-Race mehr.
- **Bekanntes Restproblem (niedrige Prio):** ein Live-Stream ist einmal eingefroren
(Einzelfall, akzeptiert). Verdacht: gedroppte/abgebrochene multipart-Verbindung, die
nicht von selbst reconnectet. Später prüfen: clientseitiger Watchdog (Frame-Timeout →
`img.src` neu setzen) bzw. ein abgebrochener `onFrame`-Write, der `cleanup()` auslöst,
ohne dass der Browser neu verbindet.
## Hardware-Testplan

View File

@@ -1,7 +1,31 @@
> # ⚙ ARCHITEKTUR-UPDATE (2026-06-05) — der Plan wird durch den Node-MJPEG-Schalter EINFACHER
>
> Diese Roadmap wurde für den **go2rtc**-Aufbau geschrieben. Der ist seit 2026-06-05 ersetzt:
> **Node besitzt alle Kameras selbst** (`src/cameraSwitch.js`, eine `CameraSwitch` pro Gerät),
> go2rtc ist weg. Das ändert mehrere Grundannahmen — überwiegend zugunsten weniger Aufwand:
>
> | Roadmap-Annahme (alt) | Neue Realität | Folge für den Plan |
> |-----------------------|---------------|--------------------|
> | Zwei Kamera-Klassen (`stream:true` via go2rtc, `stream:false` via eigenem FFmpeg) | **Eine** Klasse: jede Kamera ist eine `CameraSwitch` mit On-Demand | **Phase 2 (separater one-shot-Pfad) entfällt** — `getFrame()`/`grabHires()` gelten für alle |
> | go2rtc-Config parallel pflegen (Redeploy je Kamera) | nur `cameras.json` → erzeugt `CameraSwitch`-Instanzen | Phase 1 wird einfacher, **keine Doppelpflege** |
> | „~25 % CPU pro Live-Stream, dauerhaft" | **On-Demand: 0 % idle**, ~35 %/Kamera **nur während aktiv beobachtet** | „23 live" kostet nur was, wenn wirklich jemand zuschaut |
> | HD-Grab = „Phase-2-Dance" (Consumer release → cam_hires) | `grabHires()` (Live stoppen → 1280 → zurück), **fertig** | Phase-2-Dance-Beschreibungen sind überholt |
> | ffmpeg im Node-Container „noch zu ergänzen" | **bereits drin** (+ Geräte durchgereicht) | Phase-2-Voraussetzung schon erfüllt |
> | `stream: false` = „kein Live möglich" | jede Kamera *kann* live (On-Demand); `stream:false` = **nur Viewer zeigt keine Live-Box** | reine Anzeige-Entscheidung, kein anderer Grab-Pfad |
>
> **Netto:** Phasen 1, 3, 4 bleiben sinnvoll (cameras.json/Metadaten, „Snapshot alle", WebService).
> **Phase 2 ist großteils erledigt/obsolet.** USB-Bandbreite (unten) gilt unverändert.
> Details der Architektur: `05_screenShot_roadmap.md` (Abschnitt „Node-MJPEG-Schalter").
> Die folgenden Abschnitte sind als Konzept erhalten; go2rtc-spezifische Stellen sind
> inline mit ⚠ markiert.
---
# AppRobotWebcam Multiple Kameras
> Status: **Konzept**. Aktuelle Implementierung: 2 Streaming-Kameras + HD-Grab via Phase 2
> (`05_screenShot_roadmap.md`). Diese Datei beschreibt den Ausbau auf bis zu 10 Kameras.
> Status: **Konzept** (teils überholt, s. Banner oben). Aktuelle Implementierung: Node-MJPEG-
> Schalter, alle Kameras On-Demand, HD-Grab via `grabHires()` (`05_screenShot_roadmap.md`).
> Diese Datei beschreibt den Ausbau auf bis zu 10 Kameras.
---
@@ -18,22 +42,29 @@
---
## Grundproblem: Zwei Kamera-Klassen
## Grundproblem: ~~Zwei Kamera-Klassen~~ → EINE Klasse (aktualisiert 2026-06-05)
Eine USB-Kamera kann gleichzeitig nur **einmal** geöffnet werden. go2rtc hält
Streaming-Kameras offen. Für Nicht-Streaming-Kameras steht das Gerät dagegen
jederzeit frei. Daraus ergeben sich zwei Klassen mit unterschiedlichem Grab-Pfad:
Eine USB-Kamera kann gleichzeitig nur **einmal** geöffnet werden — das bleibt der
harte Constraint. ⚠ **Die frühere Zwei-Klassen-Aufteilung ist mit dem Node-Schalter
hinfällig:** Jede Kamera ist eine `CameraSwitch`, die das Gerät **on-demand** öffnet.
Es gibt **einen** Grab-Pfad für alle:
| Klasse | `stream: true` | `stream: false` |
| | `stream: true` | `stream: false` |
|---|---|---|
| In go2rtc-Config | ja (`cam_front`, `cam_front_hires`) | nein |
| Live-View im Viewer | ja | nein (nur Snapshot-Symbol) |
| Hires-Grab | Phase-2-Dance (release → cam_hires) | FFmpeg one-shot direkt |
| CPU im Idle | ~25 % (solange Client verbunden) | 0 % |
| Grab-Komplexität | hoch | niedrig |
| Was ist es | `CameraSwitch` (wie alle) | `CameraSwitch` (wie alle) |
| Live-View im Viewer | ja (Live-Box) | nein (nur Snapshot-Symbol)**reine Anzeige-Wahl** |
| Snapshot 640 | `getFrame()` (startet Gerät on-demand) | identisch `getFrame()` |
| Hires 1280 | `grabHires()` | identisch `grabHires()` |
| CPU im Idle | **0 %** (On-Demand, niemand schaut) | **0 %** |
| CPU aktiv | ~35 %/Kamera nur solange beobachtet/gegrabbt | nur während eines Grabs (~2 s) |
**Faustregel:** Kameras, die permanent beobachtet werden müssen → `stream: true`.
Kameras, die nur beim Homing / Trigger relevant sind → `stream: false`.
**`stream` steuert jetzt nur, ob der Viewer eine Live-Box aufmacht** — nicht mehr den
Grab-Mechanismus. Das alte „Phase-2-Dance" und der separate FFmpeg-one-shot-Pfad
entfallen; der Schalter macht das einheitlich.
**Faustregel (unverändert sinnvoll):** dauernd zu beobachtende Kameras → `stream: true`
(Live-Box); nur beim Homing/Trigger relevante → `stream: false` (spart Viewer-Last und
Bandbreite, da kein Dauer-`<img>`).
---
@@ -83,7 +114,7 @@ Statt hardcodierter Gerätenamen eine maschinenlesbare Liste:
| `device` | string | `/dev/videoN` — oder besser persistenter Pfad (s.u.) |
| `name` | string | Anzeigename im Viewer |
| `position` | string | frei; hilfreich für Homing-Auswertung |
| `stream` | bool | `true`Live-Stream in go2rtc; `false` → nur Snapshot |
| `stream` | bool | `true`Viewer zeigt Live-Box; `false` → nur Snapshot-Symbol (Grab-Pfad identisch, On-Demand) |
| `hires` | bool | `false` → nur 640er-Snapshot verfügbar (z.B. bei alter Kamera) |
| `note` | string | Freitext, erscheint nicht im Viewer |
@@ -100,26 +131,23 @@ Symlinks auf `/dev/videoN` — einmal prüfen, dann in `cameras.json` eintragen.
---
## Architektur-Überblick
## Architektur-Überblick (aktualisiert 2026-06-05)
```
cameras.json
── stream: true → go2rtc-Config (cam_front, cam_front_hires, …)
│ │
│ └── Live-View (Browser WS → go2rtc :1984)
│ Hires-Grab (Phase-2-Dance)
── server.js erzeugt je Eintrag EINE CameraSwitch (besitzt /dev/videoN, On-Demand)
── stream: false → kein go2rtc-Eintrag
└── Hires-Grab (FFmpeg one-shot direkt, Node.js)
Kein Live-View
── stream:true Viewer zeigt Live-Box → GET /api/stream/:id (MJPEG multipart)
└── stream:false → Viewer zeigt nur Snapshot-Symbol (kein Dauer-<img>)
(Grab-Pfad für beide identisch — getFrame / grabHires)
Node.js / Express :8444
├── GET /api/cameras → Liste aus cameras.json (mit Metadaten)
├── GET /api/snapshot/:id → 640er JPEG (streaming: via go2rtc; non-streaming: one-shot)
├── GET /api/snapshot/:id/hires → 1280er JPEG (streaming: Phase-2; non-streaming: one-shot)
── POST /api/snapshot/all → alle Kameras grabben, JSON-Antwort mit Metadaten [Phase 4]
Node.js / Express :8444 (go2rtc ENTFERNT)
├── GET /api/cameras → Liste aus cameras.json (mit Metadaten) [Phase 4A]
├── GET /api/stream/:id → MJPEG multipart/x-mixed-replace (Live, On-Demand)
├── GET /api/snapshot/:id → 640er JPEG (getFrame startet Gerät bei Bedarf)
── GET /api/snapshot/:id/hires → 1280er JPEG (grabHires Live kurz pausieren)
└── POST /api/snapshot/all → alle Kameras grabben, JSON mit Metadaten [Phase 4B]
```
---
@@ -155,12 +183,13 @@ Kein funktionaler Unterschied, aber Grundlage für alles Folgende.
**`server.js`**
- `cameras.json` beim Start laden, validieren
- go2rtc-Config-Block in `docker-compose.yaml` weiterhin manuell pflegen
(Redeploy nötig bei Kamera-Änderung — akzeptiert, da Infrastruktur)
- Je Eintrag eine `CameraSwitch` erzeugen (ersetzt das heutige `detectDevices()`-Mapping).
**Keine go2rtc-Config mehr** — die Kamera-Definition lebt nur noch in `cameras.json`.
Geräte müssen weiterhin in `docker-compose.yaml` durchgereicht werden (`devices:`).
**`src/snapshotService.js`**
- `GET /api/snapshot` → liest Kamera-Metadaten aus `cameras.json` statt aus go2rtc
- Gefilterte Liste (kein `_hires`) bleibt; Felder `name`, `position`, `stream` mitliefern
- `GET /api/snapshot` → liest Kamera-Metadaten aus `cameras.json` (Felder `name`,
`position`, `stream` mitliefern). Die Switch-Map kommt aus `server.js`.
**`public/viewer.js`**
- Kamera-Box zeigt `name` + `position` statt rohem `id`
@@ -173,9 +202,15 @@ Kein funktionaler Unterschied, aber Grundlage für alles Folgende.
---
### Phase 2 — Snapshot-only-Kameras (FFmpeg one-shot)
### Phase 2 — Snapshot-only-Kameras (FFmpeg one-shot) — ⚠ GRÖSSTENTEILS OBSOLET
**Ziel:** Kameras mit `stream: false` können Snapshots liefern, ohne go2rtc zu berühren.
> **Hinweis (2026-06-05):** Mit dem Node-Schalter ist dieser separate Pfad **nicht mehr
> nötig.** Jede `CameraSwitch` macht Snapshots on-demand (`getFrame`/`grabHires`) — egal ob
> `stream:true` oder `false`. ffmpeg im Container + Geräte-Durchreichung sind bereits erledigt.
> Der einzige offene Teil aus dieser Phase: bei `stream:false` im Viewer **keine** Live-Box
> rendern. Der untenstehende one-shot-Plan ist nur noch historischer Kontext.
**Ziel (alt):** Kameras mit `stream: false` können Snapshots liefern, ohne go2rtc zu berühren.
**Voraussetzung:** `ffmpeg` im Node-Container verfügbar.
@@ -223,10 +258,10 @@ nicht kollidieren.
**Ziel:** Ein Button-Klick erzeugt Bilder **aller** Kameras gleichzeitig.
**Streaming-Kameras** (`stream: true`): bisheriger paralleler Phase-2-Dance (bereits implementiert).
**Snapshot-only-Kameras** (`stream: false`): direkter FFmpeg one-shot, kein „Release" nötig
→ können parallel zu den Streaming-Grabs laufen (verschiedene Geräte).
**Vereinfacht (2026-06-05):** kein Unterschied mehr zwischen den Klassen. **Alle** Kameras
nutzen `grabHires()` (Live kurz pausieren → 1280 → zurück). Da jede `CameraSwitch` ihr
eigenes Gerät besitzt, laufen die Grabs gefahrlos parallel — genau das macht
`snapshotAllHires()` im Viewer heute schon.
**`public/viewer.js``snapshotAllHires()` erweitern:**
```
@@ -334,8 +369,8 @@ synchron warten kann und keine grossen Bilder überträgt.
| Gerätenamen `/dev/videoN` wechseln nach Reboot | mittel | persistente by-id-Pfade in `cameras.json` |
| USB-Bandbreite bei >4 Kameras gleichzeitig | mittel | separate USB-Controller; `lsusb -t` prüfen |
| ffmpeg im Node-Container (Phase 2) | niedrig | einmalige Dockerfile-Änderung; bewährt in `04_*` |
| go2rtc-Config bei >3 Streaming-Kameras | CPU | max. 23 `stream: true`; Rest `stream: false` |
| Warmup-Schwarzbild bei Snapshot-only (Phase 2) | bekannt | `select=gte(n,15)` bewährt aus `04_*` |
| CPU bei vielen Kameras | niedriger als gedacht | On-Demand: nur **gleichzeitig beobachtete** Streams kosten CPU (~35 %/Kam). Idle = 0 %. Grenze ist die Zahl der parallel offenen Live-Views + USB-Bandbreite, nicht die Gesamtzahl der Kameras |
| Warmup-Schwarzbild bei Hi-Res | bekannt, gelöst | `CameraSwitch.grabHires` verwirft die ersten Frames (`settleFrames`/`minSize`) |
| Parallele Grabs auf gleichem Gerät | beherrschbar | Mutex pro Device (nicht pro ID) |
| Job-Queue Phase 4B bei mehreren Clients | gering | für Single-Operator akzeptiert; sonst persistente Queue |
@@ -344,11 +379,12 @@ synchron warten kann und keine grossen Bilder überträgt.
## Empfohlene Reihenfolge
```
Phase 1 (cameras.json) ~2 h Grundlage, kein Risiko
Phase 1 (cameras.json + Switch-Erzeugung) ~2 h Grundlage, kein Risiko
Phase 2 (Snapshot-only, ffmpeg) ~3 h ffmpeg-Abhängigkeit klären
Phase 2 (Snapshot-only, ffmpeg) ~0 h ⚠ erledigt durch Schalter; nur noch:
Viewer rendert bei stream:false keine Live-Box
Phase 3 (Snapshot alle erweitert) ~1 h baut auf Phase 1+2
Phase 3 (Snapshot alle erweitert) ~1 h Logik existiert (snapshotAllHires), nur Liste erweitern
Phase 4A (GET /api/cameras) ~1 h sofort nützlich für andere Container

View File

@@ -1,5 +1,11 @@
## Multi-User ##
> ✅ **GELÖST (2026-06-05) durch den Node-MJPEG-Schalter.** Genau die „Schalter"-Idee aus
> Option 2 wurde umgesetzt: ein FFmpeg pro Gerät, der Server verteilt an alle Browser
> (Fan-out). Clients halten kein Gerät mehr → HD-Grab pausiert nur kurz die eine Quelle,
> unabhängig von der Client-Zahl. Details: `05_screenShot_roadmap.md`. (Beschreibung unten
> = ursprünglicher Bug-Report.)
Wenn zwei (oder mehr) User streamen, dann kann kein High-Res Bild mehr
gemacht werden. Das Problem: Der Stream bleibt irgendwo aufrecht erhalten.
@@ -125,3 +131,22 @@ kein Transcode. Fallback `mjpeg` = Re-Encode mjpeg→mjpeg (~50%, wie go2rtc).
Lehre erneut: **erst die Doku-Fakten anwenden, dann bauen** — und Optimierungen **messen**
(Punkt 3), nicht vorhersagen.
---
## Stream-Freeze (Einzelfall) — offen, niedrige Priorität
**Beobachtet 2026-06-05:** Ein Live-Stream ist **einmal** eingefroren (Bild stand still,
während die Last normal blieb). Vom User als akzeptabel eingestuft, später anschauen.
**Verdachtsmomente (zu prüfen, wenn es erneut auftritt):**
- Die multipart-Verbindung des `<img>` brach ab (z. B. ein fehlgeschlagener `res.write`
`cleanup()` meldet den Verbraucher ab), **ohne** dass der Browser von selbst neu
verbindet. Der `<img>`-`error`-Handler in `viewer.js` reconnectet nur bei einem echten
`error`-Event — ein „eingefrorenes" Bild ohne Fehler-Event fällt durch.
- Ein einzelnes korruptes JPEG aus dem Kamera-MJPEG (die `APP fields`-Warnung), das der
Browser nicht rendert und danach hängt.
**Möglicher Fix (wenn relevant):** clientseitiger Watchdog — Frames/Zeit zählen
(`img.onload`-Takt); kommt N Sekunden kein neues Bild, `img.src` neu setzen (Reconnect).
Server-seitig ist die Quelle stabil (CPU/Log unauffällig), daher zuerst clientseitig ansetzen.