Files
appRobotWebcam/doc/09_Bug_reports.md
2026-06-05 07:32:05 +02:00

152 lines
8.0 KiB
Markdown
Raw Permalink 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.
## Multi-User ##
> ✅ **GELÖST (2026-06-05) durch den Node-MJPEG-Schalter.** Genau die „Schalter"-Idee aus
> Option 2 wurde umgesetzt: ein FFmpeg pro Gerät, der Server verteilt an alle Browser
> (Fan-out). Clients halten kein Gerät mehr → HD-Grab pausiert nur kurz die eine Quelle,
> unabhängig von der Client-Zahl. Details: `05_screenShot_roadmap.md`. (Beschreibung unten
> = ursprünglicher Bug-Report.)
Wenn zwei (oder mehr) User streamen, dann kann kein High-Res Bild mehr
gemacht werden. Das Problem: Der Stream bleibt irgendwo aufrecht erhalten.
Fix-Vorschlag Option 1: Alle Browser erhalten bei einem High-Res Request
die Anweisung, den Stream abzumelden.
Fix-Vorschlag Option 2: Der Stream wird umgeleitet zu einer Zwischenstelle "Schalter" und erst von dort aus an die Browser verteilt. Dann wird die
"unterbrechung" bzw. "umleitung" des Streams serverseitig erledigt.
## BugReport 106% ##
Anmelden abmelden funktioniert. Es bleibt bei 35% Prozessor-Last
Anmelden Screenshot funktioniert. Es bleibt bei 35% Prozessor-Last
Anmelden Screenshot neu anmelden > Es gibt 108% Prozessor-Last und ein Stream friert ein.
Wenn ich den Container restarte, funktioniert es.
siehe auch doc/04_Delay_roadmap.md sowie doc/05_screenshot_roadmap.md
### Root Cause (2026-06-05)
**Race condition:** `cam_hires` und `cam` belegen kurz gleichzeitig dasselbe `/dev/videoN`.
```
1. stopStream(cam0) → 0 Consumer → go2rtc stoppt cam0-FFmpeg → /dev/video0 frei
2. Server: frame.jpeg → go2rtc startet cam0_hires-FFmpeg auf /dev/video0 (1280×960)
3. Response fertig → go2rtc sendet SIGTERM an cam0_hires-FFmpeg
4. Server antwortet Client (OHNE auf FFmpeg-Exit zu warten!) ← Fehler
5. Client sleep(600ms) → startStream(cam0) → go2rtc startet cam0-FFmpeg auf /dev/video0
⚠ cam0_hires-FFmpeg hält /dev/video0 noch offen → 2 FFmpeg auf 1 Device → 108%
```
Der Konflikt in Schritt 5 versetzt `cam0_hires` in einen Fehlerzustand in go2rtc.
Beim nächsten Reconnect ("neu anmelden") startet go2rtc's Retry `cam0_hires` erneut —
gleichzeitig mit `cam0` → wieder 108% + Stream friert ein.
### ❌ Fix-Versuch 1 (2026-06-05) GESCHEITERT — „Warten bis cam_hires gestoppt"
Idee war: Server pollt nach dem frame.jpeg-Fetch, bis `cam_hires`-Producer nicht mehr
`state=='running'` ist (+400ms Puffer), erst dann Antwort an Client.
**Der Log beweist: der Poll ist ein No-op.**
```
[hires][cam1] Versuch 1: 90586 bytes, Breite=1280
[hires][cam1] cam_hires gestoppt nach 1ms Gerät frei ← bricht SOFORT ab
[hires][cam0] cam_hires gestoppt nach 1ms Gerät frei
```
go2rtc meldet den hires-Producer schon **1ms** nach dem Frame als „nicht running" —
obwohl der FFmpeg gerade eben noch ein 1280-Bild geliefert hat. Die tragende Annahme
(*API-State `running` ⇒ FFmpeg hält das Device*) ist **falsch**. Übrig bleibt nur das
blinde `sleep(400)`. Das ist exakt der Fehler-Typ aus 04 (#5/#7): „verifiziert"
behauptet, aber die sicherheitskritische Annahme war ungemessen.
**Folge: jetzt löst schon EIN Screenshot 106% aus** (vorher erst beim Reconnect). Erklärung:
Es war immer ein **Race auf dem geteilten `/dev/videoN`**. Die 400ms haben das Timing nur
verschoben — jetzt landen wir auf der schlechten Seite. **Timing-Pflaster verschieben das
Race, sie beseitigen es nicht.** → zurückgerollt (Schritt 3 wieder entfernt).
### Eigentliches Problem (eine Ebene tiefer)
`cam` (640) und `cam_hires` (1280) zeigen auf **dasselbe physische `/dev/videoN`**.
Eine USB-Kamera = ein Öffner (eiserne Regel 04). 106% = **zwei Encoder gleichzeitig** auf
einem Device (bestätigt: nicht „device busy → exit", sondern beide laufen).
**Kernhürde:** go2rtc's REST-API kann **nicht** zuverlässig sagen, wann FFmpeg den
Device-FD wirklich freigegeben hat. Die State-Felder flippen, bevor der Kernel-FD frei
ist (beim Start zeigte der Monitor sogar `cam1_hires producer state="unknown"`). Damit
ist **jede Timing-basierte Übergabe ein Ratespiel**.
Der **Hinweg** (Schritt 1: warten bis `cam` frei, bevor `cam_hires` startet) funktioniert
— er wartet echt (4870ms / 5069ms im Log). Kaputt ist der **Rückweg** (`cam_hires``cam`):
dort wird nicht zuverlässig gewartet.
### ✅ GELÖST (2026-06-05) — Node-MJPEG-Schalter, go2rtc entfernt
Statt go2rtc zu orchestrieren (blind, racet weiter) **besitzt Node die Kameras jetzt
selbst**. Damit liefert das `close`-Event des selbst gestarteten FFmpeg den harten Beweis
„Gerät frei" — genau das Signal, das go2rtcs API verweigerte. Das Race ist **eliminiert,
nicht getimt**. Details der finalen Architektur: `doc/05_screenshot_roadmap.md`.
**Was sich änderte:**
| Komponente | vorher (go2rtc) | jetzt (Node-Schalter) |
|-----------|-----------------|----------------------|
| Geräte-Öffner | go2rtc | **Node** (`src/cameraSwitch.js`, eine Instanz pro Gerät) |
| Live-Auslieferung | go2rtc WS + `video-stream.js` | MJPEG `multipart/x-mixed-replace``<img>` |
| HD-Snapshot | 2. go2rtc-Stream `cam_hires` (Race!) | Schalter stoppt Live (Prozess-`close` = FD frei), greift 1280, zurück |
| Multi-User | brach (Consumer ≠ 0) | **gelöst**: ein FFmpeg → Fan-out an alle, Clients halten kein Gerät |
| go2rtc | nötig | **entfernt** |
**Warum 106% jetzt nicht mehr auftritt:** Pro Gerät hält der Schalter immer nur **einen**
FFmpeg. Übergang Live→HD und HD→Live wird über das `close`-Event synchronisiert — zwei
Encoder auf einem `/dev/videoN` sind konstruktiv ausgeschlossen.
**Verifiziert (lokal, ohne Kamera):** MJPEG-Parser (Content-Length-basiert, Chunk-robust,
`\r\n\r\n` im Body) per Unittest; HTTP-Routing (snapshot/stream/health, 404/503-Pfade);
Crash-Auto-Restart rate-limitiert. **Auf der Hardware noch zu verifizieren:** CPU-Last,
Latenz, HD-Blackout-Dauer, kein 106% nach Screenshot+Reconnect (Testplan in 05).
**FFmpeg (Default `ENCODE_MODE=copybsf`):** `-f v4l2 -input_format mjpeg -video_size
640x480 -framerate 30 -i … -c:v copy -bsf:v mjpeg2jpeg -f mpjpeg pipe:1` — Bitstream-Copy,
kein Transcode. Fallback `mjpeg` = Re-Encode mjpeg→mjpeg (~50%, wie go2rtc).
### ⚠ Regression am 2026-06-05 (eingebaut → korrigiert → optimiert)
1. **Fehler:** Erste Schalter-Fassung nutzte plain `-c:v copy` (ohne Bitstream-Filter)
**CPU 100%+ und hängendes Bild.** Das Kamera-MJPEG lässt JPEG-Tables weg, der
mpjpeg-Muxer verschluckt sich (`[mjpeg] unable to decode APP fields`) → keine validen
Frames. **Exakt der in 04/05 dokumentierte und von mir ignorierte Punkt.**
2. **Sofort-Fix:** `-c:v mjpeg` (Re-Encode, ~50%, wie go2rtc) — stabil, aber nicht besser
als der alte Stand.
3. **Host-Messung (User, 2026-06-05):** `-c:v copy -bsf:v mjpeg2jpeg` läuft 10 s sauber
durch (Copy bestätigt, nur einmalige Probe-Warnung). Der `mjpeg2jpeg`-Filter ergänzt
die fehlenden Tables ohne Decode → **kein Transcode, CPU < Re-Encode, valide JPEGs.**
→ als Default `copybsf` verdrahtet, `mjpeg` bleibt als Fallback (`ENCODE_MODE`).
Lehre erneut: **erst die Doku-Fakten anwenden, dann bauen** — und Optimierungen **messen**
(Punkt 3), nicht vorhersagen.
---
## Stream-Freeze (Einzelfall) — offen, niedrige Priorität
**Beobachtet 2026-06-05:** Ein Live-Stream ist **einmal** eingefroren (Bild stand still,
während die Last normal blieb). Vom User als akzeptabel eingestuft, später anschauen.
**Verdachtsmomente (zu prüfen, wenn es erneut auftritt):**
- Die multipart-Verbindung des `<img>` brach ab (z. B. ein fehlgeschlagener `res.write`
`cleanup()` meldet den Verbraucher ab), **ohne** dass der Browser von selbst neu
verbindet. Der `<img>`-`error`-Handler in `viewer.js` reconnectet nur bei einem echten
`error`-Event — ein „eingefrorenes" Bild ohne Fehler-Event fällt durch.
- Ein einzelnes korruptes JPEG aus dem Kamera-MJPEG (die `APP fields`-Warnung), das der
Browser nicht rendert und danach hängt.
**Möglicher Fix (wenn relevant):** clientseitiger Watchdog — Frames/Zeit zählen
(`img.onload`-Takt); kommt N Sekunden kein neues Bild, `img.src` neu setzen (Reconnect).
Server-seitig ist die Quelle stabil (CPU/Log unauffällig), daher zuerst clientseitig ansetzen.