From 92b17b18befcc5393ea2d717dc688db9f3c3e304 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:12:18 +0200 Subject: [PATCH] Claude: Dokumentation --- README.md | 65 ++++++++++ doc/01_WebcamRoadmap.md | 218 ++++++++++++++++--------------- doc/05_screenShot_roadmap.md | 235 +++++++++++++++------------------- doc/07_multipleCam_roadmap.md | 41 +++++- docker-compose.yaml | 56 +++++--- go2rtc.yaml | 23 ---- 6 files changed, 351 insertions(+), 287 deletions(-) delete mode 100644 go2rtc.yaml diff --git a/README.md b/README.md index e69de29..d6e2e47 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,65 @@ +# AppRobotWebcam + +Webcam-Service für den AppRobot. Liefert Live-MJPEG-Streams und HD-Standbilder +über einen einzelnen HTTP-Port — als Docker-Container, ohne externe Streaming-Server. + +## Was es tut + +| | | +|---|---| +| **Live-Stream** | MJPEG multipart im Browser ``, ~139 ms Latenz | +| **HD-Snapshot** | Ein JPEG pro Kamera auf Knopfdruck oder per HTTP GET | +| **Snapshot alle** | Alle Kameras parallel in einem Schritt | +| **REST-API** | Kameraliste, Snapshots, Streams — für andere Container nutzbar | + +## Kameras (aktuell) + +| ID | Modell | Live | HD-Grab | +|---|---|---|---| +| cam0 | Logitech C270 | 640×480 | 1280×960 | +| cam1 | Logitech C270 | 640×480 | 1280×960 | +| cam2 | Logitech C920 | 640×480 | 1920×1080 | + +Konfiguration ausschliesslich über `cameras.json` — kein Redeploy bei Kamera-Änderungen. + +## Zugriff + +``` +http://:8444/ Viewer +http://:8444/api/stream/cam0 Live-MJPEG +http://:8444/api/snapshot/cam0 640er JPEG +http://:8444/api/snapshot/cam0/hires HD-JPEG +http://:8444/api/cameras Kamera-Metadaten (JSON) +http://:8444/health Status +``` + +## Deploy (Portainer) + +1. Portainer → Stacks → Web editor → `docker-compose.yaml` einfügen +2. `APP_PATH` auf den absoluten Pfad des Projektverzeichnisses setzen +3. Deploy — der Container baut sich selbst (Node + FFmpeg) + +```yaml +# Minimal-Konfiguration: +APP_PATH=/home/user/appRobotWebcam +``` + +## Architektur + +``` +cameras.json → server.js → CameraSwitch (/dev/videoN) + ├── Live: ffmpeg → MJPEG → Browser + └── Grab: Live stoppen → hires → zurück +``` + +Ein FFmpeg pro Kamera, nie zwei gleichzeitig. Das `close`-Event ist der harte Beweis +„Gerät frei" — kein Race, kein 106%-CPU-Bug (der mit go2rtc aufgetreten war). + +## Dokumentation + +| Datei | Inhalt | +|---|---| +| `doc/01_WebcamRoadmap.md` | Ziel, Architektur, Entwicklungsgeschichte | +| `doc/05_screenShot_roadmap.md` | HD-Grab, Encode-Qualität, Kamera-Eigenheiten | +| `doc/07_multipleCam_roadmap.md` | cameras.json-Referenz, Multi-Kamera-Setup | +| `doc/09_Bug_reports.md` | Bug-Dokumentation | diff --git a/doc/01_WebcamRoadmap.md b/doc/01_WebcamRoadmap.md index 0da420b..0cf6b0a 100644 --- a/doc/01_WebcamRoadmap.md +++ b/doc/01_WebcamRoadmap.md @@ -2,121 +2,99 @@ ## Ziel -Sauberer, fokussierter Webcam-Service als Docker-Container. Kein Robot-Control, kein -ArUco – nur zwei Verantwortlichkeiten: +Fokussierter Webcam-Service als Docker-Container. Zwei Verantwortlichkeiten: -1. **Live-Video** mit minimaler Latenz (WebRTC via go2rtc) -2. **Standbilder** auf Abruf via HTTP REST (`/api/snapshot/cam{n}`) +1. **Live-Video** mit minimaler Latenz (MJPEG via Browser ``) +2. **HD-Standbilder** auf Abruf via HTTP REST (`/api/snapshot/:id/hires`) -Das Homing-Projekt holt seine Standbilder über den Snapshot-Endpunkt – keine weitere Kopplung. +Das Homing-Projekt und andere Container holen Standbilder über den HTTP-Endpunkt — +keine weitere Kopplung. --- -## Architektur (final) +## Architektur (aktuell) ``` -USB Kameras - │ - ▼ -go2rtc (Capture · H.264-Encode · WebRTC) ── intern, Port 1984 + UDP 8555 - │ - ▼ -Node.js / Express ── öffentlich, Port 8444 - ├── / eigener Viewer (go2rtc -Component) - ├── /api/ws WebRTC-Signaling → proxied zu go2rtc - ├── /api/snapshot/* Standbilder → proxied zu go2rtc /api/frame.jpeg - └── /health +cameras.json → server.js → CameraSwitch (eine Instanz pro /dev/videoN) + │ + ┌──────────────┴──────────────┐ + │ Live (On-Demand) │ HD-Grab + │ ffmpeg MJPEG passthrough │ Live stoppen → hires → zurück + │ → multipart/x-mixed-replace │ close-Event = FD frei (kein Race) + ▼ ▼ + Browser JPEG via HTTP + +Node.js / Express :8444 + ├── GET / Viewer (index.html + viewer.js) + ├── GET /api/cameras Metadaten aller Kameras (aus cameras.json) + ├── GET /api/snapshot Liste der Kameras mit Metadaten + ├── GET /api/snapshot/:id 640er JPEG (aus Live-Puffer, on-demand) + ├── GET /api/snapshot/:id/hires HD-JPEG (grabHires, 2–3 s) + ├── GET /api/stream/:id MJPEG multipart/x-mixed-replace (Live) + └── GET /health Zustand aller CameraSwitch-Instanzen ``` -**Warum go2rtc statt eigenem FFmpeg-Stream:** -Erste Version (eigener Node-FFmpeg-MJPEG-Stream über WebSocket) hatte spürbare Latenz. -go2rtc ist ein spezialisierter Streaming-Server: WebRTC mit ~50–150 ms, automatisches -Encoding, ICE-Negotiation, robuster Client mit Auto-Fallback (WebRTC→MSE→MJPEG). +**Warum kein go2rtc mehr:** go2rtc konnte nicht zuverlässig melden, wann FFmpeg das +Gerät freigibt → Race: zwei Encoder auf `/dev/videoN` → 106 % CPU + Hang. Mit eigenem +FFmpeg-Start ist das `close`-Event des Kindprozesses der harte Beweis „Gerät frei". +→ Details: `doc/09_Bug_reports.md`, Entwicklungsgeschichte: `doc/05_screenShot_roadmap.md`. -**Warum Node davor (statt go2rtc direkt):** -- Ein einziger öffentlicher Port (8444); go2rtc-Admin bleibt unerreichbar -- Stabile Snapshot-Schnittstelle, entkoppelt von go2rtc-Interna -- Eigener, schlanker Viewer - -**Stack-Entscheide:** +**Stack:** | Komponente | Wahl | Begründung | -|------------|------|------------| -| Streaming | go2rtc | WebRTC out-of-the-box, niedrige Latenz, internet-tauglich | +|---|---|---| +| Streaming | Node-eigener FFmpeg → MJPEG | kein Race, geringer Overhead | | Webserver | Node.js + Express | wartbar, user-präferiert | -| Proxy | http-proxy-middleware | reicht HTTP + WebSocket transparent durch | -| Live-Protokoll | WebRTC (Fallback MSE/MJPEG) | niedrigste Latenz, skaliert über Internet | -| Snapshot-API | HTTP GET → JPEG | einfachste Schnittstelle für Consumer | -| Container | docker-compose, `configs` inline | kein Dockerfile-File, Portainer-tauglich | +| Live-Protokoll | MJPEG multipart (``) | native Browser-Unterstützung, ~139 ms | +| HD-Grab | MJPEG copybsf (Kamera-JPEG pur) | kein Re-Encode, keine zweite Kompression | +| Container | docker-compose, inline Dockerfile | Portainer-tauglich, kein externes Image | --- -## Tasks +## Gemessene Werte (Hardware, 2026-06-05/06) -### Phase 1 – Grundgerüst ✅ -- [x] Projektstruktur, package.json, docker-compose mit inline-Config -- [x] Erste Version (Node-FFmpeg-MJPEG über WebSocket) – verworfen wegen Latenz - -### Phase 2 – Umstieg auf go2rtc / WebRTC ✅ -- [x] go2rtc als Streaming-Backend (Kamera-Capture + WebRTC) -- [x] go2rtc-Config in docker-compose eingebettet (`configs.content`) -- [x] Node als Reverse-Proxy (`/api/ws`, `/api/frame.jpeg`, Player-Scripts) -- [x] Eigener Viewer mit go2rtc ``-Component (Auto-Fallback) -- [x] Stabile Snapshot-API `/api/snapshot/cam{n}` -- [x] Auflösung fest 640×480 → Latenz „akzeptabel" (war vorher das Hauptproblem) - -### Phase 3 – Latenz final tunen ✅ -- [x] Messvergleich WebRTC ⟷ MJPEG: **WebRTC ~130 ms, MJPEG ~200 ms** → WebRTC gewinnt -- [x] Entscheid: bei WebRTC bleiben (niedrigere Latenz + besser für Internet) -- [ ] Optional: Prüfen ob Kamera natives H.264 liefert (`v4l2-ctl --list-formats`) → kein Re-Encode -- [ ] Optional: Keyframe-Intervall / Encoder-Preset tunen wenn <100 ms gefordert - -### Phase 4 – Internet-Härtung (offen, vor Produktiv-Schaltung) -- [ ] **TLS via Caddy** – empfohlen, weil Caddy WebSocket-Proxy nativ und zuverlässig kann. - Aktuell verbindet Browser `/api/ws` direkt zu go2rtc Port 1984. - Caddy bündelt beides hinter einer Domain: - ``` - robot.example.com { - handle /api/ws* { reverse_proxy localhost:1984 } - handle /api/stream* { reverse_proxy localhost:1984 } - handle /video-*.js { reverse_proxy localhost:1984 } - handle { reverse_proxy localhost:8444 } - } - ``` - Danach: viewer.js `go2rtcPort` auf 443 (wss://) setzen, Node GO2RTC_PORT=443. -- [ ] **WebRTC-Candidate**: `stun:8555` testen; bei NAT-Problemen → feste IP/Domain: - `candidates: [robot.example.com:8555]` in go2rtc-Config -- [ ] **TURN**: nur wenn STUN + UDP 8555 nicht reicht (sehr restriktive NATs) -- [ ] **Zugriffsschutz**: Basic-Auth am Caddy (1–3 bekannte User) -- [ ] **Firewall**: TCP 443 (Caddy) + UDP 8555 (WebRTC) forwarden; 1984 + 8444 intern - -### Phase 5 – Robustheit (optional) -- [ ] Kamera hot-plug: go2rtc-Verhalten bei Device-Verlust prüfen -- [ ] Resource Limits dokumentieren (`mem_limit`, `cpus`) -- [ ] JSON-Logging -- [ ] Snapshot-Metadaten / optionaler Webhook nach Snapshot +| Kenngrösse | Wert | +|---|---| +| Live-Latenz Kamera→Browser | **139 ms** | +| CPU idle (niemand schaut) | **~5 %** (On-Demand) | +| CPU aktiv | **~35 %/Kamera** (copybsf) | +| HD-Grab Dauer | **~2–3 s** (settleFrames + Format-Switch) | +| HD-Auflösung C270 | 1280×960 JPEG | +| HD-Auflösung C920 | 1920×1080 JPEG | --- -## Abgrenzung zu appRobotVideoControls +## Kamera-Konfiguration (`cameras.json`) -| Feature | appRobotVideoControls | appRobotWebcam | -|---------|-----------------------|----------------| -| Video-Streaming | eigener FFmpeg-MJPEG/WS | go2rtc / WebRTC | -| Snapshots | komplex (dual-pipe) | HTTP REST, einfach | -| Robot-Control (G-Code) | ✅ | ❌ anderes Projekt | -| ArUco / Homing | ✅ | ❌ anderes Projekt | -| Separates Dockerfile | ✅ (OpenCV-Build) | ❌ (inline in compose) | +Einzige Konfigurationsquelle — kein Hardcode im Code, kein Redeploy für neue Kameras. ---- +```json +{ + "cameras": [ + { "id": "cam0", "device": "/dev/video0", "name": "Kamera 0", + "position": "front", "stream": true, "hires": true, + "hiresSize": "1280x960", + "note": "usb-046d_0825_3BB3FE20-video-index0" }, + { "id": "cam1", "device": "/dev/video2", "name": "Kamera 1", + "position": "left", "stream": true, "hires": true, + "hiresSize": "1280x960", + "note": "usb-046d_081b_342D4F40-video-index0" }, + { "id": "cam2", "device": "/dev/video4", "name": "Kamera 2", + "position": "right", "stream": true, "hires": true, + "hiresSize": "1920x1080", + "note": "usb-046d_HD_Pro_Webcam_C920_9C5591DF-video-index0" } + ] +} +``` -## Ports +Per-Kamera-Felder: `liveSize`, `liveFps`, `hiresSize`, `hiresFps`, `encode`, +`hiresEncode` (überschreibt globale Env-Defaults). Details: `doc/07_multipleCam_roadmap.md`. -| Dienst | Port | Exponiert? | -|--------|------|-----------| -| Node Viewer + API + Signaling | TCP 8444 | ja (Firewall) | -| WebRTC Media | UDP 8555 | ja (Firewall) | -| go2rtc HTTP/Debug-UI | TCP 1984 | nein (nur intern/LAN) | +Geräte in `docker-compose.yaml` über stabile `by-id`-Pfade einbinden: +```yaml +devices: + - /dev/v4l/by-id/usb-...-video-index0:/dev/videoN +``` --- @@ -124,17 +102,55 @@ Encoding, ICE-Negotiation, robuster Client mit Auto-Fallback (WebRTC→MSE→MJP ``` appRobotWebcam/ +├── cameras.json Kamera-Konfiguration (Geräte, Namen, Auflösungen) +├── server.js Einstiegspunkt; lädt cameras.json, erzeugt CameraSwitch ├── src/ -│ └── snapshotService.js Snapshot-Router (proxied go2rtc /api/frame.jpeg) +│ ├── cameraSwitch.js CameraSwitch-Klasse (ein FFmpeg pro Gerät, Live + Grab) +│ └── snapshotService.js Express-Router für /api/snapshot, /api/stream, /api/cameras ├── public/ -│ ├── index.html Viewer (lädt go2rtc ) -│ └── viewer.js baut Kamera-Views, Auto-Fallback WebRTC→MSE→MJPEG -├── doc/ -│ ├── 01_WebcamRoadmap.md (diese Datei) -│ ├── 03_Protocoll_roadmap.md WebRTC⟷MJPEG-Vergleich (nachzuholen) -│ └── 05_OptionalToDo_roadmap.md Control-Integration (Optionen) -├── docker-compose.yaml einzige Deploy-Datei (go2rtc-Config eingebettet) -├── go2rtc.yaml nur Referenz/lokal (Config ist in compose eingebettet) +│ ├── index.html Viewer-HTML + CSS +│ └── viewer.js Kamera-Boxen, Live-Start/Stop, HD-Grab, Snapshot-alle +├── tools/ +│ └── hires-probe.js Diagnose-Skript: Hires-Grab direkt auf dem Host testen +├── docker-compose.yaml Einzige Deploy-Datei (inline Dockerfile, by-id devices) ├── package.json -└── server.js Node-Einstiegspunkt +└── doc/ + ├── 01_WebcamRoadmap.md (diese Datei) + ├── 04_Delay_roadmap.md Latenz-Geschichte + Messwerte + ├── 05_screenShot_roadmap.md HD-Grab Architektur + Encode-Qualität + ├── 06_portForwarding_roadmap.md Port-Forwarding / Internet-Zugang + ├── 07_multipleCam_roadmap.md Multi-Kamera-Konfiguration, cameras.json-Referenz + └── 09_Bug_reports.md Bug-Dokumentation (go2rtc-Race, Warmup-Irrweg, …) ``` + +--- + +## Entwicklungsgeschichte (Phasen) + +### Phase 1 — Grundgerüst ✅ +Projektstruktur, Node + Express, erster eigener FFmpeg-MJPEG-Stream über WebSocket. +Verworfen wegen Latenz. + +### Phase 2 — go2rtc / WebRTC ✅ (abgelöst) +go2rtc als Streaming-Backend (Kamera-Capture, H.264/WebRTC). WebRTC ~130 ms, MJPEG +~200 ms. Stabiler Snapshot-Endpunkt `/api/snapshot/cam{n}` via Node-Proxy. + +### Phase 3 — go2rtc-Race-Bug → Node-MJPEG-Schalter ✅ (2026-06-05) +go2rtc konnte den Gerätezustand beim HD-Grab nicht zuverlässig signalisieren → 106 %-CPU- +Race. go2rtc entfernt. Node besitzt die Kameras direkt; `close`-Event = FD frei. +Latenz: **139 ms** (besser als go2rtc). CPU idle: **0 %** (On-Demand). + +### Phase 4 — Multi-Kamera + cameras.json ✅ (2026-06-06) +`cameras.json` als Konfigurationsquelle. Dritte Kamera (C920, 1920×1080) in Betrieb. +`hiresEncode` pro Kamera. Stabile by-id-Gerätepfade. + +--- + +## Offene Punkte + +| Punkt | Priorität | +|---|---| +| Internet-Zugang (TLS via Caddy / Reverse-Proxy) | mittel — `doc/06_portForwarding_roadmap.md` | +| WebService-Push (POST /api/snapshot/trigger + Volume) | niedrig — erst bei konkretem Aufrufer | +| Verlustfreie Standbilder (YUYV→PNG) | niedrig — nur falls JPEG-Qualität nicht reicht | +| Stream-Freeze (selten, Einzelfall) | niedrig — clientseitiger Watchdog noch offen | diff --git a/doc/05_screenShot_roadmap.md b/doc/05_screenShot_roadmap.md index 7a71b45..ed79897 100644 --- a/doc/05_screenShot_roadmap.md +++ b/doc/05_screenShot_roadmap.md @@ -1,7 +1,8 @@ # AppRobotWebcam – Snapshot & HD-Grab -> Status: **Implementiert**. Architektur seit 2026-06-05 (Node-MJPEG-Schalter). -> Gemessene Werte: Latenz 139 ms, ~5 % idle, ~35 %/Kamera aktiv, HD-Grab ~2–3 s. +> Status: **Implementiert & verifiziert** (3 Kameras: 2× C270, 1× C920). +> Architektur seit 2026-06-05 (Node-MJPEG-Schalter), C920-Hires + Qualität 2026-06-06. +> Werte: Live-Latenz 139 ms, ~5 % idle, ~35 %/Kamera aktiv, HD-Grab ~2–3 s. --- @@ -19,7 +20,8 @@ cameras.json ``` Das `close`-Event des Kindprozesses ist der harte Beweis „Gerät frei" — kein Timing, -keine Polls. Das Race (zwei Encoder auf einem Gerät) ist konstruktiv ausgeschlossen. +keine Polls, **kein zweiter Öffner**. Das Race (zwei Encoder auf einem Gerät) ist +konstruktiv ausgeschlossen. --- @@ -29,137 +31,113 @@ keine Polls. Das Race (zwei Encoder auf einem Gerät) ist konstruktiv ausgeschlo ffmpeg -fflags nobuffer -f v4l2 -input_format mjpeg -video_size -framerate -i /dev/videoN - -c:v copy -bsf:v mjpeg2jpeg ← copybsf (Default) + -c:v copy -bsf:v mjpeg2jpeg ← copybsf (Default) -f mpjpeg -flush_packets 1 pipe:1 ``` -| Encode-Modus | CPU | Wann | -|---|---|---| -| `copybsf` (Default) | ~35 %/Kamera | Kamera liefert natives MJPEG | -| `mjpeg` (Fallback) | ~50 %/Kamera | Re-Encode, falls copybsf Probleme macht | - -**`mjpeg2jpeg` ist Pflicht bei copybsf** — Kamera-MJPEG lässt JPEG-Huffman-Tabellen +**`mjpeg2jpeg` ist Pflicht bei copybsf** — Kamera-MJPEG lässt die JPEG-Huffman-Tabellen weg; ohne den Filter verschluckt sich der mpjpeg-Muxer (107 % CPU + Hang). **Latenz-Tuning:** `-fflags nobuffer` + `-flush_packets 1` + `socket.setNoDelay(true)` + `cork/uncork` (Header+JPEG+Trailer = ein TCP-Segment). Gemessen: **139 ms** Kamera→Browser. -**On-Demand:** Live läuft nur wenn Clients verbunden sind. `acquire()`/`release()` zählen -Verbraucher. Nach dem letzten + `IDLE_GRACE_MS` (15 s): Stop → **0 % idle**. Abschaltbar +**On-Demand:** Live läuft nur, wenn Clients verbunden sind. `acquire()`/`release()` zählen +Verbraucher; nach dem letzten + `IDLE_GRACE_MS` (15 s) Stop → **0 % idle**. Abschaltbar via `ON_DEMAND=false`. --- ## HD-Grab (`grabHires`) -### Fall A — liveSize == hiresSize +### Fall A — `liveSize == hiresSize` -Wenn die Live-Auflösung bereits der gewünschten Hires-Auflösung entspricht (z.B. C920 -mit `liveSize: "1920x1080"`): kein Format-Wechsel nötig. `grabHires` ruft direkt -`getFrame()` auf dem laufenden Live-Stream auf. Kein Neustart, kein Übergangs-Problem. +Kein Format-Wechsel nötig: `grabHires` gibt direkt den laufenden Live-Frame zurück +(`getFrame()`). Schnell, kein Geräte-Neustart. -``` -Kamera ──live 1920×1080──► getFrame() → JPEG 1920×1080 -``` - -### Fall B — liveSize ≠ hiresSize (C270: 640×480 → 1280×960) +### Fall B — `liveSize ≠ hiresSize` (der Normalfall) ``` 1. Live-FFmpeg SIGTERM → warte auf close (= FD frei) -2. sleep(800ms) ← Kamera-/Treiberreset, Puffer auslaufen lassen -3. optional: 1280x720-Warmup-Format öffnen -4. sleep(500ms) ← Warmup/Formatwechsel stabilisieren -5. hires-FFmpeg bei hiresSize/hiresFps starten -6. Frames warmlaufen lassen (settleFrames=6) + minWidth-Check (90 % der Soll-Breite) -7. Ersten validen Frame nehmen → hires-FFmpeg SIGTERM -8. finally: Live-FFmpeg neu starten (immer, auch bei Fehler) +2. hires-FFmpeg bei hiresSize/hiresFps starten +3. Frames warmlaufen lassen (settleFrames = 6) + minWidth-Check +4. ersten validen Frame nehmen → hires-FFmpeg beenden +5. finally: Live-FFmpeg neu starten (immer, auch bei Fehler) ``` +**Kein Warmup, kein `sleep` zwischen Stop und Grab.** Auf dem Host verifiziert +(A/B-Test 2026-06-06): ein direkter Open auf die Zielauflösung liefert sofort korrekte +Frames — sowohl 1280×960 (C270) als auch 1920×1080 (C920), in beiden Encode-Modi. +Ein zweites FFmpeg dazwischen (früher als „Warmup-Zwischenformat" eingebaut) erzeugt +nur `Device or resource busy` und ist entfernt. + +**`minWidth`** wird automatisch aus `hiresSize` abgeleitet (`floor(hiresW × 0.9)`). +Beispiel `1920x1080` → `minWidth = 1728`. Damit werden etwaige Übergangs-Frames in +falscher Auflösung (v4l2-Buffer-Reste) abgelehnt. + +**`_captureHires`** setzt zusätzlich `-probesize 5000000 -analyzeduration 1000000`, damit +FFmpeg das MJPEG-Format sicher erkennt. + **Blackout:** Der `` friert ~2–3 s ein und läuft danach weiter. Kein Client-Handling nötig. -**minWidth** wird automatisch aus `hiresSize` abgeleitet (`floor(hiresW × 0.9)`). -Damit werden Frames abgelehnt, die noch auf der alten Live-Auflösung liegen -(v4l2-Buffer-Reste vom vorherigen Format). - -**FFmpeg-Probe:** Für das finale hires-Open werden jetzt zusätzliche Probe-Parameter -gesetzt (`-probesize 5000000`, `-analyzeduration 1000000`), um das MJPEG-Format -und die Kameraparameter sicherer zu erkennen. - --- -## Aktueller Ablauf des HD-Grabs +## Standbild-Qualität: Encode-Wahl -Der aktuelle Ablauf ist bewusst defensiv: -- Live-Stream beenden und auf FFmpeg-`close` warten. -- 800 ms Pause zum Zurücksetzen des Kameratreibers. -- Wenn liveSize deutlich kleiner als hiresSize ist, zunächst ein Zwischenformat - `1280x720` starten und kurz einlaufen lassen. -- 500 ms warten, damit das Gerät in den neuen Auflösungsmodus umschaltet. -- Hires-Stream starten, mehrere Frames puffern und den ersten validen Frame - mit genügend Bytes und Breite auswählen. -- Hires-Stream beenden, Live-Stream neu starten. +Die Kamera liefert bei `input_format mjpeg` bereits **JPEG-komprimiert** (Hardware, +unvermeidbar). Entscheidend ist, ob wir eine **zweite** Kompression daraufsetzen: -### Log- und Fehlerbild +| `hiresEncode` | FFmpeg | Effekt | Empfehlung | +|---|---|---|---| +| `copybsf` (Default) | `-c:v copy -bsf:v mjpeg2jpeg` | Kamera-JPEG **pur durchgereicht**, keine zweite Kompression | **Standardwahl, auch für Standbilder** | +| `mjpeg` | `-c:v mjpeg -q:v 5` | Re-Encode = **zweite** Kompression → sichtbare Artefakte | nur Fallback, falls copybsf zickt | -Aktuelle Log-Meldungen zeigen typische MJPEG/Treiber-Phänomene: -- `unable to decode APP fields`: meist kosmetisch beim MJPEG-Parser, häufig bei - UVC-Webcams. Das bedeutet nicht zwingend einen fehlgeschlagenen Grab. -- `input is truncated` / `Error applying bitstream filters`: kann auftreten, wenn - FFmpeg beim Beenden gerade ein MJPEG-Paket verarbeitet. Das ist ein Hinweis auf - einen abrupten Stream-Abbruch, nicht zwingend auf ein ungültiges Bild. -- `Could not find codec parameters ... unspecified pixel format`: deutet darauf hin, - dass der neue HD-Stream noch nicht sauber genug erkannt wurde. Deshalb nutzen - wir jetzt größere Probe-Parameter. +- `hiresEncode` überschreibt `encode` nur für den Grab; fehlt es, gilt `encode`. +- cam2 lief kurzzeitig auf `mjpeg` (Workaround) → sichtbare Artefakte, **131 kB**. Mit + `copybsf` kommt das ungekürzte Kamera-JPEG (~300–450 kB) → keine Re-Encode-Artefakte. -### Mögliche Ursachen - -- Treiberzustand der Kamera nach einem schnellen Formatwechsel: der C920 kann - noch Frames aus dem alten Modus liefern oder zwischen `640×480` und `1920×1080` - in einen inkonsistenten Zustand kommen. -- MJPEG-Frames ohne vollständige JPEG-Header oder Huffman-Tabellen, die erst durch - `mjpeg2jpeg` ergänzt werden. -- Abrupte Beendigung des `ffmpeg`-Prozesses während eines laufenden MJPEG-Pakets. +**Wirklich verlustfrei** (kein JPEG-Verlust überhaupt) ginge nur über das rohe +**YUYV**-Format → PNG. Eigener Grab-Pfad, große Dateien (~3–6 MB), langsamer, USB-intensiv. +**Noch nicht implementiert** — nur bauen, wenn die JPEG-Qualität nicht reicht. --- ## Kamera-spezifische Konfiguration -### C920 (HD Pro Webcam) — liveSize = hiresSize - -Die C920 hat ein v4l2-Treiber-Eigenheit: nach einem Live-Stream bei 640×480 kann sie -beim Neustart auf 1920×1080 nicht sauber wechseln — der Treiber gibt 1280×720-Frames -zurück (Übergangs-Artefakt, trotz korrekter Format-Anforderung). - -**Lösung:** `liveSize` und `hiresSize` identisch setzen. `grabHires` wechselt dann kein -Format — es liest direkt aus dem laufenden 1920×1080-Stream (Fall A). +### C270 (`cam0`, `cam1`) ```json -{ - "id": "cam2", - "liveSize": "1920x1080", - "liveFps": 15, - "hiresSize": "1920x1080" -} +{ "hiresSize": "1280x960" } ``` +Live 640×480 (Default), Grab 1280×960, `copybsf`. Bewährter Standardfall. -### C270 — Standard (Fall B) +### C920 (`cam2`) ```json -{ - "id": "cam0", - "hiresSize": "1280x960" -} +{ "hiresSize": "1920x1080" } ``` - -Live bei 640×480, Grab bei 1280×960. Format-Wechsel + 300ms-Pause funktioniert. +Live 640×480 (Default, USB-schonend), Grab 1920×1080, `copybsf`. Der Wechsel 640→1920 +funktioniert direkt — **kein** `liveSize`-Override und **kein** Warmup nötig. ### Auflösung prüfen -Nur MJPG-native Auflösungen in `liveSize`/`hiresSize` verwenden (kein Software-Encode): - ```bash v4l2-ctl --list-formats-ext -d /dev/videoN | grep -A 20 MJPG ``` +Nur unter `'MJPG'` gelistete Auflösungen verwenden (sonst Software-Encode = teuer). + +--- + +## Harmlose Log-Meldungen + +Diese erscheinen im Normalbetrieb und bedeuten **keinen** Fehler: + +| Meldung | Bedeutung | +|---|---| +| `unable to decode APP fields: Invalid data found` | einmalige FFmpeg-Probe-Warnung beim Stream-Start (C270) | +| `Dequeued v4l2 buffer contains corrupted data` | erster Frame nach Geräte-Open ist oft unvollständig — wird verworfen | +| `input is truncated` / `Error applying bitstream filters` | copybsf droppt den ersten korrupten Frame; `settleFrames` nimmt einen späteren | + +Entscheidend ist die `HD OK`-Zeile mit `Breite=…px` passend zur `Soll`-Breite. --- @@ -179,10 +157,8 @@ v4l2-ctl --list-formats-ext -d /dev/videoN | grep -A 20 MJPG | Problem | Priorität | Zustand | |---|---|---| -| Stream friert selten dauerhaft ein | niedrig | Einzelfall; clientseitiger Watchdog (Frame-Timeout → `img.src` neu) noch nicht implementiert | -| `unable to decode APP fields` im Log (C270) | kosmetisch | Einmalige Probe-Warnung von FFmpeg, kein Auswirkung | -| `input is truncated` / `Error applying bitstream filters` beim HD-Grab | mittel | Hinweis auf abrupten Stream-Abbruch beim Formatwechsel; Bild kann dennoch gültig sein | -| `Could not find codec parameters ... unspecified pixel format` | mittel | Zeichen für unvollständige FFmpeg-Probe nach Formatwechsel; größere probe-Parameter helfen | +| Stream friert selten dauerhaft ein | niedrig | Einzelfall; clientseitiger Watchdog (Frame-Timeout → `img.src` neu) noch nicht gebaut | +| Verlustfreie Standbilder (YUYV→PNG) | niedrig | offen; siehe „Standbild-Qualität" | --- @@ -190,58 +166,47 @@ v4l2-ctl --list-formats-ext -d /dev/videoN | grep -A 20 MJPG ## Architektur-Entwicklung (Historie) -Dieser Abschnitt dokumentiert den Weg zur aktuellen Lösung. Für die maßgebliche Implementierung gelten ausschliesslich die Abschnitte oben. +Dieser Teil dokumentiert den Weg dorthin. ### go2rtc-Ansatz (2026-06-04) — abgelöst -#### Grundidee: Consumer-Umhängen +go2rtc verwaltete die Kameras. Für HD-Grabs wurde der Live-Consumer losgelassen, damit +go2rtc das Gerät freigibt, dann ein separater `*_hires`-Stream geöffnet. Phase 1 (Freigabe +messen) und Phase 2 (Grab) funktionierten grundsätzlich, aber: -go2rtc verwaltete die Kameras. Für HD-Grabs wurde der Live-Consumer (`cam0`) losgelassen, -damit go2rtc das Gerät freigibt, dann ein separater `cam0_hires`-Stream bei 1280×960 geöffnet. +**Race-Bug (2026-06-05):** go2rtcs API konnte nicht zuverlässig melden, wann FFmpeg +`/dev/videoN` freigibt → zwei FFmpeg gleichzeitig auf demselben Gerät → **106 % CPU + Hang** +(Details: `09_Bug_reports.md`). Strukturell nicht lösbar, weil go2rtc keine harte Garantie +über den Gerätezustand gibt. -#### Phase 1 — Freigabe messen (2026-06-04, ✅ erfolgreich) - -Endpunkt `GET /api/snapshot/:id/release-test` pollt go2rtcs `/api/streams` alle 200 ms -und misst, wann nach Consumer-Verlust der Producer stoppt (= Gerät frei). - -**Ergebnis:** -```json -{ "freed": true, "msUntilFree": 0, "zeroConsumerAt": 4850 } -``` - -go2rtc gibt das Gerät sofort frei wenn der letzte Consumer weg ist. Kein „warm halten". -Die ~5 s (`zeroConsumerAt`) sind die WS-Abmeldelatenz von go2rtc selbst. - -#### Phase 2 — HD-Grab implementiert (2026-06-04, ✅ funktionierte) - -HD-Grab lieferte echte 1280×960-Frames (~76 KB). Einschränkungen: -- Nur bei einem einzigen Viewer-Tab zuverlässig (bei mehreren fiel Consumer-Count nie auf 0) -- Sequenz: Browser löst cam0 → 5 s warten → Grab → Browser hängt zurück -- Blackout ~8–10 s (5 s Pause + Grab-Zeit) - -#### Race-Bug entdeckt (2026-06-05) → go2rtc entfernt - -go2rtcs API konnte nicht zuverlässig melden, wann FFmpeg `/dev/videoN` freigibt. -Folge: zwei FFmpeg gleichzeitig auf demselben Gerät → **106 % CPU + Hang** (reproduzierbar). -Details: `09_Bug_reports.md`. - -**Erkenntnis:** Das Problem ist strukturell — go2rtc gibt keine harten Garantien über -den Gerätezustand. Der einzige zuverlässige Beweis „Gerät frei" ist das `close`-Event -des FFmpeg-Prozesses selbst. - -→ **Entscheidung 2026-06-05:** go2rtc entfernen. Node startet FFmpeg direkt. +→ **Entscheidung:** go2rtc entfernen. Node startet FFmpeg selbst; dessen `close`-Event ist +der harte Beweis „Gerät frei". Die alte `go2rtc.yaml` wurde 2026-06-06 gelöscht (liegt in +der Git-Historie). ### Node-MJPEG-Schalter (2026-06-05) — aktuelle Architektur -Mit `src/cameraSwitch.js` besitzt Node die Kameras direkt. Das `close`-Event des eigenen -FFmpeg-Kindprozesses ist der harte Beweis „FD geschlossen = Gerät frei". Keine go2rtc- -Abhängigkeit, kein Race. +Sofort gemessen: Latenz 139 ms (vorher ~340 ms), Idle-CPU ~5 % (On-Demand), ~35 %/Kamera +aktiv, HD-Grab beider C270 parallel ~2,3 s. -**Sofort gemessene Werte:** -- Latenz: 139 ms (vorher ~340 ms mit go2rtc) -- CPU idle: ~5 % (On-Demand) -- CPU aktiv: ~35 %/Kamera (copybsf) -- HD-Grab beide Kameras parallel: ~2,3 s, je 1280×960 +### C920-Hires & der Warmup-Irrweg (2026-06-06) -Alle weiteren Details: aktuelle Architektur-Abschnitte oben. +Beim Einbau der dritten Kamera (C920, 1920×1080) schien der Grab „nur 720" zu liefern. +Mehrere falsche Theorien (Kamera kann kein 1080-MJPEG; Treiber stuft 1080→720 zurück) +wurden **durch Host-Tests widerlegt**: `v4l2-ctl` + direkte `ffmpeg`-Grabs zeigten +1920×1080-MJPEG auf jedem Frame, in beiden Encode-Modi. + +Die zwei realen Ursachen: +1. **Veralteter Code im Container** — eine fehlgeschlagene Git-Übertragung; der Container + lief alten Code (verifiziert via `docker exec … grep`). Jede „Fix→Redeploy"-Runde + testete Code, der gar nicht lief. +2. **Warmup-Zwischenformat** — ein nachträglich eingebauter Schritt öffnete ein zweites + FFmpeg (1280×720) vor dem Grab → `Device or resource busy` → alle Grabs brachen ab. + +**Lehre (festgehalten):** Erst auf dem **Host** mit `ffmpeg`/`ffprobe`/`v4l2-ctl` +verifizieren, was die Kamera real liefert, und sicherstellen, dass der Container den +aktuellen Code fährt — **bevor** Code-Theorien gebaut werden. Diagnose-Tool dafür: +`tools/hires-probe.js`. + +Nach Entfernen des Warmups und mit aktuellem Code: alle drei Kameras liefern korrekte +HD-Grabs (cam2 = 1920×1080). Encode auf `copybsf` für artefaktfreie Standbilder. diff --git a/doc/07_multipleCam_roadmap.md b/doc/07_multipleCam_roadmap.md index e41cfd4..af53f82 100644 --- a/doc/07_multipleCam_roadmap.md +++ b/doc/07_multipleCam_roadmap.md @@ -68,16 +68,19 @@ Liegt im Projektverzeichnis, wird beim Start geladen und validiert. "position": "right", "stream": true, "hires": true, - "liveSize": "1280x720", - "liveFps": 30, "hiresSize": "1920x1080", - "hiresFps": 30, "note": "usb-046d_HD_Pro_Webcam_C920_9C5591DF-video-index0" } ] } ``` +> **cam2 (C920):** Live läuft auf dem globalen Default 640×480 (USB-schonend, flüssig), +> nur der HD-Grab geht auf 1920×1080. `encode`/`hiresEncode` sind **nicht** gesetzt → +> beide nutzen `copybsf` (Kamera-JPEG ohne Re-Encode = beste Standbild-Qualität, s.u.). +> Der Wechsel 640→1920 beim Grab funktioniert direkt; **kein** Warmup/Zwischenformat nötig +> (auf dem Host verifiziert, 2026-06-06). Details: `05_screenShot_roadmap.md`. + ### Felder | Feld | Typ | Pflicht | Bedeutung | @@ -92,7 +95,8 @@ Liegt im Projektverzeichnis, wird beim Start geladen und validiert. | `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 | +| `encode` | string | | `"copybsf"` oder `"mjpeg"` — überschreibt `ENCODE_MODE`-Env (gilt für Live; und für Grab, falls `hiresEncode` fehlt) | +| `hiresEncode` | string | | Encode **nur** für den HD-Grab; überschreibt `encode`. Default = `encode`. Für Standbilder `copybsf` bevorzugen (s.u.) | | `note` | string | | Freitext; empfohlen: by-id-Name des Geräts (Dokumentation) | **Globale Env-Defaults** (gelten wenn das Feld in cameras.json fehlt): @@ -136,9 +140,31 @@ 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). +> **Lehrgeld (2026-06-06):** Beim Einbau der C920 schien 1920×1080 „nur 720 zu liefern". +> Ursache war **nicht** die Kamera (1920×1080 MJPEG ist nativ, auf dem Host verifiziert), +> sondern (a) veralteter Code im Container und (b) ein nachträglich eingebauter +> „Warmup-Zwischenformat"-Schritt, der einen zweiten FFmpeg auf dem Gerät startete +> (`Device or resource busy`). Beides entfernt. Lehre: erst auf dem **Host** mit +> `ffmpeg`/`ffprobe` verifizieren, was die Kamera real liefert, bevor man Code-Theorien baut. + +--- + +## Standbild-Qualität: Encode-Wahl + +Eine USB-Kamera liefert das Bild bei `input_format mjpeg` bereits **JPEG-komprimiert** +(Hardware, unvermeidbar). Wie der HD-Grab das ausgibt, entscheidet `hiresEncode`/`encode`: + +| Modus | Was passiert | Qualität / Größe | Wann | +|---|---|---|---| +| `copybsf` (Default) | Kamera-JPEG **durchreichen** (`-c:v copy -bsf:v mjpeg2jpeg`), keine zweite Kompression | beste JPEG-Qualität, klein, niedrigste CPU | **Standardwahl, auch für Standbilder** | +| `mjpeg` | Re-Encode (`-c:v mjpeg -q:v 5`) — **zweite** Kompression | sichtbare Artefakte bei Standbildern, ~50 % CPU | nur als Fallback, wenn copybsf bei einer Kamera zickt | + +→ Für scharfe Standbilder **`copybsf`** verwenden (kein `hiresEncode` setzen). `mjpeg` +war ein Workaround und erzeugte bei cam2 sichtbare Artefakte (131 kB vs. ~350 kB copybsf). + +**Wirklich verlustfrei** ginge nur über das rohe **YUYV**-Format → PNG (kein JPEG-Verlust +überhaupt). Das ist ein eigener Grab-Pfad (große Dateien ~3–6 MB, langsamer) und noch +**nicht implementiert** — bei Bedarf siehe „Offene Punkte". --- @@ -222,5 +248,6 @@ Blockiert ~8–10 s. Nur sinnvoll wenn der Aufrufer synchron warten kann. | Punkt | Priorität | Massnahme | |---|---|---| | Phase 4B/C WebService-Push | niedrig | erst wenn aufrufender Container konkret | +| Verlustfreie Standbilder (YUYV→PNG) | niedrig | eigener Grab-Pfad: rohes YUYV lesen → PNG; nur wenn JPEG-Qualität nicht reicht | | USB-Bandbreite bei >4 aktiven Streams | mittel | `lsusb -t` prüfen, Kameras auf Controller verteilen | | Stream-Freeze (selten) | niedrig | bekannt; noch kein reproduzierbarer Fall | diff --git a/docker-compose.yaml b/docker-compose.yaml index 43b5753..0d3eb4b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,31 +1,31 @@ name: approbotwebcam # ════════════════════════════════════════════════════════════════════════════ -# NODE-MJPEG-SCHALTER – ein Node-Container besitzt die Kameras selbst +# AppRobotWebcam – Node-MJPEG-Schalter # ════════════════════════════════════════════════════════════════════════════ # -# Node startet pro Kamera EINEN FFmpeg (640 MJPEG passthrough) und verteilt den -# Stream als multipart/x-mixed-replace an die Browser (). Für HD-Snapshots -# stoppt der Schalter den Live-FFmpeg sauber (Prozess-close = Gerät frei), -# greift 1280×960, schaltet zurück. go2rtc wird NICHT mehr gebraucht. +# Node besitzt jede Kamera direkt (eine CameraSwitch-Instanz pro /dev/videoN). +# Live: FFmpeg → MJPEG multipart → Browser . Latenz: ~139 ms. +# HD-Grab: Live-FFmpeg stoppen (close-Event = FD frei) → hires-FFmpeg → +# JPEG an Client → Live zurück. Auflösungen in cameras.json konfiguriert. # -# Warum so: go2rtcs API konnte nicht zuverlässig melden, wann FFmpeg das Gerät -# freigibt → Race (zwei Encoder auf /dev/videoN = ~108% CPU). Wenn Node FFmpeg -# selbst startet, ist dessen 'close'-Event der harte Beweis „Gerät frei". -# Siehe doc/09_Bug_reports.md. +# Kameras (aktuell, by-id = stabil über Reboots): +# cam0 C270 /dev/video0 Live 640×480, Hires 1280×960 +# cam1 C270 /dev/video2 Live 640×480, Hires 1280×960 +# cam2 C920 /dev/video4 Live 640×480, Hires 1920×1080 # # Portainer: Stack → Web editor → dieses YAML → Deploy. -# APP_PATH = /absoluter/pfad/zum/appRobotWebcam (Code muss dort liegen) +# APP_PATH = /absoluter/pfad/zum/appRobotWebcam # -# Firewall (Internet): TCP 8444 (Viewer + Stream + API). Sonst nichts mehr. +# Firewall: TCP 8444 (Viewer + Stream + API) # # Zugriff: # Viewer: http://:8444/ # Live-Stream: http://:8444/api/stream/cam0 -# Snapshot (Homing): http://:8444/api/snapshot/cam0 (+ /hires) -# -# ROLLBACK auf den alten go2rtc-Aufbau: git checkout -- docker-compose.yaml -# server.js public/ src/ (der go2rtc-Stand liegt in der Git-Historie). +# Snapshot: http://:8444/api/snapshot/cam0 +# HD-Snapshot: http://:8444/api/snapshot/cam0/hires +# Kamera-Liste: http://:8444/api/cameras +# Status: http://:8444/health # ════════════════════════════════════════════════════════════════════════════ services: @@ -69,10 +69,24 @@ services: # - IDLE_GRACE_MS=15000 # Karenz nach letztem Zuschauer vor dem Stop # ── Hinweise ──────────────────────────────────────────────────────────────────── -# • Bleibt eine Kamera schwarz? Geräte prüfen: v4l2-ctl --list-devices -# und ob 640x480 bzw. 1280x960 als MJPEG nativ angeboten werden: -# v4l2-ctl --list-formats-ext -d /dev/video0 -# Nur MJPEG-native Auflösungen bleiben CPU-arm (YUYV → Software-Encode = teuer). -# • Meckert Portainer über sehr alte Compose-Syntax (dockerfile_inline)? Dann -# Compose/Docker-Engine aktualisieren – dieser Aufbau braucht Compose v2. +# • Neue oder geänderte Kamera: cameras.json anpassen + Redeploy (kein Code-Änderung). +# by-id-Namen ermitteln: ls -la /dev/v4l/by-id/ +# Neues Device hier eintragen (by-id → /dev/videoN), dann cameras.json-Eintrag. +# +# • Bleibt eine Kamera schwarz oder liefert falsche Auflösung? +# Direkt auf dem Host testen (ohne Docker, beweist was die Kamera real kann): +# node tools/hires-probe.js /dev/video4 1920x1080 copybsf +# Alternativ manuell: +# v4l2-ctl --list-formats-ext -d /dev/video4 # MJPG-Auflösungen anzeigen +# Nur MJPEG-native Auflösungen in cameras.json verwenden (YUYV = Software-Encode = ~50% CPU). +# +# • HD-Grab liefert schlechte Qualität? +# Standard ist copybsf (Kamera-JPEG pur, keine zweite Kompression). +# "hiresEncode": "mjpeg" in cameras.json nur als Fallback, erzeugt Re-Encode-Artefakte. +# +# • Code-Stand im Container prüfen: +# docker exec AppRobotWebcam grep -n "Grab (minWidth" src/cameraSwitch.js +# Zeigt die neue Log-Zeile → aktueller Code läuft. "1280-Grab" → staler Code. +# +# • Compose v2 ist Pflicht (dockerfile_inline). Bei Portainer-Warnung: Docker-Engine updaten. # ──────────────────────────────────────────────────────────────────────────────── diff --git a/go2rtc.yaml b/go2rtc.yaml deleted file mode 100644 index c40cea6..0000000 --- a/go2rtc.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Hinweis: Diese Datei wird für das Portainer-Deployment NICHT mehr gebraucht – -# die Config ist jetzt direkt in docker-compose.yaml eingebettet (configs.content). -# Sie bleibt hier nur als Referenz / für lokales go2rtc ohne Compose. - -streams: - # Einfache Form: bestätigt funktionsfähig für beide Kameras. - # #video=h264 → für WebRTC (transcodiert) - # #video=mjpeg → für MJPEG-Fallback + /api/frame.jpeg (Snapshot) - cam0: "ffmpeg:/dev/video0#video=h264#video=mjpeg" - cam1: "ffmpeg:/dev/video2#video=h264#video=mjpeg" - -webrtc: - listen: ":8555" - candidates: - - stun:8555 - -api: - listen: ":1984" - # erlaubt WebSocket-Signaling vom Viewer auf Port 8444 (anderer Origin) - origin: "*" - -log: - level: info