526 lines
24 KiB
Markdown
526 lines
24 KiB
Markdown
# AppRobotWebcam – Delay / Ruckler-Analyse
|
||
|
||
## Symptom
|
||
|
||
Nach Umstieg auf WebRTC/H.264: Bild ruckelt, friert teils 1–2 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 | ~35–103 % |
|
||
| AppRobotGo2RTC, 2 Clients | 65–114 % |
|
||
| 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 (2–3 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:** ~1–2 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: ~30–40 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: ~1–2 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 ~3–4s (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.
|