From 1af16267f4bab01163308278fa768128713e81a1 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:35:36 +0200 Subject: [PATCH] Claude: ScreenShot Kommentare --- doc/04_Delay_roadmap.md | 120 ++++++++++++++++++++++++++++++++-------- 1 file changed, 97 insertions(+), 23 deletions(-) diff --git a/doc/04_Delay_roadmap.md b/doc/04_Delay_roadmap.md index aef958a..959df2d 100644 --- a/doc/04_Delay_roadmap.md +++ b/doc/04_Delay_roadmap.md @@ -190,15 +190,16 @@ Das erklärt rückwirkend **alles**: ### 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. +Damit zieht der Browser MJPEG statt WebRTC — **go2rtc transcodiert nicht mehr nach H.264.** ``` -CPU ~0% · keine Freezes · ~200ms Latenz · skaliert auf mehr Kameras +keine Freezes · ~200ms Latenz · kein H.264-Transcode ``` -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. +**⚠ 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`. --- @@ -327,9 +328,11 @@ function captureOneFrame(device, size) { } ``` -go2rtc-API-Endpunkte (verifiziert): +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 → startet Producer neu +- `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)** @@ -361,7 +364,7 @@ try { /* ... */ } finally { hiresLock = false; } 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: Option 2 (Blackout-Snapshot) ✓ (implementiert)** +**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`: @@ -374,7 +377,7 @@ GET /api/snapshot/cam0/hires Umgesetzt in `src/snapshotService.js` und `docker-compose.yaml`. -### Erster Live-Test (2026-06-04): erfolgreich + 2 Bugs behoben +### 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: @@ -413,8 +416,10 @@ greift `/dev/video0` zurück, bevor der externe FFmpeg es öffnen kann → „de 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. -**Lösung (umgesetzt): go2rtc-interner Hi-Res-Grab — kein zweiter Prozess.** -go2rtc behält die Geräte-Hoheit. Node schaltet nur kurz dessen Quelle um: +**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 @@ -430,16 +435,85 @@ Bild, vom Browser per CSS skaliert) — der vom Nutzer ausdrücklich akzeptierte Die zweite Kamera ist nicht betroffen. Umgesetzt in `src/snapshotService.js` (externer FFmpeg + `captureOneFrame` entfernt). -### Offene Punkte (ToDo) +--- -- **go2rtc-CPU ~50% bei 2 aktiven Live-Streams.** Besser als H.264-Transcode (~127%), - aber kein echtes Null. go2rtc re-encodiert MJPEG→MJPEG (kein `-c:v copy`) statt - reinem Durchreichen. ~0,5 CPU-Kerne für 2 Kameras → stabil und unkritisch auf dieser - Maschine. **Optionaler Hebel falls je nötig:** prüfen ob go2rtc-Quelle auf echtes - Copy/Passthrough umstellbar ist. Risiko: Stabilität des laufenden Streams — nur - anfassen wenn CPU real zum Problem wird. -- **Cleanup (unkritisch):** Der webcam-Container braucht jetzt **kein** `ffmpeg` und - **keine** `devices`/`group_add: video` mehr (kein externer Grab). Kann beim nächsten - bewussten Aufräumen aus `docker-compose.yaml` raus — aktuell harmlos (nur ungenutzt). -- **Falls der PATCH-Restart je hakt** (frame.jpeg bleibt zu klein/640): Warmup-Zeit - oder Retry-Anzahl in `snapshotService.js` erhöhen (`HIRES_WARMUP_MS`, `HIRES_TRIES`). +## ✅ 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". | + +### 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. **go2rtc-API-Verhalten wird auf einem Wegwerf-Stream verifiziert**, bevor es eine echte Kamera berührt — nie angenommen. +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 in 1280×960, Browser skaliert per CSS auf 640, Snapshot = der +bestehende read-only `frame.jpeg`-Grab (dann schon 1280).** +Der Snapshot-Mechanismus ist hier **garantiert sicher** (read-only, keine Mutation des +Streams — kann den CPU-Vorfall nicht auslösen). Offen ist **nicht die Korrektheit, +sondern der Preis**: Dauerbetrieb 1280 kostet CPU + Bandbreite. Hängt am ungeklärten +Hebel **„kann go2rtc echtes `-c:v copy`?"**: +- Wenn ja → 1280 ist CPU-billig (nur mehr Bytes), Snapshot „gratis", elegant. +- Wenn nein → 1280-Re-Encode ist teuer (vgl. gemessene 53 % / 127 %). +- **Dieser Hebel ist UNVERIFIZIERT → zwingend zuerst auf einem Wegwerf-Stream prüfen, nie auf cam0.** + +**Wovon ich NICHT sicher bin:** dass sich die *eine* Live-Kamera on-demand sicher +zwischen 640 und 1280 umschalten lässt. Das ginge allenfalls über ein **vollständiges +Replace (DELETE + PUT, nicht PATCH)** mit Grab über go2rtcs `frame.jpeg` — aber nur nach +Verifikation auf einem Test-Stream. Ohne diese Verifikation: **kein Versprechen.** + +### Nächster gefahrloser Schritt (wenn Hi-Res wieder dran ist) + +Auf einem **Wegwerf-Stream** `test` (separate Kamera oder Test-Datei, **nicht** cam0) +verifizieren — der Live-Stream wird dabei nicht berührt: +1. Kann go2rtc `-c:v copy` (echtes Passthrough, ~0 % CPU bei beliebiger Auflösung)? → entscheidet Weg C. +2. Wie verhalten sich `PATCH` und `DELETE`+`PUT` wirklich (ersetzen vs. anhängen, Geräte-Freigabe)? + +Erst wenn das **belegt** ist, cam0 anfassen. Bis dahin ist der read-only 640er-Snapshot +(`/api/snapshot/cam{n}`) der stabile Stand. + +### Empfehlung + +- **Budget für Hardware vorhanden → Weg A.** Sauberste, garantierte Lösung, kein Risiko für den Live-Stream. +- **Kein Hardware-Budget → erst den `-c:v copy`-Hebel auf einem Test-Stream verifizieren.** Geht er → Weg C. Geht er nicht → mit dem 1280-Preis leben oder doch Weg A.