## Roadmap – Dynamische Kamera-Konfiguration (config.html) **Ziel:** Live-Auflösung pro Kamera zur Laufzeit umschalten, ohne Datei-Editor oder Container-Restart. Erreichbar über einen Link aus `index.html`. **Priorität (unverändert):** Latenz > CPU > Bildqualität. Bandbreite ist der Engpass beim Internet-Zugriff. --- ## Kontext Alle drei Kameras (C270, C920, C922) unterstützen dieselben MJPEG-nativen Auflösungen. Auf dem Host bestätigt (`v4l2-ctl --list-formats-ext`): | Auflösung | Pixel | Faktor ggü. 640×480 | Seitenverhältnis | |-----------|-------|----------------------|------------------| | 160×120 | 19'200 | −94% | 4:3 | | 320×240 | 76'800 | −75% | 4:3 | | 640×360 | 230'400 | −25% | 16:9 | | 640×480 | 307'200 | Referenz (aktuell) | 4:3 | | 800×600 | 480'000 | +56% | 4:3 | | 1280×720 | 921'600 | +200% | 16:9 | Nur MJPEG-native Auflösungen verwenden – sonst fällt V4L2 auf YUYV (unkomprimiert) zurück und FFmpeg muss software-encoden (~50% CPU pro Kamera). Siehe `09_Bug_reports.md`. --- ## ℹ️ Info: C920 Bandbreiten-Anomalie (nur Doku, NICHT umsetzen) Messung auf dem Host (2026-06-07), alle Kameras auf `liveSize: 320x240`: | Setup | Gesamt-Bandbreite | |-------|-------------------| | nur cam0 (C270) bzw. cam1 (C270) | ~4 Mbps pro Kamera | | **cam2 (C920) allein** | **~13 Mbps** | **Befund:** Die C920 produziert bei kleinen 4:3-Auflösungen (320×240) unverhältnismässig grosse MJPEG-Frames. Ihr Hardware-Encoder ist für den nativen 1920×1080-Sensor (16:9) kalibriert und komprimiert bei heruntergerechneten 4:3-Formaten kaum – ~3× mehr Bandbreite als die C270 bei identischer Auflösung. Die `compression_quality` lässt sich per V4L2 nicht steuern (auf dem Host geprüft: Control existiert nicht), und es gibt kein Logitech-Tool dafür. **Mögliche spätere Gegenmassnahmen für cam2 (bewusst NICHT Teil dieser Roadmap):** 1. **Native 16:9-Auflösung erzwingen** – `640x360` statt `320x240`. Mehr Pixel, aber der Encoder arbeitet im nativen Seitenverhältnis evtl. effizienter. Muss gemessen werden. 2. **FPS senken** – `liveFps: 10` für cam2. Linear weniger Bandbreite, keine Latenz-Kosten. 3. **Server-Re-Encode** – `"encode": "mjpeg"` + steuerbares `-q:v` nur für cam2. Volle Kontrolle über die Frame-Grösse, kostet aber ~20% CPU für diese eine Kamera. → Diese drei Punkte sind ein separates Thema (MJPEG-Qualität / FPS). Diese Roadmap deckt **nur die Auflösungs-Umschaltung** ab. Die Erkenntnis ist hier nur dokumentiert, damit bei der UI-Auswahl klar ist, warum 320×240 für die C920 nicht automatisch „sparsam" bedeutet. --- ## Geplante UI: `/config.html` Separate Admin-Seite (kein Viewer-Umbau nötig). Pro Kamera eine Zeile mit ComboBox: ``` ┌─────────────────────────────────────────────────────────┐ │ AppRobotWebcam · Konfiguration [← zum Viewer] │ ├──────────┬──────────────┬────────────────┬──────────────┤ │ cam0 │ Kamera 0 │ [320×240 ▼] │ aktuell: 320×240 │ │ cam1 │ Kamera 1 │ [640×360 ▼] │ aktuell: 640×360 │ │ cam2 │ Kamera 2 ⚠ │ [Aus ▼] │ aktuell: aus │ ├──────────┴──────────────┴────────────────┴──────────────┤ │ Status: bereit [Speichern & Anwenden] │ └─────────────────────────────────────────────────────────┘ ``` **ComboBox-Optionen pro Kamera** (zentral als Konstante, von Server + Client geteilt): `Aus`, `160×120`, `320×240`, `640×360`, `640×480`, `800×600`, `1280×720` - `Aus` → `stream: false` = **Snapshot-Modus**: kein Video. Der Viewer zeigt stattdessen alle 15 s ein **HD-Einzelbild** (`GET /api/snapshot//hires`, HD-Auflösung pro Kamera aus cameras.json) mit grossem Banner „Single Picture no Video". Server-seitig öffnet jeder Grab das Gerät kurz und schliesst es wieder → Gerät meist geschlossen, minimale Mobil- Bandbreite (1 HD-JPEG / 15 s statt 30 Frames/s). Der generische Endpunkt `GET /api/snapshot/` (z. B. fürs Homing) bleibt unverändert (Live-Frame bzw. one-shot via `grabSnapshot()` an `liveSize`). - Auflösung → `stream: true` + `liveSize: "x"` (kontinuierlicher MJPEG-Stream) - Bei cam2 (C920) ein dezenter ⚠-Hinweis-Tooltip: „C920 braucht bei kleinen 4:3-Auflösungen überdurchschnittlich Bandbreite – siehe Doku." --- ## Implementierung Reihenfolge: Phase 3 (Hot-Reload) → Phase 1 (API) → Phase 2 (UI) → Phase 4 (Link). So ist jede Phase einzeln testbar (zuerst per `curl`, dann erst die UI). ### Erlaubte Auflösungen – Single Source of Truth Neue Datei `src/liveSizes.js`, von `server.js` und `config.html` (über `/api/config`) genutzt: ```js 'use strict'; // MJPEG-native Auflösungen ALLER eingesetzten Kameras (C270/C920/C922). // Auf dem Host mit `v4l2-ctl --list-formats-ext` verifiziert (2026-06-07). const LIVE_SIZES = ['160x120', '320x240', '640x360', '640x480', '800x600', '1280x720']; module.exports = { LIVE_SIZES }; ``` `videoOutArgs`/Re-Encode bleibt unberührt. Validierung im POST-Handler nutzt diese Liste. ### Phase 3 – Hot-Reload in `CameraSwitch` (`src/cameraSwitch.js`) Neue Methode `reconfigure({ liveSize, stream })`. Nutzt die vorhandenen Bausteine (`_killCurrentAndWait`, `_spawnLive`), respektiert Lock und On-Demand: ```js async reconfigure({ liveSize, stream } = {}) { // Während eines HD-Grabs nicht eingreifen – nach dem Grab gilt eh die neue liveSize. if (this.lock) { if (liveSize) this.liveSize = liveSize; this.streamEnabled = stream; return; } const sizeChanged = liveSize && liveSize !== this.liveSize; if (liveSize) this.liveSize = liveSize; // Laufenden Live-Prozess stoppen, damit er mit neuer Auflösung neu startet. if (sizeChanged && this.proc && this.state === 'live') { await this._killCurrentAndWait(); // FD frei (close-Event) } // Neu starten nur wenn Stream erwünscht UND (On-Demand: Verbraucher vorhanden). if (this.state === 'stopped' && !this.proc && (!this.onDemand || this.subscribers > 0)) { this._spawnLive(); } } ``` > **Umgesetzt:** `CameraSwitch` hat ein `streamEnabled`-Flag, das NUR den kontinuierlichen > Live-Stream sperrt (`_spawnLive`/`acquire`/`_scheduleRestart` sind dadurch gegated). > Einzel-Snapshots umgehen das über `grabSnapshot()` (one-shot open/grab/close an > `liveSize`) – so liefert eine „Aus"-Kamera weiterhin Bilder, ohne dauerhaft zu streamen. > `reconfigure({stream:false})` killt einen evtl. laufenden Live-Prozess und startet ihn > nicht neu. **Wichtig – laufende Browser-``-Streams:** Eine reine *Auflösungs*-Änderung ist für den Client nahtlos – der multipart-Stream läuft weiter, nur die Frame-Grösse ändert sich (kurzes Einfrieren während des FFmpeg-Neustarts, wie beim HD-Grab). Ein *Aus*-Schalten wirkt erst nach Viewer-Reload. Das in der UI-Statuszeile vermerken. ### Phase 1 – API (`server.js`, neuer Router `src/configService.js`) `server.js` hält bereits `camerasJson` (vollständig geparst) und `switches`/`camsMeta`. Den geparsten `camerasJson` als `let` im Modulscope behalten, damit der POST-Handler beim Schreiben **alle übrigen Felder erhält** (device, name, hiresSize, note …) und nur `liveSize`/`stream` patcht. **GET `/api/config`** → aktuelle Konfiguration + erlaubte Optionen: ```json { "liveSizes": ["160x120","320x240","640x360","640x480","800x600","1280x720"], "cameras": [ { "id": "cam0", "name": "Kamera 0", "liveSize": "320x240", "stream": true }, { "id": "cam1", "name": "Kamera 1", "liveSize": "640x360", "stream": true }, { "id": "cam2", "name": "Kamera 2", "liveSize": "320x240", "stream": false } ] } ``` `liveSize` pro Kamera aus `switches[id].liveSize` (Laufzeit-Wahrheit), `stream`/`name` aus `camsMeta`. **POST `/api/config`** (Body = Array von `{ id, liveSize?, stream }`): 1. **Validieren:** jede `id` muss existieren; `liveSize` (falls gesetzt) muss in `LIVE_SIZES` sein; sonst `400` mit Fehlertext, **keine** Teiländerung. 2. **In-Memory patchen:** `camsMeta[i].stream` setzen; in `camerasJson.cameras[i]` `liveSize`/`stream` setzen (übrige Felder bleiben). 3. **Persistieren:** `cameras.json` atomar schreiben → in `cameras.json.tmp` schreiben, dann `fs.renameSync` über das Original (kein halb-geschriebenes File bei Crash). 4. **Anwenden:** für jede geänderte Kamera `await switches[id].reconfigure({...})`. 5. Antwort `200` mit der frischen Config (wie GET), damit die UI sofort den Ist-Stand zeigt. `express.json()`-Middleware in `server.js` ergänzen (aktuell nicht aktiv). Router unter `app.use('/api/config', createConfigRouter(switches, camsMeta, () => camerasJson, persistFn))`. ### Phase 2 – `public/config.html` + `public/config.js` - Stil aus `index.html` übernehmen (dunkles Monospace-Theme, gleiche Header-Leiste). - Beim Laden `GET /api/config` → Tabelle rendern, `