Claude: Lag Arbeiten
This commit is contained in:
@@ -2,190 +2,199 @@
|
|||||||
|
|
||||||
## Symptom
|
## Symptom
|
||||||
|
|
||||||
Nach Umstieg auf WebRTC/H.264: Bild ruckelt, friert teils >1 s ein, manchmal
|
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.
|
bleibt ein Einzelbild ganz stehen. Im reinen MJPEG-Modus trat das **nicht** auf.
|
||||||
|
|
||||||
## Messung (2026-06-03)
|
---
|
||||||
|
|
||||||
|
## Diagnose-Verlauf
|
||||||
|
|
||||||
|
### Schritt 1 — CPU-Messung (erste Verdachtsphase)
|
||||||
|
|
||||||
| Quelle | CPU |
|
| Quelle | CPU |
|
||||||
|--------|-----|
|
|--------|-----|
|
||||||
| System gesamt | ~40 % |
|
| System gesamt | ~40 % |
|
||||||
| **Container AppRobotGo2RTC** | **~95 %** |
|
| AppRobotGo2RTC, 1 Client | **~35 %** |
|
||||||
|
| AppRobotGo2RTC, 2 Clients (Laptop + Handy) | **65–114 %** |
|
||||||
|
| AppRobotWebcam (Node.js) | **0 %** |
|
||||||
|
|
||||||
`docker stats` rechnet pro Kern: **95 % ≈ ein CPU-Kern voll ausgelastet.**
|
`docker stats` rechnet pro Kern: 114 % = mehr als ein Kern voll ausgelastet.
|
||||||
→ Flaschenhals ist **go2rtc (Encoding)**, nicht Netzwerk und nicht der Node-Server.
|
|
||||||
|
**Erkenntnis:** go2rtc re-encodiert nicht einmal pro Stream, sondern aufwändiger
|
||||||
|
pro Client-Verbindung (WebRTC-Session). Zwei Clients = fast doppelte CPU-Last.
|
||||||
|
|
||||||
|
### Schritt 2 — Browser-Client als Ursache ausgeschlossen
|
||||||
|
|
||||||
|
WebRTC `getStats()` lieferte über mehrere Minuten:
|
||||||
|
```
|
||||||
|
recv=30/s decoded=30/s dropped=0/s lost=+0 jitter=13–35ms
|
||||||
|
```
|
||||||
|
→ Server liefert alle Frames, Netz verliert nichts, Decoder schafft alles.
|
||||||
|
**Der Browser ist nicht das Problem.**
|
||||||
|
|
||||||
|
### Schritt 3 — Netz als Ursache ausgeschlossen
|
||||||
|
|
||||||
|
Zwei Browser-Fenster (Laptop + Handy) zeigen exakt dieselbe Verzögerung
|
||||||
|
für cam0 bzw. cam1 — synchron auf die Millisekunde. Ändert sich die Latenz
|
||||||
|
von cam0, ändert sie sich auf beiden Clients gleichzeitig.
|
||||||
|
|
||||||
|
→ **Das Problem sitzt in go2rtc/FFmpeg, nicht im Netz oder Browser.**
|
||||||
|
|
||||||
|
### Schritt 4 — Root Cause: FFmpeg-Flags und `exec timeout`
|
||||||
|
|
||||||
|
go2rtc generiert intern folgenden FFmpeg-Befehl:
|
||||||
|
```
|
||||||
|
-readrate_initial_burst 0.001 -re -i /dev/video2
|
||||||
|
-c:v libx264 -g 50 -profile:v high -preset:v superfast -tune:v zerolatency
|
||||||
|
```
|
||||||
|
|
||||||
|
Zwei Probleme identifiziert:
|
||||||
|
|
||||||
|
**Problem A — `-re` (Rate-Emulation für Live-Input):**
|
||||||
|
`-re` = „lies Input im Echtzeit-Takt". Für Datei-Wiedergabe gedacht.
|
||||||
|
Für eine Live-Kamera (die ohnehin Echtzeit-Frames liefert) puffert `-re`
|
||||||
|
Frames künstlich, statt sie sofort durchzureichen. Wenn der Encoder unter
|
||||||
|
Last minimal in Rückstand gerät, baut sich ein Puffer auf → variable Latenz.
|
||||||
|
`-readrate_initial_burst 0.001` macht den Start besonders langsam → erklärt
|
||||||
|
den langsamen Stream-Aufbau.
|
||||||
|
|
||||||
|
**Problem B — `-g 50` (Keyframe-Abstand 1,67 Sekunden bei 30 fps):**
|
||||||
|
H.264 überträgt zwischen Keyframes nur Differenzbilder. Der Browser kann erst
|
||||||
|
ab einem Keyframe decodieren. Nach jedem Paket-Verlust oder Neuverbindung
|
||||||
|
wartet der Browser bis zu 1,67 s auf den nächsten Keyframe → Standbild.
|
||||||
|
Da cam0 und cam1 ihre Keyframe-Takte unabhängig haben, friert mal der eine,
|
||||||
|
mal der andere ein — aber auf allen Clients gleichzeitig (wegen Schritt 3).
|
||||||
|
|
||||||
|
**Problem C — `ERR [exec] timeout` für /dev/video2 (cam1):**
|
||||||
|
go2rtc's FFmpeg für cam1 läuft gelegentlich in einen Timeout (Kamera-Init
|
||||||
|
zu langsam, USB-Bandbreitenproblem, Treiberproblem). go2rtc startet den
|
||||||
|
Encoder neu → cam1 friert für mehrere Sekunden ein, während cam0 läuft.
|
||||||
|
|
||||||
|
### Was bisher versucht wurde
|
||||||
|
|
||||||
|
| Massnahme | Ergebnis |
|
||||||
|
|-----------|----------|
|
||||||
|
| `#video=h264#video=mjpeg` entfernt → nur `#video=h264` | CPU-Last von ~95% auf ~35% reduziert |
|
||||||
|
| `getVideoPlaybackQuality()` als Überlast-Detektor | Fehlalarm (misst Render-Drops, nicht echte Überlast) |
|
||||||
|
| Umstieg auf `getStats()` (inbound-rtp) | Verlässlich, bestätigt: Client ist nicht das Problem |
|
||||||
|
| Aufwärmphase (15s nach `playing`) in Browser-Überwachung | Fehlalarme beim Stream-Aufbau beseitigt |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Ursachenanalyse
|
## Ursachen-Zusammenfassung
|
||||||
|
|
||||||
### Ursache 1 — Software-H.264-Encoding sättigt die CPU
|
| Ursache | Symptom | Behebbar ohne go2rtc-Patch? |
|
||||||
Die Kamera liefert **MJPEG** nativ. WebRTC im Browser braucht aber **H.264**.
|
|---------|---------|----------------------------|
|
||||||
go2rtc transcodiert also jeden Frame MJPEG→H.264 in Software (libx264).
|
| `-re` + `-readrate_initial_burst 0.001` | Variable Latenz, langsamer Aufbau | Ja (anderer Source-Typ) |
|
||||||
Zwei Kameras parallel → ein Kern voll. Wenn der Encoder nicht nachkommt,
|
| `-g 50` (1,67s GOP) | Bis zu 1,67s Standbild | Ja (exec: mit eigenem FFmpeg) |
|
||||||
stauen sich Frames → Ruckeln und Aussetzer.
|
| Software-H.264 × 2 Kameras × n Clients | CPU-Sättigung ab 2 Clients | Ja (Hardware-Encode) |
|
||||||
|
| cam1 FFmpeg timeout | Multi-Sekunden-Freeze cam1 | Teilweise (v4l2: Source) |
|
||||||
|
|
||||||
> Verstärkt wurde es vorher durch `#video=h264#video=mjpeg`: das ließ go2rtc
|
**go2rtc kann diese FFmpeg-Flags nicht per einfacher URL-Syntax konfiguriert werden.**
|
||||||
> **doppelt** encodieren (H.264 *und* MJPEG). Das `#video=mjpeg` ist inzwischen
|
Sie sind hard-coded im `ffmpeg:` Source-Handler von go2rtc 1.9.x.
|
||||||
> in der Config entfernt — der Hauptkostenfaktor (H.264-Software-Encode) bleibt.
|
|
||||||
|
|
||||||
### Ursache 2 — Großes GOP (Keyframe-Abstand `-g 50`)
|
|
||||||
go2rtc setzt standardmäßig ein Keyframe alle 50 Frames = **1,67 s bei 30 fps**.
|
|
||||||
H.264 überträgt zwischen Keyframes nur Differenzbilder. Geht ein Paket verloren
|
|
||||||
oder verbindet sich der Client neu, **wartet der Browser bis zum nächsten Keyframe**
|
|
||||||
— bis zu 1,67 s Standbild. Das erklärt exakt das „ein Bild bleibt ganz stehen".
|
|
||||||
|
|
||||||
### Der Grundkonflikt
|
|
||||||
- **MJPEG**: kein Encode (Kamera-nativ), kein GOP → flüssig, ~200 ms, höhere Bandbreite
|
|
||||||
- **H.264/WebRTC**: ~130 ms, geringe Bandbreite → aber Encode-Last + GOP-Freezes
|
|
||||||
|
|
||||||
Wir zahlen also CPU-Last und Komplexität für ~70 ms Latenzgewinn. Ob sich das
|
|
||||||
lohnt, hängt davon ab, ob wir die CPU-Last loswerden (Hardware-Encode / native H.264).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Lösungsweg — geordnet nach Aufwand/Wirkung
|
## Lösungsweg — geordnet nach Aufwand/Wirkung
|
||||||
|
|
||||||
### Schritt 1 (5 min) — Prüfen: kann die Kamera H.264 nativ?
|
### Option A — `v4l2:` Source statt `ffmpeg:` (sofort probieren)
|
||||||
```bash
|
go2rtc hat einen nativen v4l2-Treiber, der FFmpeg für den Capture umgeht:
|
||||||
docker exec AppRobotGo2RTC v4l2-ctl --list-formats-ext -d /dev/video0
|
```yaml
|
||||||
# v4l2-ctl fehlt im go2rtc-Image? → auf dem Host ausführen:
|
streams:
|
||||||
v4l2-ctl --list-formats-ext -d /dev/video0
|
cam0: "v4l2:/dev/video0#video=h264"
|
||||||
|
cam1: "v4l2:/dev/video2#video=h264"
|
||||||
```
|
```
|
||||||
- **Steht „H264" in der Liste** → go2rtc kann den Stream **durchreichen** (passthrough),
|
- Kein `-re`, kein `-readrate_initial_burst` → direkter Frame-Durchsatz
|
||||||
praktisch NULL Encode-Last und niedrigste Latenz. Bestfall.
|
- Encoding (libx264) bleibt, aber ohne künstliches Puffern
|
||||||
- Steht nur MJPEG/YUYV → weiter mit Schritt 2.
|
- Könnte den `exec timeout` auf cam1 beheben (anderer Kamera-Öffnungspfad)
|
||||||
|
- **Risiko:** v4l2-Source in go2rtc ist weniger getestet als ffmpeg-Source
|
||||||
### Schritt 2 (Hauptfix) — Hardware-Encoding (Intel QuickSync / VAAPI)
|
|
||||||
Ein ThinkCentre hat fast sicher eine Intel-iGPU mit QuickSync. Damit wandert das
|
|
||||||
H.264-Encoding von der CPU auf die GPU → **CPU von ~95 % auf ~10 %**.
|
|
||||||
|
|
||||||
|
### Option B — Hardware-Encoding Intel QuickSync / VAAPI
|
||||||
Prüfen ob GPU verfügbar:
|
Prüfen ob GPU verfügbar:
|
||||||
```bash
|
```bash
|
||||||
ls -l /dev/dri # renderD128 vorhanden?
|
ls -l /dev/dri # renderD128 vorhanden?
|
||||||
```
|
```
|
||||||
Umsetzung (später):
|
Config:
|
||||||
```yaml
|
```yaml
|
||||||
# beim go2rtc-Service:
|
# go2rtc-Service: devices: + /dev/dri:/dev/dri
|
||||||
devices:
|
|
||||||
- /dev/video0:/dev/video0
|
|
||||||
- /dev/video2:/dev/video2
|
|
||||||
- /dev/dri:/dev/dri # ← GPU durchreichen
|
|
||||||
# in der go2rtc-Config:
|
|
||||||
streams:
|
streams:
|
||||||
cam0: "ffmpeg:/dev/video0#video=h264#hardware"
|
cam0: "ffmpeg:/dev/video0#video=h264#hardware"
|
||||||
cam1: "ffmpeg:/dev/video2#video=h264#hardware"
|
cam1: "ffmpeg:/dev/video2#video=h264#hardware"
|
||||||
```
|
```
|
||||||
|
- Encoding auf GPU → CPU von ~35 % auf ~5 %
|
||||||
|
- go2rtc erzeugt anderen FFmpeg-Befehl (h264_vaapi statt libx264)
|
||||||
|
- Ob `-re` dabei ebenfalls wegfällt: **muss am Gerät verifiziert werden**
|
||||||
|
|
||||||
### Schritt 3 — GOP verkürzen (gegen Freeze nach Loss/Reconnect)
|
### Option C — Eigener FFmpeg-Befehl via exec: Source
|
||||||
Standard-Format erlaubt kein `-g`. Dafür `exec:`-Source mit eigenem FFmpeg-Befehl:
|
Vollständige Kontrolle über alle FFmpeg-Flags:
|
||||||
```yaml
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
cam0:
|
cam0:
|
||||||
- "exec:ffmpeg -hide_banner -f v4l2 -input_format mjpeg -video_size 640x480
|
- "ffmpeg:-f v4l2 -input_format mjpeg -video_size 640x480 -framerate 30
|
||||||
-framerate 30 -i /dev/video0
|
-fflags nobuffer -flags low_delay
|
||||||
-c:v h264_vaapi -g 15 -bf 0 -tune zerolatency
|
-i /dev/video0
|
||||||
-f rtsp {output}"
|
-c:v libx264 -preset ultrafast -tune zerolatency -g 15 -bf 0
|
||||||
|
#video=h264"
|
||||||
```
|
```
|
||||||
`-g 15` = Keyframe alle 0,5 s → Freeze nach Störung max 0,5 s statt 1,67 s.
|
- Kein `-re` (nicht angegeben)
|
||||||
`-bf 0` = keine B-Frames (kein Lookahead-Delay).
|
- `-g 15` = Keyframe alle 0,5 s → max 0,5 s Freeze
|
||||||
|
- `-fflags nobuffer -flags low_delay` = minimaler Input-Buffer
|
||||||
|
- **Problem:** go2rtc's `ffmpeg:` Source-Handler mit Custom-Args ist Version-abhängig;
|
||||||
|
korrekte Syntax muss verifiziert werden
|
||||||
|
|
||||||
### Schritt 4 — Stellschrauben (zusätzliche Reserve)
|
### Option D — Separater MediaMTX-Container
|
||||||
- Auflösung 640×480 → **320×240** (viertelt die Encode-Pixel)
|
MediaMTX (rtsp-simple-server) als Zwischenstufe:
|
||||||
- Framerate 30 → **15 fps** (halbiert die Encode-Frequenz)
|
```
|
||||||
|
v4l2 → FFmpeg (eigene Flags, g=15, kein -re) → RTSP (MediaMTX) → go2rtc → WebRTC
|
||||||
|
```
|
||||||
|
- Volle FFmpeg-Kontrolle
|
||||||
|
- go2rtc liest einfach `rtsp://mediamtx:8554/cam0`
|
||||||
|
- Zusätzlicher Container, aber sauber und wartbar
|
||||||
|
|
||||||
### Schritt 5 (Fallback) — zurück zu MJPEG
|
### Option E — Fallback: MJPEG
|
||||||
Falls Hardware-Encode nicht verfügbar ist oder zickt:
|
```yaml
|
||||||
- **kein Encode, kein GOP → keine Freezes**, stabil flüssig
|
streams:
|
||||||
- ~200 ms Latenz (statt 130 ms), höhere Bandbreite — bei 1–3 LAN-Usern egal
|
cam0: "ffmpeg:/dev/video0#video=mjpeg"
|
||||||
- go2rtc liefert MJPEG direkt; Viewer: `MODE = 'mjpeg'` oder simples `<img>`
|
```
|
||||||
|
- Kein H.264-Encode, kein GOP, keine `-re`-Problematik
|
||||||
|
- ~200 ms Latenz (statt 130 ms) — bei 1–3 Usern und Roboter-Überwachung ausreichend
|
||||||
|
- War nachweislich stabil und flüssig
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Entscheidungsbaum
|
## Empfohlene Reihenfolge
|
||||||
|
|
||||||
```
|
```
|
||||||
Kamera kann H.264 nativ? ──ja──► Passthrough (Schritt 1) ✓ fertig
|
1. Option A (v4l2: Source) → 5 min, kein Aufwand, könnte alles lösen
|
||||||
│ nein
|
2. Option B (Hardware-Encode) → 15 min, braucht /dev/dri-Check
|
||||||
▼
|
3. Option C (custom FFmpeg) → 30 min, volle Kontrolle
|
||||||
/dev/dri vorhanden? ──ja──► Hardware-Encode (Schritt 2) ✓ Hauptfix
|
4. Option D (MediaMTX) → 60 min, sauberste Architektur
|
||||||
│ nein + GOP kürzen (Schritt 3)
|
5. Option E (MJPEG) → 5 min, sicherer Hafen
|
||||||
▼
|
|
||||||
Latenz 200ms akzeptabel? ──ja──► MJPEG-Fallback (Schritt 5) ✓ robust
|
|
||||||
│ nein
|
|
||||||
▼
|
|
||||||
Auflösung/fps senken (Schritt 4), notfalls 1 Kamera
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Empfehlung
|
---
|
||||||
|
|
||||||
Reihenfolge **1 → 2 → 3**:
|
|
||||||
1. Erst native H.264 prüfen (kostet 5 min, evtl. löst es alles).
|
|
||||||
2. Sonst Hardware-Encoding aktivieren — das ist der eigentliche Hebel gegen die 95 %.
|
|
||||||
3. Dann GOP kürzen, damit auch die Restfreezes verschwinden.
|
|
||||||
|
|
||||||
**MJPEG (Schritt 5) ist der sichere Hafen**, falls die GPU nicht mitspielt:
|
|
||||||
es war nachweislich flüssig, nur 70 ms langsamer. Für diesen Anwendungsfall
|
|
||||||
(Roboter-Überwachung, 1–3 User) völlig ausreichend.
|
|
||||||
|
|
||||||
## Hi-Res-Snapshots — Analyse (Live-Video + Foto alle ~10 s)
|
## Hi-Res-Snapshots — Analyse (Live-Video + Foto alle ~10 s)
|
||||||
|
|
||||||
Ziel: schnelles Live-Video **und** gelegentlich (≈ alle 10 s) ein hochauflösendes Foto.
|
**Grundprinzip:** Snapshot-Auflösung = Stream-Auflösung (USB-Kamera kann nur
|
||||||
|
in einer Auflösung gleichzeitig geöffnet sein). Für Hi-Res-Fotos muss der
|
||||||
|
Stream selbst hochauflösend laufen, Browser skaliert fürs Display herunter.
|
||||||
|
|
||||||
### Die entscheidende Einschränkung
|
### Weg A — MJPEG hochauflösend (Passthrough, Option E oben)
|
||||||
Eine USB-Kamera kann **gleichzeitig nur in einer Auflösung** geöffnet werden.
|
- Kamera liefert MJPEG nativ → go2rtc reicht 1:1 durch, kein Encode
|
||||||
Solange go2rtc das Device für den Live-Stream hält, kann kein zweiter Prozess
|
- Snapshot: `/api/frame.jpeg` = voller Frame, native JPEG-Qualität, gratis
|
||||||
parallel ein höher aufgelöstes Foto ziehen (Device belegt).
|
- CPU ~5 %, keine Freezes
|
||||||
|
- Empfohlen wenn Hardware-Encoding nicht verfügbar
|
||||||
|
|
||||||
→ **Snapshot-Auflösung = Stream-Auflösung.** Es gibt keinen billigen Nebenweg zu
|
### Weg B — H.264 Hardware-Encode + MJPEG-Passthrough (Option B oben)
|
||||||
einem höher aufgelösten Foto, solange der Stream klein läuft. `/api/frame.jpeg`
|
|
||||||
decodiert immer einen Frame **aus dem laufenden Stream**.
|
|
||||||
|
|
||||||
Konsequenz: Für Hi-Res-Fotos muss der **Stream selbst hochauflösend** laufen und
|
|
||||||
fürs Live-Bild im Browser heruntergerechnet werden. Der Trick ist, das billig zu halten.
|
|
||||||
|
|
||||||
### Weg A — MJPEG hochauflösend (Passthrough)
|
|
||||||
- Quelle: Kamera hochauflösend MJPEG → go2rtc reicht 1:1 durch, **kein Encode**
|
|
||||||
- Snapshot: `/api/frame.jpeg` = voller Frame, **native JPEG-Qualität**, gratis
|
|
||||||
- Live: MJPEG, im Browser auf 480 skaliert (~200 ms, war flüssig)
|
|
||||||
- CPU ~5 %, keine Freezes. Preis: höhere LAN-Bandbreite (unkritisch bei 1–3 Usern)
|
|
||||||
|
|
||||||
### Weg B — WebRTC + Hardware-Encoding ◄ favorisiert, mit Bedingung
|
|
||||||
- Quelle: Kamera hochauflösend; Live-Track H.264 **per Intel-GPU (QuickSync)**
|
|
||||||
- Live: WebRTC ~130 ms, CPU ~10 %
|
|
||||||
|
|
||||||
**Bedingung des Users: der Frame aus dem Stream MUSS hochauflösend sein.**
|
|
||||||
Antwort: **ja, per Definition** — `/api/frame.jpeg` hat dieselbe Auflösung wie der
|
|
||||||
Stream. Läuft H.264 in 1280×960, ist das Foto 1280×960. Garantiert durch die Config
|
|
||||||
(Stream-Auflösung explizit hochauflösend setzen → WebRTC überträgt hochauflösend,
|
|
||||||
Browser skaliert fürs Display herunter).
|
|
||||||
|
|
||||||
**Qualitäts-Nuance:** Ein aus H.264 decodierter Frame ist leicht verlustbehaftet
|
|
||||||
(H.264 → JPEG). Für ArUco meist ausreichend, aber nicht optimal.
|
|
||||||
|
|
||||||
**Beste Variante (Hi-Res UND native Qualität)** — erst durch HW-Encode praktikabel:
|
|
||||||
```yaml
|
```yaml
|
||||||
# Quelle hochauflösend; H.264 (GPU) für Live + MJPEG-Passthrough für Snapshot
|
|
||||||
cam0: "ffmpeg:/dev/video0#video=h264#hardware#video=mjpeg"
|
cam0: "ffmpeg:/dev/video0#video=h264#hardware#video=mjpeg"
|
||||||
```
|
```
|
||||||
- Live-Track: H.264 per GPU (billig)
|
- Live: H.264 per GPU (~130 ms, niedrige Bandbreite)
|
||||||
- Snapshot-Track: MJPEG-Passthrough (gratis, kamera-nativ)
|
- Snapshot: MJPEG-Passthrough (native Qualität, gratis)
|
||||||
- `/api/frame.jpeg` sollte den **MJPEG-Track** nehmen → volle Auflösung, native Qualität
|
- Zu verifizieren: welchen Track nimmt `/api/frame.jpeg` — H.264 oder MJPEG?
|
||||||
- Das ist `#video=h264#video=mjpeg` wie früher — aber OHNE Flaschenhals, weil nur
|
|
||||||
H.264 die GPU nutzt und MJPEG reines Durchreichen ist.
|
|
||||||
|
|
||||||
### Vor Weg B zu verifizieren („sichergestellt" erst danach)
|
### Snapshot-Takt
|
||||||
1. `ls -l /dev/dri` → ist `renderD128` vorhanden? (Intel-GPU verfügbar)
|
Der 10-s-Takt erzeugt keine Dauerlast: pro Foto wird ein Frame aus dem
|
||||||
2. Hardware-Encode testweise aktivieren (`#hardware`) → fällt CPU wirklich von 95 %?
|
laufenden Stream abgegriffen. Trigger: Homing-Projekt ruft
|
||||||
3. `/api/frame.jpeg?src=cam0` abrufen → **Auflösung prüfen** (hoch?) **und Qualität**
|
`GET /api/snapshot/cam0` alle 10 s ab (aktuell so implementiert).
|
||||||
4. Klären, welchen Track `/api/frame.jpeg` bei `#video=h264#hardware#video=mjpeg`
|
|
||||||
tatsächlich verwendet (MJPEG-Passthrough = native Qualität gewünscht)
|
|
||||||
|
|
||||||
> Diese 4 Checks können nicht aus der Ferne garantiert werden — sie müssen am
|
|
||||||
> ThinkCentre laufen. Erst danach ist Weg B „sichergestellt".
|
|
||||||
|
|
||||||
### Snapshot-Takt (alle ~10 s)
|
|
||||||
Der 10-s-Takt erzeugt **keine** Dauerlast: pro Foto wird nur ein Frame aus dem
|
|
||||||
ohnehin laufenden Stream abgegriffen. Trigger wahlweise:
|
|
||||||
- Pull: Homing-Projekt ruft `/api/snapshot/cam0` alle 10 s ab (aktuell so vorgesehen)
|
|
||||||
- Push: kleiner Timer im Node-Server, der das Foto ablegt / per Webhook sendet (Phase 5)
|
|
||||||
|
|||||||
90
server.js
90
server.js
@@ -6,15 +6,14 @@ const path = require('path');
|
|||||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||||
const { createSnapshotRouter } = require('./src/snapshotService');
|
const { createSnapshotRouter } = require('./src/snapshotService');
|
||||||
|
|
||||||
const PORT = parseInt(process.env.PORT ?? '8444', 10);
|
const PORT = parseInt(process.env.PORT ?? '8444', 10);
|
||||||
const GO2RTC_URL = process.env.GO2RTC_URL ?? 'http://localhost:1984';
|
const GO2RTC_URL = process.env.GO2RTC_URL ?? 'http://localhost:1984';
|
||||||
const GO2RTC_PORT = parseInt(process.env.GO2RTC_PORT ?? '1984', 10);
|
const GO2RTC_PORT = parseInt(process.env.GO2RTC_PORT ?? '1984', 10);
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// ── 1. Eigene Endpunkte (vor dem Proxy registrieren) ─────────────────────────
|
// ── 1. Eigene Endpunkte (vor dem Proxy registrieren) ─────────────────────────
|
||||||
|
|
||||||
// Stabile Snapshot-API für das Homing-Projekt
|
|
||||||
app.use('/api/snapshot', createSnapshotRouter(GO2RTC_URL));
|
app.use('/api/snapshot', createSnapshotRouter(GO2RTC_URL));
|
||||||
|
|
||||||
app.get('/health', async (_req, res) => {
|
app.get('/health', async (_req, res) => {
|
||||||
@@ -27,21 +26,18 @@ app.get('/health', async (_req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gibt dem Viewer die go2rtc-Port-Nummer mit – Browser baut WS direkt zu go2rtc.
|
|
||||||
// Trennung HTTP (Node) / WebSocket (go2rtc) ist sauberer als ein fragiler WS-Proxy.
|
|
||||||
app.get('/config.json', (_req, res) => {
|
app.get('/config.json', (_req, res) => {
|
||||||
res.json({ go2rtcPort: GO2RTC_PORT });
|
res.json({ go2rtcPort: GO2RTC_PORT });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 2. HTTP-Proxy zu go2rtc (nur für Script-Dateien und API ohne WS) ─────────
|
// ── 2. HTTP-Proxy zu go2rtc ───────────────────────────────────────────────────
|
||||||
// /api/ws NICHT hier proxy-en – das macht der Browser direkt (s. viewer.js).
|
|
||||||
const go2rtcProxy = createProxyMiddleware({
|
const go2rtcProxy = createProxyMiddleware({
|
||||||
target: GO2RTC_URL,
|
target: GO2RTC_URL,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
pathFilter: ['/api', '/video-rtc.js', '/video-stream.js'],
|
pathFilter: ['/api', '/video-rtc.js', '/video-stream.js'],
|
||||||
logger: console,
|
logger: console,
|
||||||
on: {
|
on: {
|
||||||
error: (err, req, res) => {
|
error: (err, _req, res) => {
|
||||||
console.error('[HPM] proxy error:', err.message);
|
console.error('[HPM] proxy error:', err.message);
|
||||||
if (!res.headersSent) res.status(502).json({ error: 'go2rtc nicht erreichbar' });
|
if (!res.headersSent) res.status(502).json({ error: 'go2rtc nicht erreichbar' });
|
||||||
},
|
},
|
||||||
@@ -49,9 +45,70 @@ const go2rtcProxy = createProxyMiddleware({
|
|||||||
});
|
});
|
||||||
app.use(go2rtcProxy);
|
app.use(go2rtcProxy);
|
||||||
|
|
||||||
// ── 3. Statische Dateien (eigener Viewer) ────────────────────────────────────
|
// ── 3. Statische Dateien ──────────────────────────────────────────────────────
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
// ── 4. go2rtc Stream-Monitor (server-seitiges Logging) ───────────────────────
|
||||||
|
// Pollt alle 5 s go2rtc /api/streams und loggt Änderungen.
|
||||||
|
// Sichtbar im Portainer-Log von AppRobotWebcam.
|
||||||
|
// Logt: Producer-Starts/-Stops, Consumer-Anzahl, Timeouts/Restarts.
|
||||||
|
//
|
||||||
|
// go2rtc /api/streams liefert z.B.:
|
||||||
|
// { "cam0": { "producers": [{"url":"...","state":"running"}], "consumers": [...] } }
|
||||||
|
//
|
||||||
|
const STREAM_POLL_MS = 5000;
|
||||||
|
let prevStreamState = {};
|
||||||
|
|
||||||
|
async function pollGo2rtcStreams() {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${GO2RTC_URL}/api/streams`);
|
||||||
|
if (!r.ok) { console.warn(`[monitor] /api/streams → HTTP ${r.status}`); return; }
|
||||||
|
const streams = await r.json();
|
||||||
|
|
||||||
|
for (const [name, data] of Object.entries(streams)) {
|
||||||
|
const producers = data.producers ?? [];
|
||||||
|
const consumers = data.consumers ?? [];
|
||||||
|
const nConsumers = consumers.length;
|
||||||
|
const prev = prevStreamState[name] ?? {};
|
||||||
|
|
||||||
|
// Producer-Status
|
||||||
|
for (let i = 0; i < producers.length; i++) {
|
||||||
|
const p = producers[i];
|
||||||
|
const state = p.state ?? 'unknown';
|
||||||
|
const key = `${name}.p${i}`;
|
||||||
|
const pPrev = prevStreamState[key];
|
||||||
|
|
||||||
|
if (pPrev !== state) {
|
||||||
|
if (state === 'running') console.log(`[monitor][${name}] producer #${i} LÄUFT (${p.url ?? ''})`);
|
||||||
|
if (state === 'error') console.error(`[monitor][${name}] producer #${i} FEHLER (${p.url ?? ''})`);
|
||||||
|
if (state === 'stop') console.warn(`[monitor][${name}] producer #${i} GESTOPPT`);
|
||||||
|
if (!['running','error','stop'].includes(state))
|
||||||
|
console.log(`[monitor][${name}] producer #${i} state="${state}"`);
|
||||||
|
prevStreamState[key] = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consumer-Anzahl — nur loggen wenn sie sich ändert
|
||||||
|
if (prev.nConsumers !== nConsumers) {
|
||||||
|
console.log(`[monitor][${name}] consumers: ${prev.nConsumers ?? '?'} → ${nConsumers}`);
|
||||||
|
prevStreamState[name] = { ...prev, nConsumers };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Streams die verschwunden sind (Timeout/Restart)
|
||||||
|
for (const name of Object.keys(prevStreamState)) {
|
||||||
|
if (name.includes('.')) continue; // skip producer-state keys
|
||||||
|
if (!streams[name]) {
|
||||||
|
console.warn(`[monitor][${name}] Stream verschwunden aus go2rtc`);
|
||||||
|
delete prevStreamState[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[monitor] go2rtc nicht erreichbar:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
@@ -61,6 +118,13 @@ server.listen(PORT, '0.0.0.0', () => {
|
|||||||
console.log(` go2rtc WS: ws://[host]:${GO2RTC_PORT} (Browser verbindet direkt)`);
|
console.log(` go2rtc WS: ws://[host]:${GO2RTC_PORT} (Browser verbindet direkt)`);
|
||||||
console.log(` Viewer: http://0.0.0.0:${PORT}/`);
|
console.log(` Viewer: http://0.0.0.0:${PORT}/`);
|
||||||
console.log(` Snapshot API: http://0.0.0.0:${PORT}/api/snapshot/cam0`);
|
console.log(` Snapshot API: http://0.0.0.0:${PORT}/api/snapshot/cam0`);
|
||||||
|
console.log(` Stream-Monitor: alle ${STREAM_POLL_MS / 1000}s → Portainer-Log`);
|
||||||
|
|
||||||
|
// Ersten Poll nach 3 s (go2rtc braucht einen Moment zum Starten)
|
||||||
|
setTimeout(() => {
|
||||||
|
pollGo2rtcStreams();
|
||||||
|
setInterval(pollGo2rtcStreams, STREAM_POLL_MS);
|
||||||
|
}, 3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
const shutdown = (sig) => {
|
const shutdown = (sig) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user