From 8c8c769e22505e812b7a692d74fc06c391d7928d Mon Sep 17 00:00:00 2001
From: chk <79915315+ChKendel@users.noreply.github.com>
Date: Fri, 5 Jun 2026 06:36:48 +0200
Subject: [PATCH] Umbau mit cameraSwitch
---
doc/04_Delay_roadmap.md | 7 +
doc/05_screenShot_roadmap.md | 109 ++++++++-
doc/09_Bug_reports.md | 66 ++---
docker-compose.yaml | 103 +++-----
package.json | 7 +-
public/index.html | 8 +-
public/viewer.js | 455 ++++++-----------------------------
server.js | 151 ++++--------
src/cameraSwitch.js | 259 ++++++++++++++++++++
src/snapshotService.js | 315 +++++++-----------------
10 files changed, 641 insertions(+), 839 deletions(-)
create mode 100644 src/cameraSwitch.js
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 @@
-