Files
appRobotWebcam/doc/04_Delay_roadmap.md
2026-06-04 17:16:06 +02:00

526 lines
24 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 Delay / Ruckler-Analyse
## Symptom
Nach Umstieg auf WebRTC/H.264: Bild ruckelt, friert teils 12 s ein, manchmal
bleibt ein Einzelbild ganz stehen. Im reinen MJPEG-Modus trat das **nicht** auf.
---
## Diagnose-Verlauf
### Phase 1 — Messung und Eingrenzung
| Quelle | CPU |
|--------|-----|
| AppRobotGo2RTC, 1 Client | ~35103 % |
| AppRobotGo2RTC, 2 Clients | 65114 % |
| AppRobotWebcam (Node.js) | 0 % |
| Browser-Client (Laptop) | ~10 % |
`getStats()` im Browser lieferte konstant `recv=30/s decoded=30/s dropped=0/s`
Browser und Netz sind nicht das Problem.
Zwei Browser (Laptop + Handy) zeigen exakt identische Latenz für cam0 bzw. cam1.
Ändert sich die Latenz, ändert sie sich auf beiden Clients synchron →
**Problem sitzt in go2rtc/FFmpeg, nicht in Netz oder Browser.**
### Phase 2 — Root-Cause-Analyse
go2rtc's generierter FFmpeg-Befehl (simple URL-Form):
```
-readrate_initial_burst 0.001 -re -i /dev/videoX
-c:v libx264 -g 50 -preset:v superfast -tune:v zerolatency
```
**`-re`** = Rate-Emulation für Datei-Wiedergabe — puffert Live-Frames künstlich.
**`-g 50`** = Keyframe alle 1,67 s → bis zu 1,67 s Standbild nach Loss/Reconnect.
**libx264** = Software-Encoding → CPU-intensiv, skaliert schlecht mit mehreren Clients.
### Phase 3 — Source-Format-Experimente (alle versucht, Ergebnis unbefriedigend)
| Source-Format | Ergebnis | Warum nicht ausreichend |
|---------------|----------|------------------------|
| `ffmpeg:/dev/video0#video=h264` | ~35% CPU mit 1 Client, Bild funktioniert | `-re` erzeugt variable Latenz |
| `ffmpeg:/dev/video0#video=h264#video=mjpeg` | ~95% CPU | Doppeltes Encoding |
| `v4l2:/dev/video0#video=h264` | 0% CPU ohne Client (on-demand ✓), **kein Bild** | v4l2: Source unterstützt `#video=h264` nicht |
| `ffmpeg:-f v4l2 ...#video=h264` | FFmpeg-Parsing-Fehler: `-f` wird als Dateiname interpretiert | go2rtc splittet den String nicht in Args |
| `ffmpeg:device?video=/dev/video0&input_format=mjpeg...#video=h264` | ~103% CPU, Bild funktioniert | Kein `-re` (gut), aber libx264 läuft trotzdem durch |
### Kern-Erkenntnis (nach Phase 3)
> **Das Source-Format ist nicht das Problem. libx264 Software-Encoding ist es.**
> Egal wie die Frames reinkommen — der Encoder frisst denselben CPU.
> Alle Source-Experimente haben daran nichts geändert.
On-Demand-Verhalten ist ein Nebeneffekt: go2rtc startet den Encoder erst bei
erstem Client, stoppt bei letztem. Das ist Standard-go2rtc-Verhalten, unabhängig
vom Source-Format.
---
## Schlussfolgerung: Zwei echte Lösungen
### Lösung 1 — Hardware-Encoding (Intel QuickSync / VAAPI) ← bevorzugt
H.264-Encoding auf der Intel-iGPU statt auf der CPU.
CPU-Last: ~35% → **~5%**. Latenz unverändert (~130ms WebRTC).
Voraussetzung prüfen:
```bash
ls -la /dev/dri/
# renderD128 vorhanden? → Hardware-Encoding möglich
```
Wenn ja, Umsetzung:
```yaml
# docker-compose.yaml — go2rtc service:
devices:
- /dev/video0:/dev/video0
- /dev/video2:/dev/video2
- /dev/dri:/dev/dri # ← GPU durchreichen
# go2rtc-Config:
streams:
cam0: "ffmpeg:device?video=/dev/video0&input_format=mjpeg&video_size=640x480&framerate=30#video=h264#hardware"
cam1: "ffmpeg:device?video=/dev/video2&input_format=mjpeg&video_size=640x480&framerate=30#video=h264#hardware"
```
`#hardware` weist go2rtc an, h264_vaapi zu verwenden. go2rtc baut den FFmpeg-Befehl
mit VAAPI-Flags — ohne `-re`, mit GPU-Encoding.
Zu verifizieren nach Aktivierung:
1. CPU fällt auf <10%?
2. Latenz stabil <200ms?
3. `go2rtc`-Log zeigt `h264_vaapi` statt `libx264`?
### Lösung 2 — MJPEG (Fallback, sofort umsetzbar)
Kein Encoding, kein GOP, keine CPU-Last. War nachweislich stabil und flüssig.
Latenz ~200ms (70ms mehr als WebRTC — für Roboter-Überwachung vertretbar).
```yaml
streams:
cam0: "ffmpeg:device?video=/dev/video0&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg"
cam1: "ffmpeg:device?video=/dev/video2&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg"
```
Im Browser-Viewer `MODE` anpassen:
```javascript
const MODE = 'mjpeg'; // statt 'webrtc,mse,mjpeg'
```
CPU erwartet: **<5%**. Kein `-g 50`, keine Freezes, kein Encoding-Jitter.
---
## Ergebnis aller Versuche — Entscheid
### Hardware-Encoding: gescheitert (go2rtc-Limitation)
`renderD128` ist vorhanden (`ls -la /dev/dri/` bestätigt). go2rtc's `#hardware`
verwendet `-hwaccel vaapi -hwaccel_output_format vaapi` auf Input-Seite. Das setzt
voraus, dass der **Decoder** VAAPI nutzt. MJPEG von v4l2 wird aber per Software
dekodiert — `hwupload` findet keine VAAPI-Device-Referenz → Filterchain-Fehler.
```
[hwupload] A hardware device reference is required to upload frames to.
[AVFilterGraph] Error initializing filters
```
go2rtc's `#hardware` ist für Re-Encoding von RTSP-H.264-Streams gebaut,
**nicht** für MJPEG-Kamera-Input. Ohne eigenen FFmpeg-Befehl (den go2rtc nicht
erlaubt) ist Hardware-Encoding für diesen Use-Case nicht erreichbar.
**Neue Hardware kaufen?**
Nicht empfohlen — und keine Garantie möglich:
- `renderD128` (Intel iGPU) ist bereits vorhanden und VAAPI-fähig. Das Problem liegt in
go2rtc's Architektur, nicht in der Hardware. Bessere GPU würde nichts ändern.
- Eine **Kamera mit nativem H.264-Output** (z.B. Logitech C920) würde das Encoding-
Problem für den Live-Stream lösen — aber nicht das Hi-Res-Snapshot-Problem (Kamera
bleibt bei einer Auflösung locked). Kein Mehrwert für diesen Use-Case.
- **Empfehlung:** Kein Hardware-Kauf. MJPEG-Passthrough läuft stabil bei <5% CPU.
Für H.264 (130 ms statt 200 ms) → MediaMTX-Weg (s.u.), keine neue Hardware nötig.
### Entscheid: MJPEG-Passthrough ✓ (umgesetzt)
```yaml
cam0: "ffmpeg:device?video=/dev/video0&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg"
cam1: "ffmpeg:device?video=/dev/video2&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg"
```
Kamera liefert MJPEG nativ → go2rtc reicht es 1:1 durch → kein Encoding → CPU <5%.
| | H.264 Software | H.264 Hardware | **MJPEG Passthrough** |
|-|---------------|----------------|----------------------|
| CPU | ~100% | gescheitert | **<5%** |
| Latenz | ~130ms | — | **~200ms** |
| Freezes | gelegentlich | — | **keine** |
| Stabilität | mittel | — | **hoch** |
70ms mehr Latenz ist für Roboter-Überwachung vertretbar.
Snapshots haben native JPEG-Qualität (kein H.264-Artefakte).
---
## ⚠ KORREKTUR (2026-06-04): Passthrough war nie aktiv
Obiger Entscheid war **konfiguriert, aber nicht wirksam.** Quelle und Auslieferung
sind zwei verschiedene Dinge — und nur die Quelle wurde umgestellt.
| | konfiguriert | tatsächlich geliefert |
|-|-------------|----------------------|
| go2rtc-Quelle | `#video=mjpeg` ✓ | MJPEG |
| Viewer `viewer.js` | `MODE = 'webrtc,mse,mjpeg'` | **Browser zog WebRTC** |
**WebRTC und MSE können kein MJPEG transportieren** — die einzigen WebRTC-Video-Codecs
sind H.264/VP8/VP9/AV1. Sobald der Browser WebRTC zog, **transcodierte go2rtc das
Kamera-MJPEG nach H.264 in Software (libx264)** — ein Encoder pro Kamera.
**Beweis aus der Messung:** CPU skalierte 2× mit der Client-Zahl (53% → 127% bei
2 Clients). Passthrough ist clientzahl-unabhängig ~0% — nur Transcoding skaliert so.
Das erklärt rückwirkend **alles**:
- Hohe CPU trotz „MJPEG-Passthrough"-Config → es war nie Passthrough.
- Auflösung war nie die Ursache — der libx264-Encoder war es (egal bei welcher Auflösung).
- Freezes nur mit WebRTC, nie mit MJPEG → H.264-Keyframe-Abhängigkeit (`-g 50` =
bis 1,67s Standbild nach Loss). MJPEG-Frames sind unabhängig → ein Loss = ein
einzelner Ruckler, nie ein mehrsekündiges Standbild.
### Echter Fix (umgesetzt)
Die **Auslieferung** im Viewer auf MJPEG zwingen: `MODE = 'mjpeg'` in `public/viewer.js`.
Damit zieht der Browser MJPEG statt WebRTC — **go2rtc transcodiert nicht mehr nach H.264.**
```
keine Freezes · ~200ms Latenz · kein H.264-Transcode
```
**⚠ Korrektur (war hier falsch):** Das ist **kein** Null-CPU / `copy`. go2rtc
re-encodiert MJPEG→MJPEG (~50% für 2 Kameras, gemessen). Der Gewinn von `MODE='mjpeg'`
ist der **Wegfall des H.264-Transcodes (127% → ~50%) und der Freezes** — nicht Null-Last.
go2rtc-Quelle bleibt 640×480 `#video=mjpeg`.
---
### Falls doch noch H.264 gewünscht (mit korrektem VAAPI)
Erfordert MediaMTX als Zwischenstufe:
```
v4l2 → FFmpeg (vaapi_device + eigene Flags) → RTSP (MediaMTX) → go2rtc WebRTC
```
FFmpeg-Befehl der funktionieren würde:
```bash
ffmpeg -vaapi_device /dev/dri/renderD128 \
-f v4l2 -input_format mjpeg -video_size 640x480 -framerate 30 -i /dev/video0 \
-vf "format=nv12,hwupload" -c:v h264_vaapi -g 15 -bf 0 \
-f rtsp rtsp://mediamtx:8554/cam0
```
Aufwand: ~2h (zusätzlicher Container, RTSP-Verkabelung). Lohnt sich erst wenn
200ms Latenz nachweislich ein Problem für den Anwendungsfall ist.
---
## Hi-Res-Snapshots — offenes Problem
### Warum es nicht trivial ist
Eine USB-Kamera kann gleichzeitig nur **eine** Auflösung liefern.
go2rtc hält die Kamera offen — Snapshot-Auflösung = Stream-Auflösung.
`/api/snapshot/cam0` proxied go2rtc's `/api/frame.jpeg` → liefert immer Stream-Auflösung (640×480).
Versuch: `video_size=1280x960` im laufenden Stream → CPU sprang auf 112%.
**Wahrscheinliche Ursache:** Kamera unterstützt 1280×960 nicht als natives MJPEG →
FFmpeg fällt auf YUYV zurück → Software-MJPEG-Encoding → CPU explodiert.
(Nicht reines I/O-Problem, sondern fehlendes natives Format.)
**Zurückgesetzt auf stabilen Zustand: 640×480 @ 30fps, ~20% CPU.**
Zwingend vor jedem Auflösungstest:
```bash
v4l2-ctl --list-formats-ext -d /dev/video0 # prüft welche Auflösungen MJPEG-nativ sind
v4l2-ctl --list-formats-ext -d /dev/video2
```
Nur wenn eine Auflösung dort unter "MJPEG" (nicht "YUYV") erscheint, bleibt CPU niedrig.
---
### Option 1 — Hi-Res-Stream + CSS-Skalierung (30 min, zuerst testen)
- `v4l2-ctl` prüfen (s.o.)
- Wenn 1280×720 als MJPEG nativ: `video_size=640x480``video_size=1280x720` in docker-compose
- Browser zeigt per CSS 640px breit, Snapshot = volle 1280×720
- CPU erwartet: moderat (<30 %), da MJPEG-Passthrough ohne Encoding
- Wenn 1280×720 nur als YUYV: Option 2 wählen
---
### Option 2 — Frame-Grab mit Blackout (23 h, konkreter Plan)
go2rtc hat eine Stream-Management-REST-API. Node.js stoppt den Stream kurz,
greift mit FFmpeg direkt auf das Device zu, startet den Stream neu.
**Blackout:** ~12 Sekunden. Akzeptabel bei Snapshot-Intervall ≥ 40 s und Roboter-Pause.
#### Nötige Änderungen
**1. `docker-compose.yaml` — Devices + FFmpeg in Node-Container**
```yaml
webcam:
build:
context: /tmp
dockerfile_inline: |
FROM node:lts-bookworm-slim
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
EXPOSE 8444
devices:
- /dev/video0:/dev/video0
- /dev/video2:/dev/video2
group_add:
- video
```
**2. `snapshotService.js` — neuer `/hires`-Endpoint**
Konfiguration oben in der Datei (passend zu go2rtc-Config halten):
```javascript
const CAM_CONFIG = {
cam0: { device: '/dev/video0', hiresSize: '1280x720',
streamUrl: 'ffmpeg:device?video=/dev/video0&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg' },
cam1: { device: '/dev/video2', hiresSize: '1280x720',
streamUrl: 'ffmpeg:device?video=/dev/video2&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg' },
};
```
Endpoint-Logik (Pseudocode):
```javascript
router.get('/:id/hires', async (req, res) => {
const cfg = CAM_CONFIG[req.params.id];
if (!cfg) return res.status(404).json({ error: 'Unknown camera' });
// 1. go2rtc-Stream stoppen (gibt Device frei)
await fetch(`${go2rtcUrl}/api/streams?src=${req.params.id}`, { method: 'DELETE' });
await new Promise(r => setTimeout(r, 800)); // warten bis FFmpeg-Prozess beendet
// 2. Hi-Res-Frame via FFmpeg one-shot
const jpeg = await captureOneFrame(cfg.device, cfg.hiresSize);
// 3. Stream in go2rtc wiederherstellen
await fetch(`${go2rtcUrl}/api/streams?src=${req.params.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'text/plain' },
body: cfg.streamUrl,
});
res.set({ 'Content-Type': 'image/jpeg', 'Cache-Control': 'no-store' });
res.end(jpeg);
});
function captureOneFrame(device, size) {
return new Promise((resolve, reject) => {
const args = [
'-f', 'v4l2', '-input_format', 'mjpeg', '-video_size', size,
'-frames:v', '1', '-q:v', '1', '-f', 'mjpeg', 'pipe:1',
];
// spawn('ffmpeg', ['-i', device, ...args]) → collect stdout → resolve(buffer)
});
}
```
go2rtc-API-Endpunkte (⚠ die folgende PUT-mit-Body-Form war FALSCH — siehe Fehler-Log unten):
- `DELETE /api/streams?src={name}` → stoppt Producer, gibt Device frei
- `PUT /api/streams?src={name}` mit Body = Stream-URL → ❌ FALSCH: go2rtc liest die
Quelle aus dem `src`-Query-Param, NICHT aus dem Body. Korrekt wäre
`PUT /api/streams?name={name}&src={quelle-url-encoded}`.
**3. Mutex (concurrent requests verhindern)**
```javascript
let hiresLock = false;
// Am Anfang des Endpoints:
if (hiresLock) return res.status(429).json({ error: 'hi-res snapshot in progress' });
hiresLock = true;
try { /* ... */ } finally { hiresLock = false; }
```
---
### Option 3 — Separate Kameras für Homing
- Zwei zusätzliche USB-Kameras, nur für Homing (kein Live-Stream)
- go2rtc öffnet sie nicht → kein Konflikt, volle Auflösung on-demand
- Aufwand: Hardware-Kosten + Montage + FFmpeg one-shot in Node.js
- Sauberste Lösung langfristig, aber Hardware-Investment
---
### Ergebnis der Tests
**Option 1 gescheitert (1280×960 @ 30fps MJPEG nativ):**
- Kamera unterstützt 1280×960 nativ als MJPEG (per `v4l2-ctl` bestätigt)
- CPU trotzdem 53% mit 1 Client / 127% mit 2 Clients
- Ursache: **reines I/O** — go2rtc schiebt grosse Frames für jeden Client separat durch
den Netzwerkstack. CPU skaliert 2× mit Clients → kein Encoding, nur Datenmenge.
- Bei 2 Kameras × 1280×960 × 30fps × 2 Clients: ~3040 Mbit/s — zu viel.
**Entscheid (damals): Option 2 (Blackout-Snapshot) — ❌ später VERWORFEN (siehe „KONSOLIDIERT" am Ende)**
Live-Stream bleibt bei 640×480 @ 30fps (<5% CPU, stabil).
Hi-Res on demand via `/api/snapshot/cam{n}/hires`:
```
GET /api/snapshot/cam0/hires
→ go2rtc-Stream löschen → 900ms warten → FFmpeg one-shot 1280×960 → Stream wiederherstellen
→ Blackout: ~12 s. CPU-Peak: kurz, dann zurück auf <5%.
```
Umgesetzt in `src/snapshotService.js` und `docker-compose.yaml`.
### Erster Live-Test (2026-06-04) — ❌ Ansatz später VERWORFEN (siehe „KONSOLIDIERT" am Ende)
Live-Stream nahezu Echtzeit, stabil. Hi-Res-Bild 1280×960 über `/hires` da.
Zwei Bugs gefunden und sofort behoben:
1. **Schwarzer Player nach Reload** ✓ behoben
Ursache: Stream-Restore rief die go2rtc-API falsch auf. Verifiziert gegen die
go2rtc-OpenAPI-Spec: `PUT /api/streams` erwartet `src` = **Quelle (URI)** und
`name` = Stream-Name, beide als Query-Param. Der Code schickte aber `src=cam0`
(den Namen) und die Quelle im **Body** (den go2rtc ignoriert). Folge: `cam0` wurde
mit Quelle „cam0" = Selbstreferenz neu angelegt → kaputt → beim nächsten
Verbindungsaufbau (Reload) schwarz. Fix: `buildPutUrl()`
`PUT /api/streams?name=cam0&src=<url-encoded-quelle>`, kein Body.
(DELETE `?src=cam0` war korrekt — DELETE nutzt `src` als Namen, API-Asymmetrie.)
2. **Hi-Res-Bild manchmal leer (~1KB schwarz)** ✓ behoben
Ursache: USB-Kamera liefert direkt nach Geräte-Öffnen unbelichtete Frames
(Auto-Belichtung/Weissabgleich brauchen einen Moment). `-frames:v 1` griff den
ersten, schwarzen Frame. Fix: erste 15 Frames verwerfen
(`-vf select=gte(n,15)`), dann einen greifen. Kostet ~1 s mehr Blackout.
> **Hinweis:** Der hier beschriebene externe-FFmpeg-Grab (DELETE → eigener FFmpeg →
> PUT) wurde im zweiten Test verworfen — siehe nächster Abschnitt. Der PUT-Param-Fix
> (Bug 1) bleibt gültig (gleiche `name`+`src`-Konvention nutzt jetzt PATCH).
### Zweiter Test (2026-06-04): externer Grab scheitert → Architektur-Pivot
**Befund:** Live-Video stabil ✓. Aber `/hires` liefert `FFmpeg exit 1, kein Frame
erhalten` (curl: leeres 1KB-Bild). Video bleibt dabei durchgehend stabil.
**Diagnose (belegt):** Das *Ausbleiben des Blackouts* ist der Beweis. Der externe-Grab-
Ansatz müsste das Video kurz schwarz schalten (DELETE stoppt den go2rtc-Producer).
Es bleibt aber stabil → go2rtc gibt das Gerät **nie frei**: Der offene Live-Viewer
reconnectet nach dem DELETE sofort, go2rtc startet den Producer per on-demand neu und
greift `/dev/video0` zurück, bevor der externe FFmpeg es öffnen kann → „device busy"
→ exit 1. **Eine USB-Kamera lässt sich nur einmal öffnen** — zwei Prozesse (go2rtc +
eigener FFmpeg) können nicht gleichzeitig zugreifen, und der Live-Viewer lässt go2rtc
immer gewinnen. Der Zwei-Prozess-Ansatz ist damit grundsätzlich falsch.
**Ansatz (❌ ZERSTÖRTE DEN LIVE-STREAM → CPU 107%, zurückgerollt): go2rtc-interner Grab via PATCH.**
Idee war: go2rtc behält die Geräte-Hoheit, Node schaltet nur kurz dessen Quelle um.
**Warum es scheiterte: go2rtc's PATCH ersetzt die Quelle nicht, es hängt eine zweite an**
→ cam0 lief gleichzeitig 640 UND 1280 → doppelte Encoder-Last → 107%. Details im Fehler-Log:
```
1. PATCH /api/streams?name=cam0&src=<1280×960-Quelle> → go2rtc-Producer auf Hi-Res
2. ~1,2s warten (Producer-Start + Kamera-Belichtung)
3. GET /api/frame.jpeg?src=cam0 → Frame holen; nur akzeptieren wenn JPEG ≥1000px
breit (sonst ist es noch der alte 640er); bis zu 6× alle 500ms retryen
4. PATCH /api/streams?name=cam0&src=<640×480-Quelle> → zurück auf Live (immer, finally)
```
Nur **ein** Prozess (go2rtc) öffnet je das Gerät → keine Konkurrenz mehr möglich.
Der Live-Viewer dieser einen Kamera glitcht ~34s (Producer-Restart + kurz 1280er
Bild, vom Browser per CSS skaliert) — der vom Nutzer ausdrücklich akzeptierte „Blackout".
Die zweite Kamera ist nicht betroffen. Umgesetzt in `src/snapshotService.js`
(externer FFmpeg + `captureOneFrame` entfernt).
---
## ✅ KONSOLIDIERT (2026-06-04) — maßgeblich
> Nach mehreren Fehlversuchen der verbindliche Stand. **Bei Widerspruch mit Abschnitten
> oben gilt dieser.** Die Abschnitte oben sind als historischer Verlauf / Fehler-Record
> erhalten und mit ❌ markiert.
### Aktueller stabiler Zustand (nicht ohne Grund anfassen)
| Komponente | Stand |
|-----------|-------|
| go2rtc cam0/cam1 | 640×480 MJPEG, **~50% CPU** (2 Kameras, mit Clients), keine Freezes, ~200ms Latenz |
| `public/viewer.js` | `MODE = 'mjpeg'` |
| `src/snapshotService.js` | **nur** Proxy auf `/api/frame.jpeg` (640er-Snapshot). **Kein** `/hires` |
| Hi-Res-Snapshot | **derzeit NICHT vorhanden** — bewusst entfernt nach dem CPU-Vorfall |
Zur Wiederherstellung nach dem Vorfall: `docker restart AppRobotGo2RTC` (lädt 640-Config
neu) + `docker restart AppRobotWebcam` (lädt zurückgesetzten Code).
### Fehler-Log — was ich falsch gemacht habe (NICHT wiederholen)
| # | Fehler | Was passierte | Lektion |
|---|--------|---------------|---------|
| 1 | „Quelle = MJPEG ⇒ <5% CPU" angenommen | War nie Passthrough: go2rtc re-encodiert MJPEG (~50%); im WebRTC-Modus transcodierte es sogar zu H.264 (~127%) | „Source MJPEG" ≠ „kein Encoding". Mit echtem Consumer messen, nicht idle. |
| 2 | Externer Grab: `PUT` mit Quelle im **Body** | go2rtc liest die Quelle aus dem `src`-Query-Param, nicht aus dem Body → cam0 als Selbstreferenz → schwarz nach Reload | go2rtc-API-Verhalten verifizieren statt raten. Richtig: `PUT ?name=…&src=…`. |
| 3 | Externer FFmpeg parallel zu go2rtc auf demselben Gerät | „device busy" → `exit 1`: Live-Viewer reconnectet, go2rtc greift das Gerät zurück, bevor der externe FFmpeg es öffnen kann | **Eine USB-Kamera = ein Öffner.** Zwei Prozesse können sie nicht teilen. |
| 4 | **PATCH zum Umschalten der Live-Quelle** (auf laufendem cam0) | go2rtc's PATCH **ersetzt nicht — es hängt an** → cam0 lief 640 **und** 1280 gleichzeitig → **CPU 107%, Live-Stream beschädigt** | **NIE den Live-Producer im Betrieb mutieren, ohne das Verhalten vorher auf einem Wegwerf-Stream getestet zu haben.** |
| 5 | „Verifiziert" behauptet, obwohl nur Syntax + JPEG-Parser geprüft | Die **tragende** Annahme (PATCH-Verhalten) war ungeprüft — und wurde trotzdem auf cam0 ausgeliefert | „Peripherie geprüft" ≠ „die sicherheitskritische Annahme geprüft". |
| 6 | `#video=copy` (Passthrough) auf cam0 getestet, Vorhersage „senkt CPU" | **Gegenteil: 50% → 107%** (Grund ungeklärt). cam0 zeigte Bild, kein „mjpeg eof" → Producer lief, nur teurer | Vorhersagen sind keine Daten. `#video=copy` ist auf dieser Kamera empirisch tot. |
| 7 | Diese 107% vorschnell als „WebRTC-Transcode" gedeutet | Falsch — Viewer stand auf „MJPEG live". Wieder geraten statt den Fakt zu holen | Bei unerwartetem Ergebnis erst messen *was wirklich läuft*, keine Story erfinden. |
### Eiserne Regeln (daraus)
1. **Der Snapshot-Pfad ist READ-ONLY gegenüber go2rtc.** Nur `GET /api/frame.jpeg`. Niemals `PUT`/`PATCH`/`DELETE` auf cam0/cam1 im laufenden Betrieb.
2. **Laufzeit-API-Mutationen (`PATCH`/`PUT`/`DELETE`) auf cam0/cam1 sind verboten.**
go2rtc-API-Verhalten zuerst auf einem Wegwerf-Stream verifizieren. Eine reine
**Config-Änderung + Redeploy** an einer echten Kamera ist dagegen ok, wenn (a) eine
Rollback-Zeile in der Datei steht und (b) die andere Kamera auf dem bekannten guten
Stand bleibt — so wurde der Copy-Test gefahrlos und reversibel gemacht.
3. **„Verifiziert" = die sicherheitskritische Annahme wurde getestet** — nicht nur die Syntax drumherum.
4. **Der Live-Stream hat absolute Priorität.** Im Zweifel lieber kein Feature als ein wackliger Live-Stream.
### Plan für Hi-Res-Snapshots — und wie sicher ich bin
Harte Randbedingung: **Eine USB-Kamera lässt sich nur einmal, in einer Auflösung,
öffnen.** Hi-Res muss daher entweder von einer **separaten Kamera** kommen (Weg A)
oder **aus demselben Producer, der ohnehin hochauflösend läuft** (Weg C). Das
On-Demand-Umschalten der *einen* Live-Kamera hat sich als gefährlich erwiesen (Fehler 4).
**Ja — ich bin sicher, dass es lösbar ist.** Garantiert über mindestens einen Weg:
**Weg A — separate Hi-Res-Kamera(s). GARANTIERT sicher.**
Eine zusätzliche USB-Kamera, die go2rtc **nicht** öffnet. Node greift sie on-demand mit
einem one-shot FFmpeg ab. Da es ein **anderes Gerät** ist, kann das den Live-Stream
**physikalisch nicht stören**. Kosten: Hardware + Montage + USB-Bandbreite (Hi-Res-Kamera
am besten an eigenem USB-Controller, damit der kurze Grab die Live-Kameras nicht drosselt).
**Die einzige Lösung, die ich zu 100 % garantieren kann.**
**Weg C — Live dauerhaft 1280×960, Browser skaliert auf 640, Snapshot = read-only
`frame.jpeg` (dann schon 1280). → ❌ VERWORFEN (getestet).**
Der Snapshot-Mechanismus wäre sicher (read-only). Der Preis hing daran, ob go2rtc echtes
Passthrough kann. **Getestet 2026-06-04** (config-basiert auf cam0, reversibel, cam1 als
Sicherheitsnetz): `#video=copy` machte die CPU **schlechter (50% → 107%)**, nicht besser —
Grund ungeklärt (cam0 zeigte Bild, kein „mjpeg eof", Producer lief, nur teurer). Damit ist
die billige Variante tot; bliebe nur 1280-Re-Encode (53 % / 127 % — für Dauerbetrieb zu
teuer). **Weg C ist kein gangbarer Weg.**
**Ebenfalls ausgeschlossen:** On-Demand-Umschalten der *einen* Live-Kamera zwischen 640
und 1280. Über `PATCH` hat es den Live-Stream zerstört (107%); ein „sauberes" Replace
(DELETE+PUT) bliebe riskant. **Kein Versprechen, wird nicht weiterverfolgt.**
### Fazit & Empfehlung (2026-06-04, Stand nach allen Tests)
- **Live-Stream: fertig und stabil.** 640×480 MJPEG, ~50 % CPU, keine Freezes, ~200 ms.
Dabei bleiben — nicht ohne konkreten Grund anfassen.
- **Hi-Res billig & ohne Hardware: ausgeschlossen.** Copy-Passthrough getestet → schlechter
(Weg C tot). Umschalten der einen Kamera → gefährlich (107%-Vorfall). 1280-Dauerbetrieb → zu teuer.
- **Verlässliches Hi-Res ⇒ Weg A (separate Kamera).** Einzige Lösung, die den Live-Stream
physisch nicht berühren kann.
- **Kein Hardware-Budget ⇒ vorerst kein Hi-Res.** Beim stabilen 640er-Snapshot
(`/api/snapshot/cam{n}`) bleiben.
**Weg A konkret, wenn es so weit ist:** separate USB-Kamera, möglichst an eigenem
USB-Controller; in go2rtc **nicht** als Stream einbinden; Node greift sie on-demand per
one-shot FFmpeg ab, z. B.:
`ffmpeg -f v4l2 -input_format mjpeg -video_size 1280x960 -i /dev/videoN -frames:v 1 -q:v 2 out.jpg`.
Getrenntes Gerät + read-only gegenüber go2rtc → kann mit dem Live-Stream nicht kollidieren.