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