Claude: WebSocket

This commit is contained in:
chk
2026-06-04 15:06:45 +02:00
parent 118441995d
commit 306aacac80
4 changed files with 378 additions and 52 deletions

View File

@@ -132,6 +132,16 @@ 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
@@ -151,6 +161,47 @@ Kamera liefert MJPEG nativ → go2rtc reicht es 1:1 durch → kein Encoding →
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 ist die Kette durchgängig MJPEG: **Kamera → go2rtc (copy) → Browser.** Kein Encoder.
```
CPU ~0% · keine Freezes · ~200ms Latenz · skaliert auf mehr Kameras
```
go2rtc-Quelle bleibt 640×480 `#video=mjpeg`. **Hardware-Encoding ist damit
gegenstandslos** — es wird gar nicht mehr encodiert. Der ganze VAAPI-Strang unten
ist nur noch relevant, falls später doch WebRTC-Latenz (~130ms) zwingend gebraucht wird.
---
### Falls doch noch H.264 gewünscht (mit korrektem VAAPI)
Erfordert MediaMTX als Zwischenstufe:
@@ -175,38 +226,150 @@ Aufwand: ~2h (zusätzlicher Container, RTSP-Verkabelung). Lohnt sich erst wenn
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%.
Ursache unklar (vermutlich MJPEG-Frames 4× grösser → mehr I/O-Last in go2rtc).
**Zurückgesetzt auf stabilen Zustand: 640x480 @ 30fps, ~20% CPU.**
**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.**
### Drei Optionen (noch nicht umgesetzt)
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 im Browser**
- Stream auf 1280x720 oder 1280x960 setzen
- Browser zeigt 640x480 (CSS), Snapshot = volle Auflösung
- Problem: CPU-Last beim Hochskalieren steigt (s.o.)
- Lösung: erst `v4l2-ctl --list-formats-ext -d /dev/video0` prüfen welche
MJPEG-Auflösungen die Kamera nativ unterstützt. Dann schrittweise testen:
640x480 → 1280x720 → 1280x960. CPU nach jedem Schritt messen.
- Zeitaufwand: 30 min
---
**Option 2Stream stoppen, Snapshot, Stream starten**
- Node.js orchestriert: go2rtc-Stream stoppen → FFmpeg einmalig
`-frames:v 1` bei maximaler Auflösung → Bild speichern → Stream neu starten
- Video-Blackout: ~12 Sekunden
- CPU-Peak: kurz, dann zurück auf normal
- Aufwand: ~2h (Node.js Orchestrierungslogik + go2rtc Stream-API)
- Geeignet wenn Snapshots nur alle 1030s gebraucht werden
### Option 1Hi-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 (verifiziert):
- `DELETE /api/streams?src={name}` → stoppt Producer, gibt Device frei
- `PUT /api/streams?src={name}` mit Body = Stream-URL → startet Producer neu
**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
**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
- Sauberste Lösung langfristig, aber Hardware-Investment
### Empfehlung
---
Option 1 zuerst, aber schrittweise mit CPU-Messung pro Auflösungsstufe.
Option 2 wenn Blackout akzeptabel und Option 1 zu viel CPU braucht.
Option 3 wenn Homing-Qualität kritisch und Budget vorhanden.
### 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: Option 2 (Blackout-Snapshot) ✓ (implementiert)**
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`.