Files
appRobotWebcam/doc/04_Delay_roadmap.md
2026-06-05 07:32:05 +02:00

26 KiB
Raw Blame History

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.

🏁 Endergebnis Delay (gemessen 2026-06-05)

Variante Latenz CPU Freezes
go2rtc H.264 (WebRTC) ~130 ms ~100 % ja
go2rtc MJPEG ~200 ms ~50 % nein
Node-MJPEG-Schalter 139 ms ~5 % idle · ~35 %/Kam aktiv nein

Der Schalter unterbietet die go2rtc-MJPEG-Latenz (139 vs. 200 ms) und kommt nahe an H.264 (139 vs. 130 ms) — ohne dessen CPU-Last und Freezes. Stellschrauben, die das brachten: -fflags nobuffer + -flush_packets 1 (FFmpeg) und **socket.setNoDelay(true)

  • cork/uncork** (Node-Stream, ein TCP-Segment pro Frame). On-Demand drückt Idle auf ~5 %.

AppRobotWebcam Delay / Ruckler-Analyse

Symptom

Nach Umstieg auf WebRTC/H.264: Bild ruckelt, friert teils 12 s ein, manchmal bleibt ein Einzelbild ganz stehen. Im reinen MJPEG-Modus trat das nicht auf.


Diagnose-Verlauf

Phase 1 — Messung und Eingrenzung

Quelle CPU
AppRobotGo2RTC, 1 Client ~35103 %
AppRobotGo2RTC, 2 Clients 65114 %
AppRobotWebcam (Node.js) 0 %
Browser-Client (Laptop) ~10 %

getStats() im Browser lieferte konstant recv=30/s decoded=30/s dropped=0/s → Browser und Netz sind nicht das Problem.

Zwei Browser (Laptop + Handy) zeigen exakt identische Latenz für cam0 bzw. cam1. Ändert sich die Latenz, ändert sie sich auf beiden Clients synchron → Problem sitzt in go2rtc/FFmpeg, nicht in Netz oder Browser.

Phase 2 — Root-Cause-Analyse

go2rtc's generierter FFmpeg-Befehl (simple URL-Form):

-readrate_initial_burst 0.001 -re -i /dev/videoX
-c:v libx264 -g 50 -preset:v superfast -tune:v zerolatency

-re = Rate-Emulation für Datei-Wiedergabe — puffert Live-Frames künstlich.
-g 50 = Keyframe alle 1,67 s → bis zu 1,67 s Standbild nach Loss/Reconnect.
libx264 = Software-Encoding → CPU-intensiv, skaliert schlecht mit mehreren Clients.

Phase 3 — Source-Format-Experimente (alle versucht, Ergebnis unbefriedigend)

Source-Format Ergebnis Warum nicht ausreichend
ffmpeg:/dev/video0#video=h264 ~35% CPU mit 1 Client, Bild funktioniert -re erzeugt variable Latenz
ffmpeg:/dev/video0#video=h264#video=mjpeg ~95% CPU Doppeltes Encoding
v4l2:/dev/video0#video=h264 0% CPU ohne Client (on-demand ✓), kein Bild v4l2: Source unterstützt #video=h264 nicht
ffmpeg:-f v4l2 ...#video=h264 FFmpeg-Parsing-Fehler: -f wird als Dateiname interpretiert go2rtc splittet den String nicht in Args
ffmpeg:device?video=/dev/video0&input_format=mjpeg...#video=h264 ~103% CPU, Bild funktioniert Kein -re (gut), aber libx264 läuft trotzdem durch

Kern-Erkenntnis (nach Phase 3)

Das Source-Format ist nicht das Problem. libx264 Software-Encoding ist es. Egal wie die Frames reinkommen — der Encoder frisst denselben CPU. Alle Source-Experimente haben daran nichts geändert.

On-Demand-Verhalten ist ein Nebeneffekt: go2rtc startet den Encoder erst bei erstem Client, stoppt bei letztem. Das ist Standard-go2rtc-Verhalten, unabhängig vom Source-Format.


Schlussfolgerung: Zwei echte Lösungen

Lösung 1 — Hardware-Encoding (Intel QuickSync / VAAPI) ← bevorzugt

H.264-Encoding auf der Intel-iGPU statt auf der CPU. CPU-Last: ~35% → ~5%. Latenz unverändert (~130ms WebRTC).

Voraussetzung prüfen:

ls -la /dev/dri/
# renderD128 vorhanden? → Hardware-Encoding möglich

Wenn ja, Umsetzung:

# docker-compose.yaml — go2rtc service:
devices:
  - /dev/video0:/dev/video0
  - /dev/video2:/dev/video2
  - /dev/dri:/dev/dri          # ← GPU durchreichen

# go2rtc-Config:
streams:
  cam0: "ffmpeg:device?video=/dev/video0&input_format=mjpeg&video_size=640x480&framerate=30#video=h264#hardware"
  cam1: "ffmpeg:device?video=/dev/video2&input_format=mjpeg&video_size=640x480&framerate=30#video=h264#hardware"

#hardware weist go2rtc an, h264_vaapi zu verwenden. go2rtc baut den FFmpeg-Befehl mit VAAPI-Flags — ohne -re, mit GPU-Encoding.

Zu verifizieren nach Aktivierung:

  1. CPU fällt auf <10%?
  2. Latenz stabil <200ms?
  3. go2rtc-Log zeigt h264_vaapi statt libx264?

Lösung 2 — MJPEG (Fallback, sofort umsetzbar)

Kein Encoding, kein GOP, keine CPU-Last. War nachweislich stabil und flüssig. Latenz ~200ms (70ms mehr als WebRTC — für Roboter-Überwachung vertretbar).

streams:
  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"

Im Browser-Viewer MODE anpassen:

const MODE = 'mjpeg';  // statt 'webrtc,mse,mjpeg'

CPU erwartet: <5%. Kein -g 50, keine Freezes, kein Encoding-Jitter.


Ergebnis aller Versuche — Entscheid

Hardware-Encoding: gescheitert (go2rtc-Limitation)

renderD128 ist vorhanden (ls -la /dev/dri/ bestätigt). go2rtc's #hardware verwendet -hwaccel vaapi -hwaccel_output_format vaapi auf Input-Seite. Das setzt voraus, dass der Decoder VAAPI nutzt. MJPEG von v4l2 wird aber per Software dekodiert — hwupload findet keine VAAPI-Device-Referenz → Filterchain-Fehler.

[hwupload] A hardware device reference is required to upload frames to.
[AVFilterGraph] Error initializing filters

go2rtc's #hardware ist für Re-Encoding von RTSP-H.264-Streams gebaut, nicht für MJPEG-Kamera-Input. Ohne eigenen FFmpeg-Befehl (den go2rtc nicht erlaubt) ist Hardware-Encoding für diesen Use-Case nicht erreichbar.

Neue Hardware kaufen? Nicht empfohlen — und keine Garantie möglich:

  • renderD128 (Intel iGPU) ist bereits vorhanden und VAAPI-fähig. Das Problem liegt in go2rtc's Architektur, nicht in der Hardware. Bessere GPU würde nichts ändern.
  • Eine Kamera mit nativem H.264-Output (z.B. Logitech C920) würde das Encoding- Problem für den Live-Stream lösen — aber nicht das Hi-Res-Snapshot-Problem (Kamera bleibt bei einer Auflösung locked). Kein Mehrwert für diesen Use-Case.
  • Empfehlung: Kein Hardware-Kauf. MJPEG-Passthrough läuft stabil bei <5% CPU. Für H.264 (130 ms statt 200 ms) → MediaMTX-Weg (s.u.), keine neue Hardware nötig.

Entscheid: MJPEG-Passthrough ✓ (umgesetzt)

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"

Kamera liefert MJPEG nativ → go2rtc reicht es 1:1 durch → kein Encoding → CPU <5%.

H.264 Software H.264 Hardware MJPEG Passthrough
CPU ~100% gescheitert <5%
Latenz ~130ms ~200ms
Freezes gelegentlich keine
Stabilität mittel hoch

70ms mehr Latenz ist für Roboter-Überwachung vertretbar. Snapshots haben native JPEG-Qualität (kein H.264-Artefakte).


⚠ KORREKTUR (2026-06-04): Passthrough war nie aktiv

Obiger Entscheid war konfiguriert, aber nicht wirksam. Quelle und Auslieferung sind zwei verschiedene Dinge — und nur die Quelle wurde umgestellt.

konfiguriert tatsächlich geliefert
go2rtc-Quelle #video=mjpeg MJPEG
Viewer viewer.js MODE = 'webrtc,mse,mjpeg' Browser zog WebRTC

WebRTC und MSE können kein MJPEG transportieren — die einzigen WebRTC-Video-Codecs sind H.264/VP8/VP9/AV1. Sobald der Browser WebRTC zog, transcodierte go2rtc das Kamera-MJPEG nach H.264 in Software (libx264) — ein Encoder pro Kamera.

Beweis aus der Messung: CPU skalierte 2× mit der Client-Zahl (53% → 127% bei 2 Clients). Passthrough ist clientzahl-unabhängig ~0% — nur Transcoding skaliert so.

Das erklärt rückwirkend alles:

  • Hohe CPU trotz „MJPEG-Passthrough"-Config → es war nie Passthrough.
  • Auflösung war nie die Ursache — der libx264-Encoder war es (egal bei welcher Auflösung).
  • Freezes nur mit WebRTC, nie mit MJPEG → H.264-Keyframe-Abhängigkeit (-g 50 = bis 1,67s Standbild nach Loss). MJPEG-Frames sind unabhängig → ein Loss = ein einzelner Ruckler, nie ein mehrsekündiges Standbild.

Echter Fix (umgesetzt)

Die Auslieferung im Viewer auf MJPEG zwingen: MODE = 'mjpeg' in public/viewer.js. Damit zieht der Browser MJPEG statt WebRTC — go2rtc transcodiert nicht mehr nach H.264.

keine Freezes   ·   ~200ms Latenz   ·   kein H.264-Transcode

⚠ Korrektur (war hier falsch): Das ist kein Null-CPU / copy. go2rtc re-encodiert MJPEG→MJPEG (~50% für 2 Kameras, gemessen). Der Gewinn von MODE='mjpeg' ist der Wegfall des H.264-Transcodes (127% → ~50%) und der Freezes — nicht Null-Last. go2rtc-Quelle bleibt 640×480 #video=mjpeg.


Falls doch noch H.264 gewünscht (mit korrektem VAAPI)

Erfordert MediaMTX als Zwischenstufe:

v4l2 → FFmpeg (vaapi_device + eigene Flags) → RTSP (MediaMTX) → go2rtc WebRTC

FFmpeg-Befehl der funktionieren würde:

ffmpeg -vaapi_device /dev/dri/renderD128 \
  -f v4l2 -input_format mjpeg -video_size 640x480 -framerate 30 -i /dev/video0 \
  -vf "format=nv12,hwupload" -c:v h264_vaapi -g 15 -bf 0 \
  -f rtsp rtsp://mediamtx:8554/cam0

Aufwand: ~2h (zusätzlicher Container, RTSP-Verkabelung). Lohnt sich erst wenn 200ms Latenz nachweislich ein Problem für den Anwendungsfall ist.


Hi-Res-Snapshots — offenes Problem

Warum es nicht trivial ist

Eine USB-Kamera kann gleichzeitig nur eine Auflösung liefern. go2rtc hält die Kamera offen — Snapshot-Auflösung = Stream-Auflösung. /api/snapshot/cam0 proxied go2rtc's /api/frame.jpeg → liefert immer Stream-Auflösung (640×480).

Versuch: video_size=1280x960 im laufenden Stream → CPU sprang auf 112%. Wahrscheinliche Ursache: Kamera unterstützt 1280×960 nicht als natives MJPEG → FFmpeg fällt auf YUYV zurück → Software-MJPEG-Encoding → CPU explodiert. (Nicht reines I/O-Problem, sondern fehlendes natives Format.) Zurückgesetzt auf stabilen Zustand: 640×480 @ 30fps, ~20% CPU.

Zwingend vor jedem Auflösungstest:

v4l2-ctl --list-formats-ext -d /dev/video0   # prüft welche Auflösungen MJPEG-nativ sind
v4l2-ctl --list-formats-ext -d /dev/video2

Nur wenn eine Auflösung dort unter "MJPEG" (nicht "YUYV") erscheint, bleibt CPU niedrig.


Option 1 — Hi-Res-Stream + CSS-Skalierung (30 min, zuerst testen)

  • v4l2-ctl prüfen (s.o.)
  • Wenn 1280×720 als MJPEG nativ: video_size=640x480video_size=1280x720 in docker-compose
  • Browser zeigt per CSS 640px breit, Snapshot = volle 1280×720
  • CPU erwartet: moderat (<30 %), da MJPEG-Passthrough ohne Encoding
  • Wenn 1280×720 nur als YUYV: Option 2 wählen

Option 2 — Frame-Grab mit Blackout (23 h, konkreter Plan)

go2rtc hat eine Stream-Management-REST-API. Node.js stoppt den Stream kurz, greift mit FFmpeg direkt auf das Device zu, startet den Stream neu.

Blackout: ~12 Sekunden. Akzeptabel bei Snapshot-Intervall ≥ 40 s und Roboter-Pause.

Nötige Änderungen

1. docker-compose.yaml — Devices + FFmpeg in Node-Container

webcam:
  build:
    context: /tmp
    dockerfile_inline: |
      FROM node:lts-bookworm-slim
      RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
      WORKDIR /usr/src/app
      EXPOSE 8444
  devices:
    - /dev/video0:/dev/video0
    - /dev/video2:/dev/video2
  group_add:
    - video

2. snapshotService.js — neuer /hires-Endpoint

Konfiguration oben in der Datei (passend zu go2rtc-Config halten):

const CAM_CONFIG = {
  cam0: { device: '/dev/video0', hiresSize: '1280x720',
          streamUrl: 'ffmpeg:device?video=/dev/video0&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg' },
  cam1: { device: '/dev/video2', hiresSize: '1280x720',
          streamUrl: 'ffmpeg:device?video=/dev/video2&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg' },
};

Endpoint-Logik (Pseudocode):

router.get('/:id/hires', async (req, res) => {
  const cfg = CAM_CONFIG[req.params.id];
  if (!cfg) return res.status(404).json({ error: 'Unknown camera' });

  // 1. go2rtc-Stream stoppen (gibt Device frei)
  await fetch(`${go2rtcUrl}/api/streams?src=${req.params.id}`, { method: 'DELETE' });
  await new Promise(r => setTimeout(r, 800)); // warten bis FFmpeg-Prozess beendet

  // 2. Hi-Res-Frame via FFmpeg one-shot
  const jpeg = await captureOneFrame(cfg.device, cfg.hiresSize);

  // 3. Stream in go2rtc wiederherstellen
  await fetch(`${go2rtcUrl}/api/streams?src=${req.params.id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'text/plain' },
    body: cfg.streamUrl,
  });

  res.set({ 'Content-Type': 'image/jpeg', 'Cache-Control': 'no-store' });
  res.end(jpeg);
});

function captureOneFrame(device, size) {
  return new Promise((resolve, reject) => {
    const args = [
      '-f', 'v4l2', '-input_format', 'mjpeg', '-video_size', size,
      '-frames:v', '1', '-q:v', '1', '-f', 'mjpeg', 'pipe:1',
    ];
    // spawn('ffmpeg', ['-i', device, ...args]) → collect stdout → resolve(buffer)
  });
}

go2rtc-API-Endpunkte (⚠ die folgende PUT-mit-Body-Form war FALSCH — siehe Fehler-Log unten):

  • DELETE /api/streams?src={name} → stoppt Producer, gibt Device frei
  • PUT /api/streams?src={name} mit Body = Stream-URL → FALSCH: go2rtc liest die Quelle aus dem src-Query-Param, NICHT aus dem Body. Korrekt wäre PUT /api/streams?name={name}&src={quelle-url-encoded}.

3. Mutex (concurrent requests verhindern)

let hiresLock = false;
// Am Anfang des Endpoints:
if (hiresLock) return res.status(429).json({ error: 'hi-res snapshot in progress' });
hiresLock = true;
try { /* ... */ } finally { hiresLock = false; }

Option 3 — Separate Kameras für Homing

  • Zwei zusätzliche USB-Kameras, nur für Homing (kein Live-Stream)
  • go2rtc öffnet sie nicht → kein Konflikt, volle Auflösung on-demand
  • Aufwand: Hardware-Kosten + Montage + FFmpeg one-shot in Node.js
  • Sauberste Lösung langfristig, aber Hardware-Investment

Ergebnis der Tests

Option 1 gescheitert (1280×960 @ 30fps MJPEG nativ):

  • Kamera unterstützt 1280×960 nativ als MJPEG (per v4l2-ctl bestätigt)
  • CPU trotzdem 53% mit 1 Client / 127% mit 2 Clients
  • Ursache: reines I/O — go2rtc schiebt grosse Frames für jeden Client separat durch den Netzwerkstack. CPU skaliert 2× mit Clients → kein Encoding, nur Datenmenge.
  • Bei 2 Kameras × 1280×960 × 30fps × 2 Clients: ~3040 Mbit/s — zu viel.

Entscheid (damals): Option 2 (Blackout-Snapshot) — später VERWORFEN (siehe „KONSOLIDIERT" am Ende)

Live-Stream bleibt bei 640×480 @ 30fps (<5% CPU, stabil). Hi-Res on demand via /api/snapshot/cam{n}/hires:

GET /api/snapshot/cam0/hires
→ go2rtc-Stream löschen → 900ms warten → FFmpeg one-shot 1280×960 → Stream wiederherstellen
→ Blackout: ~12 s.  CPU-Peak: kurz, dann zurück auf <5%.

Umgesetzt in src/snapshotService.js und docker-compose.yaml.

Erster Live-Test (2026-06-04) — Ansatz später VERWORFEN (siehe „KONSOLIDIERT" am Ende)

Live-Stream nahezu Echtzeit, stabil. Hi-Res-Bild 1280×960 über /hires da. Zwei Bugs gefunden und sofort behoben:

  1. Schwarzer Player nach Reload ✓ behoben Ursache: Stream-Restore rief die go2rtc-API falsch auf. Verifiziert gegen die go2rtc-OpenAPI-Spec: PUT /api/streams erwartet src = Quelle (URI) und name = Stream-Name, beide als Query-Param. Der Code schickte aber src=cam0 (den Namen) und die Quelle im Body (den go2rtc ignoriert). Folge: cam0 wurde mit Quelle „cam0" = Selbstreferenz neu angelegt → kaputt → beim nächsten Verbindungsaufbau (Reload) schwarz. Fix: buildPutUrl()PUT /api/streams?name=cam0&src=<url-encoded-quelle>, kein Body. (DELETE ?src=cam0 war korrekt — DELETE nutzt src als Namen, API-Asymmetrie.)

  2. Hi-Res-Bild manchmal leer (~1KB schwarz) ✓ behoben Ursache: USB-Kamera liefert direkt nach Geräte-Öffnen unbelichtete Frames (Auto-Belichtung/Weissabgleich brauchen einen Moment). -frames:v 1 griff den ersten, schwarzen Frame. Fix: erste 15 Frames verwerfen (-vf select=gte(n,15)), dann einen greifen. Kostet ~1 s mehr Blackout.

Hinweis: Der hier beschriebene externe-FFmpeg-Grab (DELETE → eigener FFmpeg → PUT) wurde im zweiten Test verworfen — siehe nächster Abschnitt. Der PUT-Param-Fix (Bug 1) bleibt gültig (gleiche name+src-Konvention nutzt jetzt PATCH).

Zweiter Test (2026-06-04): externer Grab scheitert → Architektur-Pivot

Befund: Live-Video stabil ✓. Aber /hires liefert FFmpeg exit 1, kein Frame erhalten (curl: leeres 1KB-Bild). Video bleibt dabei durchgehend stabil.

Diagnose (belegt): Das Ausbleiben des Blackouts ist der Beweis. Der externe-Grab- Ansatz müsste das Video kurz schwarz schalten (DELETE stoppt den go2rtc-Producer). Es bleibt aber stabil → go2rtc gibt das Gerät nie frei: Der offene Live-Viewer reconnectet nach dem DELETE sofort, go2rtc startet den Producer per on-demand neu und greift /dev/video0 zurück, bevor der externe FFmpeg es öffnen kann → „device busy" → exit 1. Eine USB-Kamera lässt sich nur einmal öffnen — zwei Prozesse (go2rtc + eigener FFmpeg) können nicht gleichzeitig zugreifen, und der Live-Viewer lässt go2rtc immer gewinnen. Der Zwei-Prozess-Ansatz ist damit grundsätzlich falsch.

Ansatz ( ZERSTÖRTE DEN LIVE-STREAM → CPU 107%, zurückgerollt): go2rtc-interner Grab via PATCH. Idee war: go2rtc behält die Geräte-Hoheit, Node schaltet nur kurz dessen Quelle um. Warum es scheiterte: go2rtc's PATCH ersetzt die Quelle nicht, es hängt eine zweite an → cam0 lief gleichzeitig 640 UND 1280 → doppelte Encoder-Last → 107%. Details im Fehler-Log:

1. PATCH /api/streams?name=cam0&src=<1280×960-Quelle>   → go2rtc-Producer auf Hi-Res
2. ~1,2s warten (Producer-Start + Kamera-Belichtung)
3. GET /api/frame.jpeg?src=cam0  → Frame holen; nur akzeptieren wenn JPEG ≥1000px
   breit (sonst ist es noch der alte 640er); bis zu 6× alle 500ms retryen
4. PATCH /api/streams?name=cam0&src=<640×480-Quelle>    → zurück auf Live (immer, finally)

Nur ein Prozess (go2rtc) öffnet je das Gerät → keine Konkurrenz mehr möglich. Der Live-Viewer dieser einen Kamera glitcht ~34s (Producer-Restart + kurz 1280er Bild, vom Browser per CSS skaliert) — der vom Nutzer ausdrücklich akzeptierte „Blackout". Die zweite Kamera ist nicht betroffen. Umgesetzt in src/snapshotService.js (externer FFmpeg + captureOneFrame entfernt).


KONSOLIDIERT (2026-06-04) — maßgeblich

Nach mehreren Fehlversuchen der verbindliche Stand. Bei Widerspruch mit Abschnitten oben gilt dieser. Die Abschnitte oben sind als historischer Verlauf / Fehler-Record erhalten und mit markiert.

Aktueller stabiler Zustand (nicht ohne Grund anfassen)

Komponente Stand
go2rtc cam0/cam1 640×480 MJPEG, ~50% CPU (2 Kameras, mit Clients), keine Freezes, ~200ms Latenz
public/viewer.js MODE = 'mjpeg'
src/snapshotService.js nur Proxy auf /api/frame.jpeg (640er-Snapshot). Kein /hires
Hi-Res-Snapshot derzeit NICHT vorhanden — bewusst entfernt nach dem CPU-Vorfall

Zur Wiederherstellung nach dem Vorfall: docker restart AppRobotGo2RTC (lädt 640-Config neu) + docker restart AppRobotWebcam (lädt zurückgesetzten Code).

Fehler-Log — was ich falsch gemacht habe (NICHT wiederholen)

# Fehler Was passierte Lektion
1 „Quelle = MJPEG ⇒ <5% CPU" angenommen War nie Passthrough: go2rtc re-encodiert MJPEG (~50%); im WebRTC-Modus transcodierte es sogar zu H.264 (~127%) „Source MJPEG" ≠ „kein Encoding". Mit echtem Consumer messen, nicht idle.
2 Externer Grab: PUT mit Quelle im Body go2rtc liest die Quelle aus dem src-Query-Param, nicht aus dem Body → cam0 als Selbstreferenz → schwarz nach Reload go2rtc-API-Verhalten verifizieren statt raten. Richtig: PUT ?name=…&src=….
3 Externer FFmpeg parallel zu go2rtc auf demselben Gerät „device busy" → exit 1: Live-Viewer reconnectet, go2rtc greift das Gerät zurück, bevor der externe FFmpeg es öffnen kann Eine USB-Kamera = ein Öffner. Zwei Prozesse können sie nicht teilen.
4 PATCH zum Umschalten der Live-Quelle (auf laufendem cam0) go2rtc's PATCH ersetzt nicht — es hängt an → cam0 lief 640 und 1280 gleichzeitig → CPU 107%, Live-Stream beschädigt NIE den Live-Producer im Betrieb mutieren, ohne das Verhalten vorher auf einem Wegwerf-Stream getestet zu haben.
5 „Verifiziert" behauptet, obwohl nur Syntax + JPEG-Parser geprüft Die tragende Annahme (PATCH-Verhalten) war ungeprüft — und wurde trotzdem auf cam0 ausgeliefert „Peripherie geprüft" ≠ „die sicherheitskritische Annahme geprüft".
6 #video=copy (Passthrough) auf cam0 getestet, Vorhersage „senkt CPU" Gegenteil: 50% → 107% (Grund ungeklärt). cam0 zeigte Bild, kein „mjpeg eof" → Producer lief, nur teurer Vorhersagen sind keine Daten. #video=copy ist auf dieser Kamera empirisch tot.
7 Diese 107% vorschnell als „WebRTC-Transcode" gedeutet Falsch — Viewer stand auf „MJPEG live". Wieder geraten statt den Fakt zu holen Bei unerwartetem Ergebnis erst messen was wirklich läuft, keine Story erfinden.

Eiserne Regeln (daraus)

  1. Der Snapshot-Pfad ist READ-ONLY gegenüber go2rtc. Nur GET /api/frame.jpeg. Niemals PUT/PATCH/DELETE auf cam0/cam1 im laufenden Betrieb.
  2. Laufzeit-API-Mutationen (PATCH/PUT/DELETE) auf cam0/cam1 sind verboten. go2rtc-API-Verhalten zuerst auf einem Wegwerf-Stream verifizieren. Eine reine Config-Änderung + Redeploy an einer echten Kamera ist dagegen ok, wenn (a) eine Rollback-Zeile in der Datei steht und (b) die andere Kamera auf dem bekannten guten Stand bleibt — so wurde der Copy-Test gefahrlos und reversibel gemacht.
  3. „Verifiziert" = die sicherheitskritische Annahme wurde getestet — nicht nur die Syntax drumherum.
  4. Der Live-Stream hat absolute Priorität. Im Zweifel lieber kein Feature als ein wackliger Live-Stream.

Plan für Hi-Res-Snapshots — und wie sicher ich bin

Harte Randbedingung: Eine USB-Kamera lässt sich nur einmal, in einer Auflösung, öffnen. Hi-Res muss daher entweder von einer separaten Kamera kommen (Weg A) oder aus demselben Producer, der ohnehin hochauflösend läuft (Weg C). Das On-Demand-Umschalten der einen Live-Kamera hat sich als gefährlich erwiesen (Fehler 4).

Ja — ich bin sicher, dass es lösbar ist. Garantiert über mindestens einen Weg:

Weg A — separate Hi-Res-Kamera(s). GARANTIERT sicher. Eine zusätzliche USB-Kamera, die go2rtc nicht öffnet. Node greift sie on-demand mit einem one-shot FFmpeg ab. Da es ein anderes Gerät ist, kann das den Live-Stream physikalisch nicht stören. Kosten: Hardware + Montage + USB-Bandbreite (Hi-Res-Kamera am besten an eigenem USB-Controller, damit der kurze Grab die Live-Kameras nicht drosselt). → Die einzige Lösung, die ich zu 100 % garantieren kann.

Weg C — Live dauerhaft 1280×960, Browser skaliert auf 640, Snapshot = read-only frame.jpeg (dann schon 1280). → VERWORFEN (getestet). Der Snapshot-Mechanismus wäre sicher (read-only). Der Preis hing daran, ob go2rtc echtes Passthrough kann. Getestet 2026-06-04 (config-basiert auf cam0, reversibel, cam1 als Sicherheitsnetz): #video=copy machte die CPU schlechter (50% → 107%), nicht besser — Grund ungeklärt (cam0 zeigte Bild, kein „mjpeg eof", Producer lief, nur teurer). Damit ist die billige Variante tot; bliebe nur 1280-Re-Encode (53 % / 127 % — für Dauerbetrieb zu teuer). Weg C ist kein gangbarer Weg.

Ebenfalls ausgeschlossen: On-Demand-Umschalten der einen Live-Kamera zwischen 640 und 1280. Über PATCH hat es den Live-Stream zerstört (107%); ein „sauberes" Replace (DELETE+PUT) bliebe riskant. Kein Versprechen, wird nicht weiterverfolgt.

Fazit & Empfehlung (2026-06-04, Stand nach allen Tests)

  • Live-Stream: fertig und stabil. 640×480 MJPEG, ~50 % CPU, keine Freezes, ~200 ms. Dabei bleiben — nicht ohne konkreten Grund anfassen.
  • Hi-Res billig & ohne Hardware: ausgeschlossen. Copy-Passthrough getestet → schlechter (Weg C tot). Umschalten der einen Kamera → gefährlich (107%-Vorfall). 1280-Dauerbetrieb → zu teuer.
  • Verlässliches Hi-Res ⇒ Weg A (separate Kamera). Einzige Lösung, die den Live-Stream physisch nicht berühren kann.
  • Kein Hardware-Budget ⇒ vorerst kein Hi-Res. Beim stabilen 640er-Snapshot (/api/snapshot/cam{n}) bleiben.

Weg A konkret, wenn es so weit ist: separate USB-Kamera, möglichst an eigenem USB-Controller; in go2rtc nicht als Stream einbinden; Node greift sie on-demand per one-shot FFmpeg ab, z. B.: ffmpeg -f v4l2 -input_format mjpeg -video_size 1280x960 -i /dev/videoN -frames:v 1 -q:v 2 out.jpg. Getrenntes Gerät + read-only gegenüber go2rtc → kann mit dem Live-Stream nicht kollidieren.