@@ -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 ( `c am_front` , `c am_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 | `C ameraSwitch` (wie alle) | `C ameraSwitch` (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: fals e → kein go2rtc-Eintrag
│
└── Hires-Grab (FFmpeg one-shot direkt, Node.j s)
Kein Live-View
├ ── stream:tru e → 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 / grabHire s)
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 → 128 0er JPEG (streaming: Phase-2; non-streaming: one-shot )
└ ── POS T /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 → 64 0er JPEG (getFrame – startet Gerät bei Bedarf )
├ ── GE T /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 e ine `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 l ief ern, ohne go2rtc zu berühren.
> **Hinweis (2026-06-05):** Mit dem Node-Schalter ist d ies er 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 au s `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 Frame s ( `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
↓