Files
appRobotWebcam/doc/07_multipleCam_roadmap.md
2026-06-05 05:15:43 +02:00

360 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
---
## Ziel
| Anforderung | Detail |
|---|---|
| Bis zu **10 USB-Kameras** anschliessen | Nur ein Teil streamt live; alle liefern Snapshots |
| **23 Live-Streams** im Viewer | CPU-Budget: ~25 % pro MJPEG-Stream (gemessen) |
| **Alle** Kameras bei „Snapshot alle" | Streaming- und Nur-Snapshot-Kameras |
| **Identifizierbare Bilder** | Jede Kamera hat Namen, Position, Rolle im Dateinamen sichtbar |
| **Download im Browser** | wie bisher |
| **Später: WebService-Weiterleitung** | Konzept ausarbeiten, noch nicht implementieren |
---
## Grundproblem: Zwei Kamera-Klassen
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:
| Klasse | `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 |
**Faustregel:** Kameras, die permanent beobachtet werden müssen → `stream: true`.
Kameras, die nur beim Homing / Trigger relevant sind → `stream: false`.
---
## Kamera-Konfigurationsdatei (`cameras.json`)
Statt hardcodierter Gerätenamen eine maschinenlesbare Liste:
```json
{
"cameras": [
{
"id": "cam_front",
"device": "/dev/video0",
"name": "Vorderseite",
"position": "front",
"stream": true,
"hires": true,
"note": "Logitech C270, Arm-Spitze"
},
{
"id": "cam_left",
"device": "/dev/video2",
"name": "Links",
"position": "left",
"stream": true,
"hires": true,
"note": ""
},
{
"id": "cam_top",
"device": "/dev/video4",
"name": "Draufsicht",
"position": "top",
"stream": false,
"hires": true,
"note": "Nur Snapshot, kein Live-Stream"
}
]
}
```
### Felder
| Feld | Typ | Bedeutung |
|---|---|---|
| `id` | string | stabiler Bezeichner, wird im Dateinamen verwendet |
| `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 |
| `hires` | bool | `false` → nur 640er-Snapshot verfügbar (z.B. bei alter Kamera) |
| `note` | string | Freitext, erscheint nicht im Viewer |
### Persistente Gerätenamen (empfohlen)
`/dev/video0` kann nach Reboot wechseln. Stabiler:
```
/dev/v4l/by-id/usb-Logitech_HD_Webcam_C270_<serial>-video-index0
```
Ausgabe: `ls /dev/v4l/by-id/` auf dem Server.
Symlinks auf `/dev/videoN` — einmal prüfen, dann in `cameras.json` eintragen.
---
## Architektur-Überblick
```
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
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]
```
---
## USB-Hardware: Bandbreite
**10 Kameras brauchen mehrere USB-Controller.** Faustregel:
| Auflösung | fps | MJPEG-Bitrate (ca.) | Kameras pro USB-2.0-Controller |
|---|---|---|---|
| 640×480 | 30 | ~5 MB/s | 8 (theoretisch) |
| 1280×960 | 15 | ~15 MB/s | 3 |
Praktisch: 34 Kameras pro Controller ratsam (Headroom für Bursts).
`lsusb -t` zeigt die Controller-Topologie.
Streaming-Kameras (dauernd aktiv) auf **getrennten Controllern** halten.
Snapshot-only-Kameras teilen sich einen Controller ohne Probleme (selten aktiv).
---
## Phasenplan
### Phase 1 — `cameras.json` + Viewer-Anpassung
**Ziel:** bestehende 2 Kameras aus `cameras.json` lesen statt hardcodern.
Kein funktionaler Unterschied, aber Grundlage für alles Folgende.
**Änderungen:**
**`cameras.json`** (neu, im Projektverzeichnis)
- Startet mit den bestehenden cam0/cam1-Einträgen
**`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)
**`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
**`public/viewer.js`**
- Kamera-Box zeigt `name` + `position` statt rohem `id`
- Nicht-Streaming-Kameras (`stream: false`): keine Live-Box, nur Snapshot-Symbol
(Platzhalter-Box mit Kamera-Name und einem Snapshot-Button)
**Dateinamen:** `${cam.id}_hires_${timestamp}.jpg` → bereits korrekt wenn `id` sprechend ist
**Erfolgskriterium:** Viewer zeigt `name`/`position`, Dateinamen enthalten `id`.
---
### Phase 2 — Snapshot-only-Kameras (FFmpeg one-shot)
**Ziel:** Kameras mit `stream: false` können Snapshots liefern, ohne go2rtc zu berühren.
**Voraussetzung:** `ffmpeg` im Node-Container verfügbar.
Aktuelles Dockerfile (`dockerfile_inline` in `docker-compose.yaml`) installiert kein ffmpeg.
Erweiterung:
```dockerfile
FROM node:lts-bookworm-slim
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
EXPOSE 8444
```
**`docker-compose.yaml`** — Node-Container braucht Gerätezugang:
```yaml
webcam:
devices:
- /dev/video4:/dev/video4 # je Snapshot-only-Kamera
group_add:
- video
```
**`src/snapshotService.js`** — neuer Grab-Pfad für `stream: false`:
```
GET /api/snapshot/:id → ffmpeg one-shot @ 640×480, -frames:v 1
GET /api/snapshot/:id/hires → ffmpeg one-shot @ 1280×960, -vf select=gte(n,15)
(ersten 15 Frames verwerfen = Warmup, vgl. 04_*)
```
FFmpeg-Befehl (bewährt, vgl. `04_Delay_roadmap.md`):
```bash
ffmpeg -f v4l2 -input_format mjpeg -video_size 1280x960 -framerate 15 \
-i /dev/video4 -vf select=gte(n\\,15) -frames:v 1 -q:v 2 -f image2 pipe:1
```
**Mutex:** pro `device` (nicht pro `id`), damit parallele Aufrufe auf demselben Gerät
nicht kollidieren.
**Erfolgskriterium:**
- `GET /api/snapshot/cam_top/hires` liefert 1280er-JPEG
- go2rtc-CPU unverändert (~25 % für Streaming-Kameras)
---
### Phase 3 — „Snapshot alle" inkl. Snapshot-only-Kameras
**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).
**`public/viewer.js``snapshotAllHires()` erweitern:**
```
Promise.allSettled([
...streamingCams.map(c => hiresGrab(c)), // Phase-2-Dance
...snapshotOnlyCams.map(c => oneshotGrab(c)) // direkter Fetch
])
```
**Zeitbudget:**
- Streaming-Kameras: ~810 s (release + warmup)
- Snapshot-only-Kameras: ~23 s (FFmpeg one-shot + warmup)
- Gesamtdauer: ~810 s (parallel, limitiert durch Streaming-Kameras)
**Erfolgskriterium:** Alle Kameras liefern Bilder; Dateinamen enthalten `id`.
---
### Phase 4 (Option) — WebService-Schnittstelle
#### Option A — Pull: Bestehende REST-Endpunkte (fast fertig)
Andere Container (gleicher Host, `network_mode: host`) rufen bereits:
```
GET http://localhost:8444/api/snapshot/cam_front → 640er JPEG
GET http://localhost:8444/api/snapshot/cam_front/hires → 1280er JPEG
```
Das funktioniert **jetzt schon**. Einzige Ergänzung nötig:
```
GET /api/cameras → JSON-Liste aller Kameras mit Metadaten
```
Damit kann ein fremder Container die verfügbaren Kameras abfragen und gezielt
einzelne Snapshots holen.
**Aufwand:** ~1 h (neuer Endpunkt, Metadaten aus `cameras.json`).
---
#### Option B — Push-Trigger: „Mache Screenshot" für andere Container
Ein fremder Container ruft einen Endpunkt auf; AppRobotWebcam grabbt alle Kameras
und legt die Bilder auf ein **gemeinsames Volume**:
```
POST /api/snapshot/trigger
Body (optional): { "cameras": ["cam_front", "cam_top"] } ← leer = alle
Response: { "job": "abc123", "status": "grabbing" }
GET /api/snapshot/job/abc123
Response: { "status": "done", "files": [
{ "id": "cam_front", "name": "Vorderseite", "path": "/snapshots/cam_front_hires_1234.jpg" },
{ "id": "cam_top", "name": "Draufsicht", "path": "/snapshots/cam_top_hires_1234.jpg" }
]}
```
**Nötige Änderungen:**
`docker-compose.yaml` — gemeinsames Volume:
```yaml
volumes:
snapshots: # benanntes Volume
webcam:
volumes:
- snapshots:/snapshots
homing-service: # der aufrufende Container
volumes:
- snapshots:/snapshots:ro
```
`src/snapshotService.js`:
- Bilder nicht nur als HTTP-Response, sondern auch nach `/snapshots/` schreiben
- Job-Queue (In-Memory reicht für Single-Operator-Betrieb): Map von `jobId → status`
- Dateiname: `${cam.id}_hires_${timestamp}.jpg`
**Aufwand:** ~34 h.
**Wann sinnvoll:** wenn der aufrufende Container die Bilder weiterverarbeiten soll
(OCR, ArUco-Erkennung, ML) ohne Browser-Interaktion.
---
#### Option C — Synchroner All-in-One-Endpunkt (einfachste WebService-Form)
```
GET /api/snapshot/all/hires
Response: multipart/form-data mit je einem JPEG pro Kamera
ODER: JSON { "cam_front": "<base64>", "cam_top": "<base64>" }
```
Nachteil: Antwort blockiert ~810 s (Grab-Dauer). Nur sinnvoll wenn der Aufrufer
synchron warten kann und keine grossen Bilder überträgt.
**Aufwand:** ~2 h.
---
## Offene Punkte / Risiken
| Punkt | Risiko | Umgang |
|---|---|---|
| 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_*` |
| 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 |
---
## Empfohlene Reihenfolge
```
Phase 1 (cameras.json) ~2 h Grundlage, kein Risiko
Phase 2 (Snapshot-only, ffmpeg) ~3 h ffmpeg-Abhängigkeit klären
Phase 3 (Snapshot alle erweitert) ~1 h baut auf Phase 1+2
Phase 4A (GET /api/cameras) ~1 h sofort nützlich für andere Container
Phase 4B oder 4C ~34 h nur wenn Push-Trigger gebraucht wird
```
Phase 4B/C **erst wenn** ein konkreter aufrufender Container existiert —
nicht auf Vorrat bauen.