Files
appRobotWebcam/doc/05_screenShot_roadmap.md
2026-06-04 17:47:58 +02:00

10 KiB
Raw Blame History

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 <canvas> 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 ~810 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 <canvas> zeichnen, „HD Image Work" einblenden, Canvas anstelle des <video-stream> zeigen.
  2. <video-stream> 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: <video-stream> 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. staterunning) → 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.:
    { "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:

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 ~12 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, <video-stream> 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 <video>-Elements per canvasCtx.drawImage(video, …) einfrieren.
  • Text „HD Image Work" unten rechts, ca. 30 % der Bildbreite, mit halbtransparentem Hintergrund (Lesbarkeit).
  • Canvas über/anstelle des gestoppten <video-stream> zeigen.
  • Nach Schritt 6 Canvas entfernen.
  • Rein clientseitig go2rtc sieht davon nichts.

Offene Punkte / Risiken (ehrlich)

Punkt Status Umgang
Gibt go2rtc das Gerät frei + wie schnell? ungeklärt Linchpin Phase 1 misst es. Erst danach Phase 2.
Warmup-Schwarzbild bei 1280 bekannt kurz konsumieren + Breiten/Größen-Check + Retry
Mehrere gleichzeitige Zuschauer Einschränkung Gerät wird nur frei, wenn alle cam0 loslassen. Für 1 Operator + Button ok; Multi-Client bräuchte ein Broadcast-Signal „alle auf Platzhalter".
cam0_hires greift Gerät schon beim Start? zu prüfen nach Redeploy via /api/streams bestätigen, dass es dormant ist
Fehler mitten in der Sequenz beherrschbar finally → Client immer zurück auf cam0; Worst case go2rtc-Restart, cam0-Definition bleibt heil
Orchestrierung Client↔Server Komplexität klare Reihenfolge: Client löst cam0 → ruft Endpunkt → Endpunkt wartet+grabt → Client hängt zurück

Käme das so hin? — kurz

Ja. Der Ablauf 16 mit anpassbaren Pausen ist tragfähig, wenn der Linchpin (Geräte-Freigabe nach Consumer-Verlust) hält — und genau das beweist Phase 1, ohne cam0 anzufassen und ohne einen einzigen schreibenden go2rtc-Aufruf. Zwei Vereinfachungen gegenüber der ersten Skizze: der Platzhalter ist clientseitig (kein eigener Stream), 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.