# AppRobotWebcam – Multiple Kameras > Status: **Implementiert** (Phase 1–4A). 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-) 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), 3–4 (praktisch) | | 1280×720 | 30 | ~12 MB/s | 2–3 | | 1280×960 | 15 | ~15 MB/s | 2–3 | | 1920×1080 | 30 | ~25 MB/s | 1–2 | `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:** ~3–4 h. ### Option C — Synchroner Einzel-Endpunkt ``` GET /api/snapshot/all/hires Response: JSON { "cam0": "", "cam2": "" } ``` Blockiert ~8–10 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 |