From f882263972e9ef1b66401c38827434e43b986c6f Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:47:58 +0200 Subject: [PATCH] roadmap Arbeiten --- doc/05_screenShot_roadmap.md | 219 ++++++++++++++++++ doc/06_portForwarding_roadmap.md | 179 ++++++++++++++ ..._roadmap.md => 07_OptionalToDo_roadmap.md} | 0 3 files changed, 398 insertions(+) create mode 100644 doc/05_screenShot_roadmap.md create mode 100644 doc/06_portForwarding_roadmap.md rename doc/{05_OptionalToDo_roadmap.md => 07_OptionalToDo_roadmap.md} (100%) diff --git a/doc/05_screenShot_roadmap.md b/doc/05_screenShot_roadmap.md new file mode 100644 index 0000000..3d114af --- /dev/null +++ b/doc/05_screenShot_roadmap.md @@ -0,0 +1,219 @@ +# AppRobotWebcam – Hi-Res-Snapshot via Consumer-Umhängen + +> Status: **Konzept, phasenweise testbar.** Noch nicht umgesetzt. +> Vorgeschichte & gescheiterte Ansätze: siehe `04_Delay_roadmap.md` (Abschnitt +> „KONSOLIDIERT"). Diese Datei beschreibt den Ansatz, der die dort dokumentierten +> Fehler **strukturell** umgeht. + +--- + +## Grundidee + +Das Kernproblem aller bisherigen Versuche: **Eine USB-Kamera lässt sich nur einmal +öffnen**, und der Live-Viewer zwingt go2rtc, das Gerät zu halten (per on-demand- +Reconnect). Jeder Versuch, go2rtc das Gerät zu *entreißen* oder die Live-Quelle zur +Laufzeit *umzuschalten*, ist gescheitert (device-busy, bzw. `PATCH` hängt an → 107 %). + +**Neuer Ansatz – das Problem umdrehen:** Nicht das Gerät dem Stream entreißen, sondern +**die Zuschauer vom Live-Stream wegziehen.** Hat `cam0` keine Zuschauer mehr, stoppt +go2rtc den Producer von selbst (on-demand) und gibt das Gerät frei. Dann kann ein +separater Hi-Res-Stream es kurz für sich haben. + +### Warum das sicher ist (im Gegensatz zu allem vorher) + +- **`cam0` wird nie verändert.** Kein `PATCH`/`PUT`/`DELETE` auf den Live-Stream. + Das Append-Problem (107 %) kann nicht auftreten. +- **Zur Laufzeit nur LESENDE go2rtc-Aufrufe**: `GET /api/streams`, `GET /api/frame.jpeg`. + Die einzige „schreibende" Änderung ist das Hinzufügen von `cam0_hires` in der Config + (per Redeploy, nicht zur Laufzeit). +- **Kleiner Schadensradius**: Geht etwas schief, ist die Erholung „Browser wieder auf + `cam0` hängen" → go2rtc startet `cam0` neu. Keine kaputte Stream-Definition, die bis + zum Neustart hängt. + +Damit respektiert der Ansatz die eisernen Regeln aus `04_*` (Snapshot-Pfad read-only, +keine Laufzeit-Mutation von cam0/cam1). + +--- + +## Architektur + +| Stream | Quelle | on-demand | Zweck | +|--------|--------|-----------|-------| +| `cam0` | Kamera @640 | ja | **unverändert** – Live-Stream | +| `cam1` | Kamera @640 | ja | **unverändert** – Live-Stream | +| `cam0_hires` | Kamera @1280×960 | ja | **nur** für den Hi-Res-Grab (Phase 2) | +| `cam1_hires` | Kamera @1280×960 | ja | dito für cam1 | + +**Platzhalter = rein clientseitig**, kein go2rtc-Stream nötig: +Der Browser friert beim Umhängen den zuletzt gezeigten Live-Frame auf einem `` +ein und blendet „HD Image Work" ein (ca. 30 % der Bildgröße, unten rechts). Das ist +das, was während des Grabs zu sehen ist. Vorteil: keine zusätzliche go2rtc-Last, kein +Nachschieben von Bildern in go2rtc. + +> Alternative (nur falls *mehrere* Zuschauer gleichzeitig denselben Platzhalter sehen +> sollen): ein echter go2rtc-`standbild0`-Stream aus einer statischen Bilddatei. Mehr +> Aufwand, hier zunächst nicht nötig. + +--- + +## Ziel-Ablauf (vollständig, Phase 1 + 2) + +``` +1. Browser hängt um: cam0 → Canvas-Standbild ("HD Image Work") [clientseitig] +2. cam0 hat 0 Zuschauer → go2rtc stoppt cam0-Producer → Gerät frei +3. [Pause ~4s] ← Wert ist FREI WÄHLBAR, wird aus Phase-1-Messung gesetzt +4. Node holt 1 Frame von cam0_hires → go2rtc öffnet Gerät @1280 → frame.jpeg + (Breite ≥1000px prüfen, sonst retry; Warmup beachten – s.u.) +5. cam0_hires-Consumer endet → Gerät frei [Pause ~4s] +6. Browser hängt zurück: Canvas → cam0 → Live @640 wieder da +``` + +Blackout auf **cam0** ~8–10 s (mit 4s-Pausen), cam1 unberührt. Für „alle 30 s, +Button-getriggert, Blackout ok" passt das. Die Pausen sind großzügig gewählt; sobald +Phase 1 die echte Freigabe-Zeit liefert, können sie gekürzt werden. + +> **Die 4s sind ein Startwert, keine feste Größe.** Phase 1 misst, wie schnell go2rtc +> das Gerät wirklich freigibt → daraus wird der reale Pausenwert. + +--- + +## Der eine Dreh- und Angelpunkt (= warum Phase 1 zuerst kommt) + +Der ganze Ansatz steht und fällt mit **einer** Annahme: + +> **Gibt go2rtc das Gerät frei, wenn `cam0` den letzten Zuschauer verliert — und wie +> schnell?** + +go2rtc *kann* einen Producer nach dem letzten Consumer „warm" halten statt ihn sofort +zu stoppen. Tut es das, bleibt das Gerät belegt und `cam0_hires` läuft auf „device +busy". **Diese Zahl wird in Phase 1 gemessen, bevor irgendein Hi-Res-Grab gebaut wird.** + +--- + +## PHASE 1 — Freigabe verifizieren (kein Grab, voll reversibel) + +**Ziel:** Beweisen, dass Schritt 1 → 2 → 6 funktioniert und das Gerät tatsächlich +(und wie schnell) frei wird. Kein `cam0_hires`, kein 1280-Zugriff. Risiko ~null: +im schlimmsten Fall ist es ein Reconnect von cam0. + +### Umzusetzen + +**A. Viewer (`public/viewer.js`)** – Button „Hi-Res-Test (Phase 1)": +1. Aktuellen `cam0`-Frame auf ein `` zeichnen, „HD Image Work" einblenden, + Canvas anstelle des `` zeigen. +2. `` für cam0 **entfernen/stoppen** (das ist das „Umhängen" – cam0 + verliert seinen Consumer). +3. `GET /api/snapshot/cam0/release-test` aufrufen (neuer Node-Endpunkt, s.u.). +4. Auf Antwort: `` für cam0 wieder einsetzen (Live zurück), Canvas weg. + +**B. Node (`src/snapshotService.js`)** – neuer read-only Endpunkt +`GET /api/snapshot/:id/release-test`: +1. Startzeit loggen. +2. `GET ${go2rtc}/api/streams` alle 200 ms pollen (max. ~10 s). +3. Loggen: + - Wann erreicht `cam0` **0 Consumer**? + - Wann ist der `cam0`-**Producer gestoppt** (Feld `producers` leer bzw. `state` ≠ + `running`) → das ist der Proxy für „Gerät frei". + - Dauer von „0 Consumer" → „Producer gestoppt" in ms. +4. Ergebnis ins Log schreiben **und** als JSON zurückgeben, z. B.: + ```json + { "freed": true, "msUntilFree": 1700, "samples": [...] } + ``` +5. Kein Schreibzugriff auf go2rtc. Nur Lesen. + +### Erfolgskriterium Phase 1 +- Log/JSON zeigt `freed: true` und eine **konkrete** `msUntilFree`. +- Nach dem Test (Schritt 6) zeigt cam0 wieder normal Live (~50 % CPU, stabil). + +### Was wir daraus lernen +- `msUntilFree` → der reale Pausenwert für Schritt 3/5 (statt der geratenen 4 s). +- Wird der Producer **nicht** gestoppt (`freed: false`): go2rtc hält das Gerät warm → + Ansatz so nicht tragfähig → prüfen, ob ein go2rtc-Setting das Verhalten ändert, + sonst zurück zu Weg A (separate Kamera, siehe `04_*`). + +> ⚠ Die genaue JSON-Form von `/api/streams` (Felder `producers`/`consumers`/`state`) +> vor dem Bauen kurz an der echten Instanz ansehen (`curl -s localhost:1984/api/streams`) +> und den Parser danach ausrichten — nicht annehmen. + +--- + +## PHASE 2 — Den Hi-Res-Grab ergänzen (Schritt 4 + 5) + +Nur starten, **wenn Phase 1 `freed: true` geliefert hat.** + +### Vorbereitung (Config, per Redeploy – nicht zur Laufzeit) +`docker-compose.yaml`, go2rtc-`streams` ergänzen: +```yaml +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" +``` +- `#video=mjpeg` (Re-Encode) ist ok – läuft nur ~1–2 s pro Grab. (`#video=copy` ist + laut `04_*` auf dieser Kamera tot.) +- **Präzondition prüfen:** Nach Redeploy via `/api/streams` bestätigen, dass + `cam0_hires` **dormant** ist (kein laufender Producer, solange niemand es anfragt). + Sonst würde es beim Start das Gerät greifen und mit `cam0` kollidieren. + +### Node-Endpunkt `GET /api/snapshot/:id/hires` (Phase-2-Variante) +Voraussetzung: der **Client hat cam0 bereits losgelassen** (Browser-Dance wie Phase 1). +Ablauf im Endpunkt: +1. Mutex setzen (kein paralleler Grab). +2. (optional) via `/api/streams` verifizieren: `cam0` hat 0 Consumer → sonst abbrechen + (Gerät noch belegt). +3. `sleep(msUntilFree)` – Gerät freigeben lassen. +4. **Grab mit Warmup** (robuste Variante): + - Kurz `cam0_hires` als Stream konsumieren (z. B. `GET /api/stream.mjpeg?src=cam0_hires`) + für ~1,5 s, damit die Kamera-Belichtung einschwingt und der Producer warm bleibt. + - Den **letzten** Frame behalten, der `Breite ≥ 1000 px` **und** nicht „zu klein/ + schwarz" ist (Warmup-Schutz, vgl. das frühere 1-KB-Schwarzbild). + - Einfachere Alternative: `GET /api/frame.jpeg?src=cam0_hires` mit Retry (mehrfach, + bis Breite ≥1000 px und plausible Größe). +5. Consumer von `cam0_hires` beenden → Gerät frei. +6. Mutex lösen. JPEG (1280×960) zurückgeben. + +**Client** nach Antwort: Canvas weg, `` cam0 wieder einsetzen (Schritt 6). + +### Robustheit (Pflicht) +- **`finally`/Recovery:** Egal was schiefgeht – der Client MUSS am Ende wieder auf `cam0` + hängen. Da `cam0` nie verändert wurde, reicht „wieder anhängen" zur vollen Erholung. +- **Timeout** auf den Grab (z. B. 8 s) → sonst Fehler + Recovery. +- **Mutex**: nie zwei Grabs gleichzeitig (würde zwei 1280-Producer = Gerätekonflikt + provozieren). + +--- + +## Platzhalter-Detail (Canvas „HD Image Work") + +- Beim Umhängen den aktuellen Frame des `