7.5 KiB
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-<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.
{
"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.
ls -la /dev/v4l/by-id/ # zeigt die stabilen Namen
In docker-compose.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).
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
ls -la /dev/v4l/by-id/→ by-id-Namen identifizierenv4l2-ctl --list-formats-ext -d /dev/videoN | grep -A 20 MJPG→ Auflösung wählen- Eintrag in
cameras.jsonergänzen (id, device, name, position, stream, liveSize …) docker-compose.yaml→ neues Device (by-id → /dev/videoN) eintragen- 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:
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": "<base64>", "cam2": "<base64>" }
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 |