Files
appRobotWebcam/doc/07_multipleCam_roadmap.md
2026-06-06 09:27:37 +02:00

227 lines
7.5 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: **Implementiert** (Phase 14A). Phase 4B/C offen.
> Aktueller Stand: 3 Kameras (2× C270, 1× C920), gemischte stream-Konfiguration.
---
## Architektur
```
cameras.json
└── server.js → je Eintrag EINE CameraSwitch (besitzt /dev/videoN, On-Demand)
├── stream:true → Viewer zeigt Live-Box → GET /api/stream/:id
└── stream:false → Viewer zeigt Platzhalter (kein Dauer-<img>)
Grab-Pfad identisch: getFrame() / grabHires()
Node.js / Express :8444
├── GET /api/cameras → Metadaten aller Kameras aus cameras.json
├── GET /api/snapshot → Liste mit Metadaten (id, name, position, stream …)
├── GET /api/snapshot/:id → 640er JPEG (on-demand, getFrame)
├── GET /api/snapshot/:id/hires → hires JPEG (grabHires Live kurz pausieren)
├── GET /api/stream/:id → MJPEG multipart/x-mixed-replace (Live)
└── GET /health → Status aller Switches
```
**Eine Kamera = eine `CameraSwitch`.** Es gibt keinen separaten Grab-Pfad für
Live- vs. Snapshot-Kameras. Das Feld `stream` entscheidet nur, ob der Viewer
eine Live-Box aufbaut — der Server behandelt alle Kameras gleich.
**CPU-Verbrauch:** On-Demand — 0 % idle, ~35 %/Kamera nur solange jemand schaut
oder ein Grab läuft. Limit ist USB-Bandbreite + Zahl gleichzeitiger Live-Views,
nicht die Gesamtzahl der Kameras.
---
## `cameras.json`
Einzige Konfigurationsquelle für Geräte, Namen und Auflösungen.
Liegt im Projektverzeichnis, wird beim Start geladen und validiert.
```json
{
"cameras": [
{
"id": "cam0",
"device": "/dev/video0",
"name": "Vorderseite",
"position": "front",
"stream": true,
"hires": true,
"note": "usb-046d_0825_3BB3FE20-video-index0"
},
{
"id": "cam1",
"device": "/dev/video2",
"name": "Links",
"position": "left",
"stream": false,
"hires": true,
"note": "usb-046d_081b_342D4F40-video-index0"
},
{
"id": "cam2",
"device": "/dev/video4",
"name": "Rechts",
"position": "right",
"stream": true,
"hires": true,
"liveSize": "1280x720",
"liveFps": 30,
"hiresSize": "1920x1080",
"hiresFps": 30,
"note": "usb-046d_HD_Pro_Webcam_C920_9C5591DF-video-index0"
}
]
}
```
### Felder
| Feld | Typ | Pflicht | Bedeutung |
|---|---|---|---|
| `id` | string | ✓ | Stabiler Bezeichner; wird in Dateinamen und API-Pfaden verwendet |
| `device` | string | ✓ | `/dev/videoN` — der Pfad **im Container** (siehe by-id-Mapping unten) |
| `name` | string | | Anzeigename im Viewer (Fallback: `id`) |
| `position` | string | | Frei; wird im Viewer-Label angezeigt (`name · position`) |
| `stream` | bool | | `true` → Live-Box im Viewer; `false` → Platzhalter (Default: `true`) |
| `hires` | bool | | `false` → kein hires-Grab verfügbar, z.B. bei schwacher Kamera (Default: `true`) |
| `liveSize` | string | | z.B. `"1280x720"` — überschreibt globales `LIVE_SIZE`-Env |
| `liveFps` | int | | Überschreibt globales `LIVE_FPS`-Env |
| `hiresSize` | string | | Überschreibt globales `HIRES_SIZE`-Env |
| `hiresFps` | int | | Überschreibt globales `HIRES_FPS`-Env |
| `encode` | string | | `"copybsf"` oder `"mjpeg"` — überschreibt globales `ENCODE_MODE`-Env |
| `note` | string | | Freitext; empfohlen: by-id-Name des Geräts (Dokumentation) |
**Globale Env-Defaults** (gelten wenn das Feld in cameras.json fehlt):
`LIVE_SIZE=640x480`, `LIVE_FPS=30`, `HIRES_SIZE=1280x960`, `HIRES_FPS=15`,
`ENCODE_MODE=copybsf`.
---
## Stabile Gerätepfade (`docker-compose.yaml`)
`/dev/videoN` kann nach einem Reboot eine andere Kamera bezeichnen.
Lösung: by-id auf dem **Host** → festes `/dev/videoN` im **Container**.
```bash
ls -la /dev/v4l/by-id/ # zeigt die stabilen Namen
```
In `docker-compose.yaml`:
```yaml
devices:
- /dev/v4l/by-id/usb-046d_0825_3BB3FE20-video-index0:/dev/video0 # cam0
- /dev/v4l/by-id/usb-046d_081b_342D4F40-video-index0:/dev/video2 # cam1
- /dev/v4l/by-id/usb-046d_HD_Pro_Webcam_C920_9C5591DF-video-index0:/dev/video4 # cam2
```
Der by-id-Name gehört zusätzlich ins `note`-Feld von cameras.json (Dokumentation).
`cameras.json` selbst verwendet `/dev/videoN` — den Pfad den der Container sieht.
---
## MJPEG-Auflösung prüfen
Nicht jede Auflösung ist bei jeder Kamera nativ MJPEG. Nicht-native Auflösungen
erzwingen Software-Re-Encode (~50 % CPU extra).
```bash
v4l2-ctl --list-formats-ext -d /dev/video4 | grep -A 20 MJPG
```
Nur Auflösungen unter `'MJPG'` in `liveSize`/`hiresSize` eintragen.
Falls eine Auflösung nur unter `'YUYV'` erscheint → andere Auflösung wählen.
Falls der Stream schwarz bleibt obwohl die Auflösung als MJPG gelistet ist:
`"encode": "mjpeg"` in cameras.json für diese Kamera erzwingt Re-Encode
(kompatibel mit jedem Kamera-MJPEG, aber höhere CPU-Last).
---
## USB-Hardware: Bandbreite
Mehrere aktive Live-Streams brauchen genug USB-Kapazität.
| Auflösung | fps | MJPEG-Bitrate | Streams pro USB-2.0-Controller |
|---|---|---|---|
| 640×480 | 30 | ~5 MB/s | 8 (theoretisch), 34 (praktisch) |
| 1280×720 | 30 | ~12 MB/s | 23 |
| 1280×960 | 15 | ~15 MB/s | 23 |
| 1920×1080 | 30 | ~25 MB/s | 12 |
`lsusb -t` zeigt welche Kameras am selben Controller hängen.
Streaming-Kameras (`stream:true`) möglichst auf **getrennten Controllern**.
Snapshot-only-Kameras (`stream:false`) teilen einen Controller problemlos.
---
## Neue Kamera hinzufügen
1. `ls -la /dev/v4l/by-id/` → by-id-Namen identifizieren
2. `v4l2-ctl --list-formats-ext -d /dev/videoN | grep -A 20 MJPG` → Auflösung wählen
3. Eintrag in `cameras.json` ergänzen (id, device, name, position, stream, liveSize …)
4. `docker-compose.yaml` → neues Device (`by-id → /dev/videoN`) eintragen
5. Stack in Portainer redeployen
---
## Phase 4B/C — WebService-Push (offen)
**Erst umsetzen wenn ein konkreter aufrufender Container existiert.**
### Option B — Push-Trigger mit Job-ID
Ein fremder Container löst einen Grab aus; Bilder landen auf einem gemeinsamen Volume.
```
POST /api/snapshot/trigger
Body (optional): { "cameras": ["cam0", "cam2"] } ← leer = alle
Response: { "job": "abc123", "status": "grabbing" }
GET /api/snapshot/job/abc123
Response: { "status": "done", "files": [
{ "id": "cam0", "path": "/snapshots/cam0_hires_1234.jpg" },
{ "id": "cam2", "path": "/snapshots/cam2_hires_1234.jpg" }
]}
```
`docker-compose.yaml` — gemeinsames Volume:
```yaml
volumes:
snapshots:
webcam:
volumes:
- snapshots:/snapshots
homing-service:
volumes:
- snapshots:/snapshots:ro
```
`src/snapshotService.js`: Bilder nach `/snapshots/` schreiben, In-Memory Job-Queue.
**Aufwand:** ~34 h.
### Option C — Synchroner Einzel-Endpunkt
```
GET /api/snapshot/all/hires
Response: JSON { "cam0": "<base64>", "cam2": "<base64>" }
```
Blockiert ~810 s. Nur sinnvoll wenn der Aufrufer synchron warten kann.
**Aufwand:** ~2 h.
---
## Offene Punkte
| Punkt | Priorität | Massnahme |
|---|---|---|
| Phase 4B/C WebService-Push | niedrig | erst wenn aufrufender Container konkret |
| USB-Bandbreite bei >4 aktiven Streams | mittel | `lsusb -t` prüfen, Kameras auf Controller verteilen |
| Stream-Freeze (selten) | niedrig | bekannt; noch kein reproduzierbarer Fall |