Claude: WebSocket

This commit is contained in:
chk
2026-06-04 15:06:45 +02:00
parent 118441995d
commit 306aacac80
4 changed files with 378 additions and 52 deletions

View File

@@ -132,6 +132,16 @@ 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)
```yaml
@@ -151,6 +161,47 @@ Kamera liefert MJPEG nativ → go2rtc reicht es 1:1 durch → kein Encoding →
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 ist die Kette durchgängig MJPEG: **Kamera → go2rtc (copy) → Browser.** Kein Encoder.
```
CPU ~0% · keine Freezes · ~200ms Latenz · skaliert auf mehr Kameras
```
go2rtc-Quelle bleibt 640×480 `#video=mjpeg`. **Hardware-Encoding ist damit
gegenstandslos** — es wird gar nicht mehr encodiert. Der ganze VAAPI-Strang unten
ist nur noch relevant, falls später doch WebRTC-Latenz (~130ms) zwingend gebraucht wird.
---
### Falls doch noch H.264 gewünscht (mit korrektem VAAPI)
Erfordert MediaMTX als Zwischenstufe:
@@ -175,38 +226,150 @@ Aufwand: ~2h (zusätzlicher Container, RTSP-Verkabelung). Lohnt sich erst wenn
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%.
Ursache unklar (vermutlich MJPEG-Frames 4× grösser → mehr I/O-Last in go2rtc).
**Zurückgesetzt auf stabilen Zustand: 640x480 @ 30fps, ~20% CPU.**
**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.**
### Drei Optionen (noch nicht umgesetzt)
Zwingend vor jedem Auflösungstest:
```bash
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 im Browser**
- Stream auf 1280x720 oder 1280x960 setzen
- Browser zeigt 640x480 (CSS), Snapshot = volle Auflösung
- Problem: CPU-Last beim Hochskalieren steigt (s.o.)
- Lösung: erst `v4l2-ctl --list-formats-ext -d /dev/video0` prüfen welche
MJPEG-Auflösungen die Kamera nativ unterstützt. Dann schrittweise testen:
640x480 → 1280x720 → 1280x960. CPU nach jedem Schritt messen.
- Zeitaufwand: 30 min
---
**Option 2Stream stoppen, Snapshot, Stream starten**
- Node.js orchestriert: go2rtc-Stream stoppen → FFmpeg einmalig
`-frames:v 1` bei maximaler Auflösung → Bild speichern → Stream neu starten
- Video-Blackout: ~12 Sekunden
- CPU-Peak: kurz, dann zurück auf normal
- Aufwand: ~2h (Node.js Orchestrierungslogik + go2rtc Stream-API)
- Geeignet wenn Snapshots nur alle 1030s gebraucht werden
### Option 1Hi-Res-Stream + CSS-Skalierung (30 min, zuerst testen)
- `v4l2-ctl` prüfen (s.o.)
- Wenn 1280×720 als MJPEG nativ: `video_size=640x480``video_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**
```yaml
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):
```javascript
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):
```javascript
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 (verifiziert):
- `DELETE /api/streams?src={name}` → stoppt Producer, gibt Device frei
- `PUT /api/streams?src={name}` mit Body = Stream-URL → startet Producer neu
**3. Mutex (concurrent requests verhindern)**
```javascript
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
**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
- Sauberste Lösung langfristig, aber Hardware-Investment
### Empfehlung
---
Option 1 zuerst, aber schrittweise mit CPU-Messung pro Auflösungsstufe.
Option 2 wenn Blackout akzeptabel und Option 1 zu viel CPU braucht.
Option 3 wenn Homing-Qualität kritisch und Budget vorhanden.
### 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: Option 2 (Blackout-Snapshot) ✓ (implementiert)**
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`.

View File

@@ -17,73 +17,69 @@ name: approbotwebcam
# Zugriff:
# Viewer: http://<host>:8444/
# Snapshot (Homing) http://<host>:8444/api/snapshot/cam0
# Hi-Res Snapshot http://<host>:8444/api/snapshot/cam0/hires
# go2rtc-Debug-UI http://<host>:1984/ (nur intern/LAN)
# ════════════════════════════════════════════════════════════════════════════
configs:
go2rtc_yaml:
# Komplette go2rtc-Config eingebettet keine separate Datei nötig.
content: |
streams:
# MJPEG-Passthrough: Kamera liefert MJPEG nativ → go2rtc reicht es 1:1 durch.
# Kein Encoding, kein libx264, kein VAAPI → CPU <5%, keine Freezes.
# Latenz ~200ms (vs. 130ms bei H.264) — für Roboter-Überwachung ausreichend.
# Hinweis: go2rtc's #hardware funktioniert NICHT mit MJPEG-Kamera-Input
# (hwupload benötigt VAAPI-Decoder auf Input-Seite, MJPEG läuft Software).
# 640x480 @ 30fps stabiler Live-Stream, <5% CPU, ~200ms Latenz.
# Hi-Res-Snapshots über /api/snapshot/cam{n}/hires (Node.js Blackout-Methode).
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"
webrtc:
listen: ":8555"
candidates:
# stun:8555 → go2rtc erkennt die öffentliche IP automatisch (für Internet).
# Falls das nicht klappt: feste IP/Domain eintragen, z.B.
# - robot.example.com:8555
- stun:8555
api:
listen: ":1984"
# origin "*" erlaubt das WebSocket-Signaling vom Viewer (Port 8444 = anderer Origin).
# Ohne diese Zeile blockt go2rtc den WS mit "request origin not allowed".
# LAN: unkritisch. Internet: Caddy davor schränkt den Zugriff wieder ein.
origin: "*"
log:
level: info
# On-demand bestätigt: go2rtc startet Encoder erst bei erstem Client (0% CPU ohne Client).
services:
# ── go2rtc: Kamera-Capture · H.264-Encoding · WebRTC ──────────────────────
# ── go2rtc: Kamera-Capture · MJPEG-Passthrough · Streaming ────────────────
go2rtc:
image: ghcr.io/alexxit/go2rtc
container_name: AppRobotGo2RTC
restart: unless-stopped
network_mode: host # echte Host-IP als WebRTC-ICE-Kandidat
network_mode: host
devices:
- /dev/video0:/dev/video0
- /dev/video2:/dev/video2
# /dev/dri nicht mehr nötig: MJPEG-Passthrough braucht keine GPU
group_add:
- video
# render-Gruppe NICHT hier setzen — existiert im Container-Image nicht → 500-Fehler.
# /dev/dri-Zugriff funktioniert via devices: + Container läuft als root.
configs:
- source: go2rtc_yaml
target: /config/go2rtc.yaml
# ── webcam: Node.js (Viewer · /api/ws-Proxy · Snapshot-API) ──────────────
# ffmpeg ist im Image damit der /hires-Endpunkt direkt auf das Gerät zugreifen
# kann, wenn go2rtc den Stream kurz freigibt (Blackout-Snapshot-Methode).
webcam:
build:
context: /tmp # Leerer Build-Context Code kommt per Bind-Mount
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
container_name: AppRobotWebcam
restart: unless-stopped
network_mode: host # erreicht go2rtc via localhost:1984
network_mode: host
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
@@ -98,5 +94,5 @@ services:
# - ${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=h264#video=mjpeg"
# simple, bestätigte Form (ohne Auflösung): "ffmpeg:/dev/video0#video=mjpeg"
# ────────────────────────────────────────────────────────────────────────────────

View File

@@ -1,7 +1,13 @@
'use strict';
// go2rtc Player-Modi Fallback-Reihenfolge: WebRTC → MSE → MJPEG
const MODE = 'webrtc,mse,mjpeg';
// go2rtc Player-Modi.
// 'mjpeg' → Passthrough: go2rtc reicht Kamera-MJPEG 1:1 durch.
// KEIN Transcode → ~0% CPU, keine Freezes, ~200ms Latenz.
// 'webrtc,mse,mjpeg' → WebRTC bevorzugt. ABER: WebRTC/MSE können kein MJPEG →
// go2rtc transcodiert MJPEG→H.264 in Software (libx264) →
// ~55% CPU pro Kamera + Keyframe-Freezes. ~130ms Latenz.
const MODE = 'mjpeg';
const IS_MJPEG = !MODE.includes('webrtc'); // MJPEG-Modus: kein WebRTC/getStats/Health
// ── Überwachungs-Parameter ───────────────────────────────────────────────────
const MONITOR_INTERVAL = 2500; // ms zwischen Health-Checks (getStats)
@@ -88,6 +94,14 @@ async function monitor() {
for (const cam of cameras) {
if (!cam.active) continue;
// MJPEG-Passthrough: kein PeerConnection, kein getStats, keine Decoder-Überlast.
// Das Bild läuft, sobald das Element da ist nur simple Status-Anzeige.
if (IS_MJPEG) {
const live = !!cam.box.querySelector('video-stream');
setInfo(cam, live ? 'MJPEG · live' : 'aus', live ? 'ok' : '');
continue;
}
const el = cam.box.querySelector('video-stream');
const pc = el && el.pc;
// Noch nicht verbunden oder 'playing' noch nicht gefeuert → Aufbauphase

View File

@@ -1,28 +1,56 @@
'use strict';
const express = require('express');
const express = require('express');
const { spawn } = require('child_process');
// ── Kamera-Konfiguration ──────────────────────────────────────────────────────
// Muss zur go2rtc-Config in docker-compose.yaml passen.
const CAM_CONFIG = {
cam0: {
device: '/dev/video0',
hiresSize: '1280x960',
streamUrl: 'ffmpeg:device?video=/dev/video0&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg',
},
cam1: {
device: '/dev/video2',
hiresSize: '1280x960',
streamUrl: 'ffmpeg:device?video=/dev/video2&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg',
},
};
// Stabile Snapshot-Schnittstelle für das Homing-Projekt.
// Entkoppelt den Consumer von go2rtc-Interna proxied intern auf /api/frame.jpeg.
//
// GET /api/snapshot → JSON-Liste der Kameras (aus go2rtc /api/streams)
// GET /api/snapshot/cam0 → aktuelles JPEG (aus go2rtc /api/frame.jpeg?src=cam0)
// GET /api/snapshot → JSON-Liste der Kameras
// GET /api/snapshot/cam0 → aktueller Frame (640×480, go2rtc passthrough)
// GET /api/snapshot/cam0/hires → einmaliger Hi-Res-Frame (1280×960, Blackout ~12 s)
//
// Hi-Res-Ablauf:
// 1. go2rtc-Stream temporär löschen → Gerät wird freigegeben
// 2. FFmpeg one-shot direkt auf /dev/videoX → 1280×960 MJPEG
// 3. go2rtc-Stream wiederherstellen → Live-Video läuft wieder
//
function createSnapshotRouter(go2rtcUrl) {
const router = express.Router();
// ── Kamera-Liste ─────────────────────────────────────────────────────────────
router.get('/', async (_req, res) => {
try {
const r = await fetch(`${go2rtcUrl}/api/streams`);
if (!r.ok) throw new Error(`go2rtc HTTP ${r.status}`);
const streams = await r.json();
res.json({
cameras: Object.keys(streams).map(id => ({ id, url: `/api/snapshot/${id}` })),
cameras: Object.keys(streams).map(id => ({
id,
url: `/api/snapshot/${id}`,
hiresUrl: `/api/snapshot/${id}/hires`,
})),
});
} catch (err) {
res.status(503).json({ error: `go2rtc nicht erreichbar: ${err.message}` });
}
});
// ── Standard-Snapshot (Stream-Auflösung, sofort) ─────────────────────────────
router.get('/:id', async (req, res) => {
const { id } = req.params;
try {
@@ -46,7 +74,132 @@ function createSnapshotRouter(go2rtcUrl) {
}
});
// ── Hi-Res-Snapshot (Blackout ~12 s, 1280×960) ──────────────────────────────
let hiresLock = false;
router.get('/:id/hires', async (req, res) => {
const { id } = req.params;
const cfg = CAM_CONFIG[id];
if (!cfg) {
return res.status(404).json({ error: `Kamera '${id}' nicht in CAM_CONFIG` });
}
if (hiresLock) {
return res.status(429).json({ error: 'Hi-Res-Snapshot läuft bereits bitte warten' });
}
hiresLock = true;
console.log(`[snapshot][${id}] hires-Start (${cfg.hiresSize})`);
try {
// 1. go2rtc-Stream stoppen → gibt /dev/videoX frei
const delRes = await fetch(
`${go2rtcUrl}/api/streams?src=${encodeURIComponent(id)}`,
{ method: 'DELETE' }
);
console.log(`[snapshot][${id}] go2rtc DELETE stream → HTTP ${delRes.status}`);
// kurz warten bis FFmpeg-Prozess in go2rtc beendet und Gerät freigegeben ist
await sleep(900);
// 2. Hi-Res-Frame via FFmpeg one-shot
const jpeg = await captureOneFrame(cfg.device, cfg.hiresSize);
console.log(`[snapshot][${id}] Frame captured (${jpeg.length} bytes)`);
// 3. go2rtc-Stream wiederherstellen
const putRes = await fetch(
`${go2rtcUrl}/api/streams?src=${encodeURIComponent(id)}`,
{
method: 'PUT',
headers: { 'Content-Type': 'text/plain' },
body: cfg.streamUrl,
}
);
console.log(`[snapshot][${id}] go2rtc PUT stream → HTTP ${putRes.status}`);
res.set({
'Content-Type': 'image/jpeg',
'Content-Length': jpeg.length,
'Cache-Control': 'no-store',
'X-Camera-Id': id,
'X-Resolution': cfg.hiresSize,
'X-Timestamp': new Date().toISOString(),
});
res.end(jpeg);
} catch (err) {
console.error(`[snapshot][${id}] hires-Fehler:`, err.message);
// Stream auf jeden Fall wiederherstellen, auch im Fehlerfall
try {
await fetch(`${go2rtcUrl}/api/streams?src=${encodeURIComponent(id)}`, {
method: 'PUT',
headers: { 'Content-Type': 'text/plain' },
body: cfg.streamUrl,
});
console.log(`[snapshot][${id}] Stream nach Fehler wiederhergestellt`);
} catch (restoreErr) {
console.error(`[snapshot][${id}] Stream-Wiederherstellung fehlgeschlagen:`, restoreErr.message);
}
if (!res.headersSent) {
res.status(500).json({ error: err.message });
}
} finally {
hiresLock = false;
}
});
return router;
}
// ── Hilfsfunktionen ───────────────────────────────────────────────────────────
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
// Startet FFmpeg, liest genau einen MJPEG-Frame aus stdout, gibt ihn als Buffer zurück.
function captureOneFrame(device, size, timeoutMs = 8000) {
return new Promise((resolve, reject) => {
const args = [
'-hide_banner', '-loglevel', 'error',
'-f', 'v4l2',
'-input_format', 'mjpeg',
'-video_size', size,
'-framerate', '10', // niedrige FPS → schnellerer erster Frame
'-i', device,
'-frames:v', '1',
'-q:v', '1', // beste JPEG-Qualität
'-f', 'mjpeg',
'pipe:1',
];
const chunks = [];
const proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'] });
proc.stdout.on('data', chunk => chunks.push(chunk));
proc.stderr.on('data', () => {}); // FFmpeg-Infos unterdrücken (loglevel error)
const timer = setTimeout(() => {
proc.kill('SIGKILL');
reject(new Error(`FFmpeg timeout nach ${timeoutMs}ms`));
}, timeoutMs);
proc.on('close', code => {
clearTimeout(timer);
const buf = Buffer.concat(chunks);
if (buf.length > 0) {
resolve(buf);
} else {
reject(new Error(`FFmpeg exit ${code}, kein Frame erhalten`));
}
});
proc.on('error', err => {
clearTimeout(timer);
reject(err);
});
});
}
module.exports = { createSnapshotRouter };