From 45d9837f4f309a784af5dbf3908e97ac6bba31fd Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Fri, 5 Jun 2026 07:32:05 +0200 Subject: [PATCH] Umbau mit cameraSwitch Dokumentation --- doc/04_Delay_roadmap.md | 12 ++++ doc/05_screenShot_roadmap.md | 12 +++- doc/07_multipleCam_roadmap.md | 130 ++++++++++++++++++++++------------ doc/09_Bug_reports.md | 27 ++++++- 4 files changed, 131 insertions(+), 50 deletions(-) diff --git a/doc/04_Delay_roadmap.md b/doc/04_Delay_roadmap.md index a7ed89a..651a77c 100644 --- a/doc/04_Delay_roadmap.md +++ b/doc/04_Delay_roadmap.md @@ -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 diff --git a/doc/05_screenShot_roadmap.md b/doc/05_screenShot_roadmap.md index 00235f5..89186d7 100644 --- a/doc/05_screenShot_roadmap.md +++ b/doc/05_screenShot_roadmap.md @@ -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 diff --git a/doc/07_multipleCam_roadmap.md b/doc/07_multipleCam_roadmap.md index 449de8e..78a309a 100644 --- a/doc/07_multipleCam_roadmap.md +++ b/doc/07_multipleCam_roadmap.md @@ -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** | „2–3 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-``). --- @@ -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) - │ - └── stream: false → kein go2rtc-Eintrag - │ - └── Hires-Grab (FFmpeg one-shot direkt, Node.js) - Kein Live-View + └── server.js erzeugt je Eintrag EINE CameraSwitch (besitzt /dev/videoN, On-Demand) + │ + ├── stream:true → Viewer zeigt Live-Box → GET /api/stream/:id (MJPEG multipart) + └── stream:false → Viewer zeigt nur Snapshot-Symbol (kein Dauer-) + (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. 2–3 `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,15 +379,16 @@ 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 +Phase 4A (GET /api/cameras) ~1 h sofort nützlich für andere Container ↓ -Phase 4B oder 4C ~3–4 h nur wenn Push-Trigger gebraucht wird +Phase 4B oder 4C ~3–4 h nur wenn Push-Trigger gebraucht wird ``` Phase 4B/C **erst wenn** ein konkreter aufrufender Container existiert — diff --git a/doc/09_Bug_reports.md b/doc/09_Bug_reports.md index 7f0c466..c18c0de 100644 --- a/doc/09_Bug_reports.md +++ b/doc/09_Bug_reports.md @@ -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. @@ -124,4 +130,23 @@ kein Transcode. Fallback `mjpeg` = Re-Encode mjpeg→mjpeg (~50%, wie go2rtc). → als Default `copybsf` verdrahtet, `mjpeg` bleibt als Fallback (`ENCODE_MODE`). Lehre erneut: **erst die Doku-Fakten anwenden, dann bauen** — und Optimierungen **messen** -(Punkt 3), nicht vorhersagen. \ No newline at end of file +(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 `` brach ab (z. B. ein fehlgeschlagener `res.write` + → `cleanup()` meldet den Verbraucher ab), **ohne** dass der Browser von selbst neu + verbindet. Der ``-`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. \ No newline at end of file