diff --git a/doc/04_Delay_roadmap.md b/doc/04_Delay_roadmap.md index 7b8078a..a7ed89a 100644 --- a/doc/04_Delay_roadmap.md +++ b/doc/04_Delay_roadmap.md @@ -1,3 +1,10 @@ +> # ℹ️ Architektur abgelöst (2026-06-05): go2rtc → Node-MJPEG-Schalter +> Dieses Dokument beschreibt den **go2rtc**-Aufbau (historisch wertvoll: Messungen, +> Fehler-Log, eiserne Regeln gelten weiter sinngemäß). Der Live-Stream läuft seit +> 2026-06-05 **nicht mehr über go2rtc**, sondern über einen Node-eigenen MJPEG-Schalter +> (`src/cameraSwitch.js`). Grund: das 106%-Race beim HD-Snapshot. Maßgeblich: +> `05_screenshot_roadmap.md` (Abschnitt „Node-MJPEG-Schalter") und `09_Bug_reports.md`. + # AppRobotWebcam – Delay / Ruckler-Analyse ## Symptom diff --git a/doc/05_screenShot_roadmap.md b/doc/05_screenShot_roadmap.md index 6202090..a7298a4 100644 --- a/doc/05_screenShot_roadmap.md +++ b/doc/05_screenShot_roadmap.md @@ -1,4 +1,27 @@ -# AppRobotWebcam – Hi-Res-Snapshot via Consumer-Umhängen +> # ⛔ ABGELÖST (2026-06-05) — dieser Ansatz war die Ursache des 106%-Bugs +> +> Der unten beschriebene **Consumer-Umhängen-Ansatz mit go2rtc** (`cam0` loslassen → +> go2rtc gibt Gerät frei → `cam0_hires` greifen) hat sich als **prinzipiell racy** +> erwiesen: go2rtcs API kann nicht zuverlässig melden, wann FFmpeg `/dev/videoN` +> freigibt → zwei Encoder auf einem Gerät → **106% CPU + Freeze** (siehe `09_Bug_reports.md`). +> +> **Aktuelle, maßgebliche Architektur:** **Node-MJPEG-Schalter, go2rtc entfernt.** +> Node besitzt die Kameras selbst; das `close`-Event des eigenen FFmpeg ist der harte +> Beweis „Gerät frei". Das Race ist damit konstruktiv ausgeschlossen. +> +> | | alt (unten, abgelöst) | **neu (maßgeblich)** | +> |-|----------------------|----------------------| +> | Geräte-Öffner | go2rtc | **Node** `src/cameraSwitch.js` | +> | Live | go2rtc-WS + `video-stream.js` | MJPEG multipart → `` | +> | HD-Grab | 2. go2rtc-Stream `cam_hires` (Race) | Schalter: Live stoppen (`close`=FD frei) → 1280 → zurück | +> | Multi-User | brach | gelöst (ein FFmpeg → Fan-out) | +> +> **→ Neue Architektur + Hardware-Testplan stehen weiter unten in diesem Dokument +> (Abschnitt „## Node-MJPEG-Schalter").** Alles ab hier bis dorthin ist **Historie**. + +--- + +# AppRobotWebcam – Hi-Res-Snapshot via Consumer-Umhängen ⛔ (historisch) > Status: **Phase 2 implementiert und funktional** (2026-06-04): > HD-Grab liefert echten 1280×960-Frame (76071 bytes bestätigt). Bekanntes Problem @@ -334,3 +357,87 @@ und zur Laufzeit wird go2rtc nur **gelesen**, nie verändert. **Reihenfolge:** Phase 1 (messen, ~null Risiko) → Pausen aus der Messung setzen → Phase 2 (Grab). Fällt Phase 1, bleibt Weg A (separate Kamera) aus `04_*` der sichere Fallback. + +--- +--- + +# ✅ Node-MJPEG-Schalter (2026-06-05) — maßgebliche Architektur + +> Ersetzt den gesamten go2rtc-Ansatz oben. Bei Widerspruch gilt dieser Abschnitt. + +## Kernidee: Node besitzt die Kamera selbst + +Das 106%-Race entstand, weil **zwei** FFmpeg (Live 640 + HD 1280) gleichzeitig auf +**demselben** `/dev/videoN` liefen, und go2rtcs API nicht zuverlässig melden konnte, wann +ein FFmpeg das Gerät freigibt. **Lösung:** Node startet die FFmpeg-Prozesse selbst → das +`close`-Event des Kindprozesses ist der harte Beweis „Prozess weg ⇒ Kernel-FD geschlossen +⇒ Gerät frei". Race konstruktiv ausgeschlossen, nicht über Timing entschärft. + +``` +go2rtc ── ENTFERNT +Node (server.js) + ├─ CameraSwitch cam0 ── besitzt /dev/video0 ── EIN FFmpeg (Live ODER HD) + ├─ CameraSwitch cam1 ── besitzt /dev/video2 ── EIN FFmpeg (Live ODER HD) + ├─ /api/stream/ ── MJPEG multipart/x-mixed-replace → Browser + └─ /api/snapshot/ ── 640 aus RAM · //hires → HD-Grab über den Schalter +``` + +## Der Schalter (`src/cameraSwitch.js`) + +Eine `CameraSwitch`-Instanz pro Gerät — der **einzige** Öffner von `/dev/videoN`. Hält +immer nur **einen** FFmpeg. Zustände `stopped | live | grabbing`, Mutex pro Kamera. + +- **Live (Dauerbetrieb):** `ffmpeg -f v4l2 -input_format mjpeg -video_size 640x480 + -framerate 30 -i /dev/videoN -c:v copy -f mpjpeg pipe:1` → kein Re-Encode. Node parst + die mpjpeg-Frames (Content-Length-basiert), hält den letzten (für `/api/snapshot`), + sendet sie an alle Stream-Clients. Crash → Auto-Restart nach 1,5 s. +- **HD-Grab (`grabHires`):** Live-FFmpeg `SIGTERM` → **auf `close` warten** (FD frei) → + 1280-FFmpeg, warmlaufen (ab Frame ≥6, ≥15 KB, Breite ≥1000), besten Frame greifen → + beenden, auf `close` warten → `finally`: **immer** Live zurück (Live hat Priorität). +- **Blackout:** `` friert ~1–3 s ein, läuft dann weiter. **Kein Client-Handling + nötig** (das war früher die Fehlerquelle). + +## Auslieferung / Multi-User + +`/api/stream/` = `multipart/x-mixed-replace`; ein FFmpeg → Fan-out an N Clients. +Backpressure: voller Socket-Puffer (>1 MB) eines langsamen Clients → Frames für ihn +droppen, andere bleiben flüssig. Clients halten **kein** Gerät → **Multi-User gelöst.** + +## Konfiguration (`docker-compose.yaml`) + +Ein Node-Container mit FFmpeg, Geräte durchgereicht, `group_add: video`. Env-Overrides: +`DEV0/DEV1`, `LIVE_SIZE/LIVE_FPS`, `HIRES_SIZE/HIRES_FPS`. Firewall: nur noch **TCP 8444**. + +## Verifiziert vs. offen + +- **Lokal verifiziert (ohne Kamera):** MJPEG-Parser (Unittest, Chunk-robust, `\r\n\r\n` + im Body), HTTP-Routing (snapshot/stream/health, 404/503), Crash-Auto-Restart rate-limitiert. +- **FFmpeg-Args = die der bisher funktionierenden go2rtc-Quelle** (`-f v4l2 -input_format + mjpeg -video_size … -framerate …`), nur Ausgabe `-c:v copy -f mpjpeg`. +- **Auf der Hardware noch zu verifizieren:** CPU-Last, Latenz, HD-Blackout-Dauer, und der + Bug-Reproweg unten. + +## Hardware-Testplan + +1. Code syncen, Stack neu deployen (Image baut FFmpeg ein — erster Build dauert länger). +2. Viewer öffnen → beide Kameras Live (`MJPEG · live`). **CPU messen** (Erwartung < 50 %). +3. **Bug-Reproweg:** Anmelden → „HD" → Download → Stream nach kurzem Freeze weiter → + **neu anmelden / Tab neu laden.** Erwartung: **keine 106%, kein Dauer-Freeze.** +4. Zwei Browser gleichzeitig → „HD" während beide verbunden → **kein 503** (Multi-User). +5. HD-Bild: 1280×960, nicht schwarz. Blackout-Dauer notieren. +6. Eine Kamera abziehen → Log rate-limitierter Restart, andere Kamera + Node unberührt. + +`docker logs AppRobotWebcam` zeigt jeden Zustandswechsel des Schalters. + +## Rollback + +`git checkout -- docker-compose.yaml server.js package.json public/ src/` +(der go2rtc-Stand liegt vollständig in der Git-Historie). + +## Mögliche Folgeschritte + +- **Hi-Res nativ?** `v4l2-ctl --list-formats-ext -d /dev/video0` — liefert die Kamera + 1280×960 als **MJPEG**? Falls nur YUYV → `-c:v copy` scheitert → andere native Auflösung + oder bewusst Re-Encode (teurer). +- **On-Demand Live** (FFmpeg erst bei erstem Client) wäre stromsparender, ist aber bewusst + weggelassen — Dauerbetrieb hält die Übergabe-Logik simpel (weniger Race-Fläche). diff --git a/doc/09_Bug_reports.md b/doc/09_Bug_reports.md index b1dcbc6..78dc44a 100644 --- a/doc/09_Bug_reports.md +++ b/doc/09_Bug_reports.md @@ -80,52 +80,32 @@ Der **Hinweg** (Schritt 1: warten bis `cam` frei, bevor `cam_hires` startet) fun — er wartet echt (4870ms / 5069ms im Log). Kaputt ist der **Rückweg** (`cam_hires` → `cam`): dort wird nicht zuverlässig gewartet. -### Lösungsvorschläge (geordnet nach Robustheit) +### ✅ GELÖST (2026-06-05) — Node-MJPEG-Schalter, go2rtc entfernt -**A — Separate Hi-Res-Kamera (Weg A aus 04). GARANTIERT.** -Zusätzliche USB-Kamera, die go2rtc nicht öffnet; Node grabbt sie on-demand per one-shot -FFmpeg. Anderes Device → kein Race möglich. Kostet Hardware. Einzige 100%-Lösung. +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`. -**B — Feature streichen, zurück auf KONSOLIDIERT (04). GARANTIERT.** -`cam0_hires`/`cam1_hires` aus docker-compose, `/hires` aus snapshotService, `HD`-Button -aus dem Viewer. Nur noch 640er-Snapshot (read-only `frame.jpeg`). Stabil, kein Hi-Res. +**Was sich änderte:** -**C — No-Hardware-Versuch: Rückweg robust machen. MITTLERE Sicherheit, MUSS gemessen werden.** -Statt auf `state` zu pollen: warten bis go2rtc das **Producer-Objekt entfernt hat** -(`producers`-Array leer für `cam_hires`) + großzügiger Settle. Vorher zwingend -**empirisch messen** (rein lesend, erlaubt): bei abgeschaltetem Live-Stream einmal -`frame.jpeg?src=cam0_hires` holen, dann `GET /api/streams` alle 100ms für ~10s loggen — -zeigt, ob/wann der Producer wirklich verschwindet. Erst wenn die Daten das belegen, -ausliefern. Bleibt prinzipiell ein Race auf geteiltem Device → kein Versprechen. +| 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` → `` | +| 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** | -**Empfehlung:** Wenn Hi-Res verzichtbar → **B**. Wenn Hi-Res zwingend → **A**. -**C** nur, wenn kein Hardware-Budget UND Hi-Res nötig — und nur nach Messung (nie wieder -„verifiziert" ohne Messung auf `cam`, 04 eiserne Regel 3). +**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. -### Messung Weg C (Probe) — Anleitung & Ergebnis +**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). -Temporäre, rein lesende Diagnose-Route in `snapshotService.js`: `GET /:id/hires-probe`. -Sie wartet bis `cam` frei ist, holt **einen** `cam_hires`-Frame und schreibt dann 12s lang -alle 100ms den `cam_hires`-Zustand mit (`cons`, `prods`, `states`). - -Ablauf: -1. Code auf Server syncen, **`AppRobotWebcam` neu starten** (lädt `server.js`; go2rtc unberührt). -2. Im Viewer die zu messende Kamera **ausschalten** (⏸) → `cam` hat 0 Consumer. -3. `curl http://:8444/api/snapshot/cam0/hires-probe` (oder im Browser öffnen). -4. JSON-Antwort + Container-Log (`[probe]…`) hierher. - -Entscheidend: **`producerGoneAtMs`** (wann `prods` auf 0 fällt) und wie sich `states` -entwickelt. Daraus wird der robuste Rückweg gebaut (warten bis `prods===0` + Settle). -Wenn `prods` **nie** 0 wird → go2rtc baut den Producer gar nicht ab → Weg C ist tot, -dann bleibt nur A oder B. - -**Ergebnis:** _(hier eintragen nach der Messung)_ - -Danach die `hires-probe`-Route wieder entfernen. - -### Noch offen: Multi-User (siehe Abschnitt oben) - -Unabhängig vom 106%-Race: bei ≥2 aktiven Clients kann `/hires` nicht starten, weil -Schritt 1 wartet bis `cam` 0 Consumer hat (max 8s), ein zweiter Browser die Consumer-Zahl -aber nie auf 0 fallen lässt → Timeout → 503. Variante A löst das mit (separates Device, -kein Warten auf 0 Consumer). Sonst: „Schalter"-Idee oben (ein Producer, Server verteilt). \ No newline at end of file +**FFmpeg-Argumente** sind identisch zu denen, die die *bisher funktionierende* go2rtc- +Quelle erzeugte (`-f v4l2 -input_format mjpeg -video_size 640x480 -framerate 30 -i …`), +nur `-c:v copy -f mpjpeg pipe:1` als Ausgabe → kein Re-Encode. \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 9723118..c95869c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,73 +1,42 @@ name: approbotwebcam # ════════════════════════════════════════════════════════════════════════════ -# MJPEG-AUFBAU – go2rtc (Streaming) + Node.js (Viewer/Proxy/API) +# NODE-MJPEG-SCHALTER – ein Node-Container besitzt die Kameras selbst # ════════════════════════════════════════════════════════════════════════════ # -# Portainer: Stack → Web editor → dieses YAML einfügen → Deploy. -# Vorher in Portainer → "Environment variables": +# Node startet pro Kamera EINEN FFmpeg (640 MJPEG passthrough) und verteilt den +# Stream als multipart/x-mixed-replace an die Browser (). Für HD-Snapshots +# stoppt der Schalter den Live-FFmpeg sauber (Prozess-close = Gerät frei), +# greift 1280×960, schaltet zurück. go2rtc wird NICHT mehr gebraucht. +# +# Warum so: go2rtcs API konnte nicht zuverlässig melden, wann FFmpeg das Gerät +# freigibt → Race (zwei Encoder auf /dev/videoN = ~108% CPU). Wenn Node FFmpeg +# selbst startet, ist dessen 'close'-Event der harte Beweis „Gerät frei". +# Siehe doc/09_Bug_reports.md. +# +# Portainer: Stack → Web editor → dieses YAML → Deploy. # APP_PATH = /absoluter/pfad/zum/appRobotWebcam (Code muss dort liegen) # -# WICHTIG: Vor jedem Redeploy sicherstellen, dass server.js / public/ / src/ -# auf dem Server unter APP_PATH aktuell sind (Synology-Sync abwarten). -# -# Firewall (Internet): TCP 8444 (Viewer+API) -# Port 1984 (go2rtc) NICHT nach aussen – läuft nur intern via localhost. -# UDP 8555 (WebRTC) wird NICHT verwendet – Viewer läuft im MJPEG-Modus. +# Firewall (Internet): TCP 8444 (Viewer + Stream + API). Sonst nichts mehr. # # Zugriff: -# Viewer: http://:8444/ -# Snapshot (Homing) http://:8444/api/snapshot/cam0 -# go2rtc-Debug-UI http://:1984/ (nur intern/LAN) +# Viewer: http://:8444/ +# Live-Stream: http://:8444/api/stream/cam0 +# Snapshot (Homing): http://:8444/api/snapshot/cam0 (+ /hires) +# +# ROLLBACK auf den alten go2rtc-Aufbau: git checkout -- docker-compose.yaml +# server.js public/ src/ (der go2rtc-Stand liegt in der Git-Historie). # ════════════════════════════════════════════════════════════════════════════ -configs: - go2rtc_yaml: - content: | - streams: - # 640x480 MJPEG, Re-Encode in go2rtc (~50% CPU für 2 Kameras mit Clients). - # Viewer läuft im MJPEG-Modus (MODE='mjpeg' in viewer.js) → keine Freezes, ~200ms. - # NICHT #video=copy: am 2026-06-04 getestet → CPU 50% → 107% (schlechter). Verworfen. - 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" - # Phase-2 Hi-Res: on-demand (dormant bis erster Consumer). #video=copy auf dieser - # Kamera defekt (04_*), daher #video=mjpeg. Nur ~1-2s aktiv pro Grab. - # Rollback: diese beiden Zeilen entfernen + Redeploy. - cam0_hires: "ffmpeg:device?video=/dev/video0&input_format=mjpeg&video_size=1280x960&framerate=15#video=mjpeg" - cam1_hires: "ffmpeg:device?video=/dev/video2&input_format=mjpeg&video_size=1280x960&framerate=15#video=mjpeg" - webrtc: - listen: ":8555" - candidates: - - stun:8555 - api: - listen: ":1984" - origin: "*" - log: - level: info - services: - # ── go2rtc: Kamera-Capture · MJPEG Re-Encode · Streaming ────────────────── - go2rtc: - image: ghcr.io/alexxit/go2rtc - container_name: AppRobotGo2RTC - restart: unless-stopped - network_mode: host - devices: - - /dev/video0:/dev/video0 - - /dev/video2:/dev/video2 - group_add: - - video - configs: - - source: go2rtc_yaml - target: /config/go2rtc.yaml - - # ── webcam: Node.js (Viewer · /api/ws-Proxy · Snapshot-API) ────────────── webcam: build: context: /tmp dockerfile_inline: | FROM node:lts-bookworm-slim + RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \ + && rm -rf /var/lib/apt/lists/* WORKDIR /usr/src/app EXPOSE 8444 image: approbotwebcam:latest @@ -77,19 +46,27 @@ services: command: sh -c "npm install --omit=dev && node server.js" volumes: - ${APP_PATH:-.}:/usr/src/app + devices: + - /dev/video0:/dev/video0 + - /dev/video2:/dev/video2 + group_add: + - video environment: - NODE_ENV=production - PORT=8444 - - GO2RTC_URL=http://localhost:1984 - depends_on: - - go2rtc + # Optional: Geräte/Auflösung überschreiben (sonst Auto-Detect + Defaults) + # - DEV0=/dev/video0 + # - DEV1=/dev/video2 + # - LIVE_SIZE=640x480 + # - LIVE_FPS=30 + # - HIRES_SIZE=1280x960 + # - HIRES_FPS=15 -# ── FALLBACK ────────────────────────────────────────────────────────────────── -# Meckert Portainer beim Deploy über "configs content" (sehr alte Compose-Version)? -# → den configs-Block oben löschen und stattdessen beim go2rtc-Service mounten: -# volumes: -# - ${APP_PATH:-.}/go2rtc.yaml:/config/go2rtc.yaml:ro -# -# Bleibt eine Kamera schwarz? → in der Config oben die Quelle ersetzen durch die -# simple, bestätigte Form (ohne Auflösung): "ffmpeg:/dev/video0#video=mjpeg" +# ── Hinweise ──────────────────────────────────────────────────────────────────── +# • Bleibt eine Kamera schwarz? Geräte prüfen: v4l2-ctl --list-devices +# und ob 640x480 bzw. 1280x960 als MJPEG nativ angeboten werden: +# v4l2-ctl --list-formats-ext -d /dev/video0 +# Nur MJPEG-native Auflösungen bleiben CPU-arm (YUYV → Software-Encode = teuer). +# • Meckert Portainer über sehr alte Compose-Syntax (dockerfile_inline)? Dann +# Compose/Docker-Engine aktualisieren – dieser Aufbau braucht Compose v2. # ──────────────────────────────────────────────────────────────────────────────── diff --git a/package.json b/package.json index 5986de0..ae688f1 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,14 @@ { "name": "approbotwebcam", - "version": "0.2.0", - "description": "Low-latency WebRTC webcam service (go2rtc + Node proxy) for robot vision", + "version": "0.3.0", + "description": "Low-latency MJPEG webcam service (Node owns cameras via FFmpeg) for robot vision", "main": "server.js", "scripts": { "start": "node server.js", "dev": "node server.js" }, "dependencies": { - "express": "^4.21.1", - "http-proxy-middleware": "^3.0.3" + "express": "^4.21.1" }, "engines": { "node": ">=20" diff --git a/public/index.html b/public/index.html index 40ae43e..a82ba3a 100644 --- a/public/index.html +++ b/public/index.html @@ -39,11 +39,8 @@ .cam-box { position: relative; background: #000; border: 1px solid #2a2a2a; min-width: 320px; } - video-stream { display: block; width: 640px; height: 480px; background: #111; } - video-stream video { width: 100%; height: 100%; object-fit: contain; } - - /* Eingefrorener Frame während des Hi-Res-Tests (Phase 1) */ - .cam-freeze { display: block; width: 640px; height: 480px; background: #111; } + /* Live-MJPEG (multipart/x-mixed-replace) – nativ im */ + .cam-img { display: block; width: 640px; height: 480px; background: #111; object-fit: contain; } .cam-label { position: absolute; top: 5px; left: 8px; z-index: 2; @@ -90,7 +87,6 @@
- diff --git a/public/viewer.js b/public/viewer.js index 61857dd..77ece40 100644 --- a/public/viewer.js +++ b/public/viewer.js @@ -1,404 +1,119 @@ 'use strict'; -// go2rtc Player-Modi. -// 'mjpeg' → Passthrough: go2rtc reicht Kamera-MJPEG 1:1 durch. -// KEIN Transcode → ~0% CPU, keine Freezes, ~200ms Latenz. -// 'webrtc,mse,mjpeg' → WebRTC bevorzugt. ABER: WebRTC/MSE können kein MJPEG → -// go2rtc transcodiert MJPEG→H.264 in Software (libx264) → -// ~55% CPU pro Kamera + Keyframe-Freezes. ~130ms Latenz. -const MODE = 'mjpeg'; -const IS_MJPEG = !MODE.includes('webrtc'); // MJPEG-Modus: kein WebRTC/getStats/Health +// ── Architektur ─────────────────────────────────────────────────────────────── +// Der Server (Node) besitzt die Kameras und liefert den Live-Stream als +// MJPEG multipart/x-mixed-replace unter /api/stream/. Der Browser rendert +// das nativ in einem . KEIN WebRTC, KEIN go2rtc, kein Transcode. +// +// HD-Snapshot: GET /api/snapshot//hires. Der Server-Schalter pausiert dafür +// den Live-FFmpeg kurz (~1–2 s), greift 1280×960, schaltet zurück. Der - +// Stream friert in dieser Zeit ein und läuft danach weiter – kein Client-Handling +// nötig (das war früher die Fehlerquelle). -// ── Überwachungs-Parameter ─────────────────────────────────────────────────── -const MONITOR_INTERVAL = 2500; // ms zwischen Health-Checks (getStats) -const CONNECT_GRACE_MS = 25000; // so lange darf der Verbindungsaufbau dauern (kein Alarm) -const WARMUP_MS = 15000; // Karenz nach 'playing', bis Überlast-Erkennung scharf wird -const OVERLOAD_TICKS = 3; // so viele kritische Checks in Folge → Auto-Abschaltung - -// Schwellen (auf Basis der ZUVERLÄSSIGEN getStats-Werte, nicht Render-Drops): -const SERVER_LOW_FPS = 12; // recv < 12/s nach Aufwärmen → Server liefert wenig (nur Warnung) -const CLIENT_DECODE_RATIO = 0.6; // decoded < 60% von recv → Decoder kommt nicht nach (echte Client-Überlast) -const NET_LOST_PER_TICK = 5; // mehr verlorene Pakete/Intervall → Netz-Warnung -const NET_JITTER_MS = 60; // mehr Jitter → Netz-Warnung - -// ── Logging (Browser DevTools → Console → F12) ─────────────────────────────── const P = '[WebcamViewer]'; -const log = (c, m) => console.log(`${P}[${c}] ${m}`); -const warn = (c, m) => console.warn(`${P}[${c}] ⚠ ${m}`); +const log = (c, m) => console.log(`${P}[${c}] ${m}`); +const warn = (c, m) => console.warn(`${P}[${c}] ⚠ ${m}`); const logErr = (c, m, e) => console.error(`${P}[${c}] ✗ ${m}`, e ?? ''); -const sleep = ms => new Promise(r => setTimeout(r, ms)); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); -let GO2RTC_PORT = 1984; -const cameras = []; // { id, box, infoEl, toggleBtn, active, startedAt, playingSince, statsLast, badTicks, autoOff } +const cameras = []; // { id, box, img, infoEl, toggleBtn, hdBtn, active, busy } -// ── Stream starten / stoppen ───────────────────────────────────────────────── +// ── Live-Stream an/aus ──────────────────────────────────────────────────────── function startStream(cam) { - if (cam.box.querySelector('video-stream')) return; - const wsUrl = `ws://${location.hostname}:${GO2RTC_PORT}/api/ws?src=${encodeURIComponent(cam.id)}`; - log(cam.id, `Verbinde → ${wsUrl}`); - - cam.active = true; - cam.startedAt = performance.now(); - cam.playingSince = null; // wird erst beim 'playing'-Event gesetzt - cam.statsLast = null; - cam.badTicks = 0; - - const stream = document.createElement('video-stream'); - stream.mode = MODE; - stream.addEventListener('playing', () => { - cam.playingSince = performance.now(); - cam.statsLast = null; - cam.badTicks = 0; - log(cam.id, '▶ Bild läuft (Aufwärmphase startet)'); - }, true); - stream.addEventListener('error', (e) => logErr(cam.id, 'Video-Fehler', e), true); - stream.src = wsUrl; - - cam.box.insertBefore(stream, cam.box.firstChild); + cam.active = true; + // Cache-Buster erzwingt eine frische Verbindung (sonst hängt Reconnect manchmal) + cam.img.src = `/api/stream/${encodeURIComponent(cam.id)}?t=${Date.now()}`; cam.toggleBtn.textContent = '⏸'; cam.toggleBtn.title = 'Stream ausschalten'; setInfo(cam, 'verbindet…', ''); + log(cam.id, 'Live an'); } -function stopStream(cam, auto = false) { - const el = cam.box.querySelector('video-stream'); - if (el) el.remove(); +function stopStream(cam) { cam.active = false; - cam.autoOff = auto; - cam.playingSince = null; + cam.img.removeAttribute('src'); // schließt die multipart-Verbindung cam.toggleBtn.textContent = '▶'; cam.toggleBtn.title = 'Stream einschalten'; - setInfo(cam, auto ? 'auto-aus (Client überlastet)' : 'aus', auto ? 'crit' : ''); - log(cam.id, auto ? 'AUTO-abgeschaltet (Decoder kam nicht nach)' : 'manuell aus'); - if (auto) showNotice(); + setInfo(cam, 'aus', ''); + log(cam.id, 'Live aus'); } -// ── Hi-Res Canvas-Freeze + Grab (Phase 2) ─────────────────────────────────── - -// Holt letzten 640er-Frame von /api/snapshot und zeichnet ihn auf canvas. -// Robuster als drawImage(video-stream): go2rtc MJPEG-Modus hat kein