Claude: ScreenShot v2
This commit is contained in:
@@ -395,16 +395,51 @@ Zwei Bugs gefunden und sofort behoben:
|
|||||||
ersten, schwarzen Frame. Fix: erste 15 Frames verwerfen
|
ersten, schwarzen Frame. Fix: erste 15 Frames verwerfen
|
||||||
(`-vf select=gte(n,15)`), dann einen greifen. Kostet ~1 s mehr Blackout.
|
(`-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.
|
||||||
|
|
||||||
|
**Lösung (umgesetzt): go2rtc-interner Hi-Res-Grab — kein zweiter Prozess.**
|
||||||
|
go2rtc behält die Geräte-Hoheit. Node schaltet nur kurz dessen Quelle um:
|
||||||
|
|
||||||
|
```
|
||||||
|
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 ~3–4s (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).
|
||||||
|
|
||||||
### Offene Punkte (ToDo)
|
### Offene Punkte (ToDo)
|
||||||
|
|
||||||
- **go2rtc-CPU ~53% bei 2 aktiven Live-Streams.** Besser als H.264-Transcode (~127%),
|
- **go2rtc-CPU ~50% bei 2 aktiven Live-Streams.** Besser als H.264-Transcode (~127%),
|
||||||
aber kein echtes Null. go2rtc re-encodiert MJPEG→MJPEG (kein `-c:v copy`) statt
|
aber kein echtes Null. go2rtc re-encodiert MJPEG→MJPEG (kein `-c:v copy`) statt
|
||||||
reinem Durchreichen. Das sind ~0,5 CPU-Kerne für 2 Kameras → stabil und unkritisch
|
reinem Durchreichen. ~0,5 CPU-Kerne für 2 Kameras → stabil und unkritisch auf dieser
|
||||||
auf dieser Maschine. **Optionaler Hebel falls je nötig:** prüfen ob go2rtc-Quelle
|
Maschine. **Optionaler Hebel falls je nötig:** prüfen ob go2rtc-Quelle auf echtes
|
||||||
auf echtes Copy/Passthrough umstellbar ist. Risiko: Stabilität des laufenden,
|
Copy/Passthrough umstellbar ist. Risiko: Stabilität des laufenden Streams — nur
|
||||||
funktionierenden Streams — daher nur anfassen wenn CPU real zum Problem wird.
|
anfassen wenn CPU real zum Problem wird.
|
||||||
- **Geräte-Race bei Hi-Res mit gleichzeitig offenem Live-Tab.** Ist ein Live-Consumer
|
- **Cleanup (unkritisch):** Der webcam-Container braucht jetzt **kein** `ffmpeg` und
|
||||||
aktiv, kann go2rtc das Gerät nach dem DELETE per on-demand-Reconnect sofort wieder
|
**keine** `devices`/`group_add: video` mehr (kein externer Grab). Kann beim nächsten
|
||||||
greifen und mit dem Hi-Res-Grab kollidieren. Warmup + Frame-Verwerfen fängt das
|
bewussten Aufräumen aus `docker-compose.yaml` raus — aktuell harmlos (nur ungenutzt).
|
||||||
meist ab. Falls doch leere Bilder auftreten: kurzer Retry im Grab, oder Live-Tab
|
- **Falls der PATCH-Restart je hakt** (frame.jpeg bleibt zu klein/640): Warmup-Zeit
|
||||||
vor dem Hi-Res-Klick kurz pausieren.
|
oder Retry-Anzahl in `snapshotService.js` erhöhen (`HIRES_WARMUP_MS`, `HIRES_TRIES`).
|
||||||
|
|||||||
@@ -1,34 +1,46 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { spawn } = require('child_process');
|
|
||||||
|
|
||||||
// ── Kamera-Konfiguration ──────────────────────────────────────────────────────
|
// ── Kamera-Konfiguration ──────────────────────────────────────────────────────
|
||||||
// Muss zur go2rtc-Config in docker-compose.yaml passen.
|
// liveUrl MUSS exakt der go2rtc-Config (docker-compose.yaml) entsprechen, damit
|
||||||
|
// nach dem Hi-Res-Grab der Live-Stream identisch wiederhergestellt wird.
|
||||||
|
// hiresUrl gleiche Kamera, nur höhere Auflösung – wird NUR kurz für den Snapshot
|
||||||
|
// aktiviert.
|
||||||
const CAM_CONFIG = {
|
const CAM_CONFIG = {
|
||||||
cam0: {
|
cam0: {
|
||||||
device: '/dev/video0',
|
device: '/dev/video0',
|
||||||
hiresSize: '1280x960',
|
liveUrl: 'ffmpeg:device?video=/dev/video0&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg',
|
||||||
streamUrl: 'ffmpeg:device?video=/dev/video0&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg',
|
hiresUrl: 'ffmpeg:device?video=/dev/video0&input_format=mjpeg&video_size=1280x960&framerate=15#video=mjpeg',
|
||||||
},
|
},
|
||||||
cam1: {
|
cam1: {
|
||||||
device: '/dev/video2',
|
device: '/dev/video2',
|
||||||
hiresSize: '1280x960',
|
liveUrl: 'ffmpeg:device?video=/dev/video2&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg',
|
||||||
streamUrl: 'ffmpeg:device?video=/dev/video2&input_format=mjpeg&video_size=640x480&framerate=30#video=mjpeg',
|
hiresUrl: 'ffmpeg:device?video=/dev/video2&input_format=mjpeg&video_size=1280x960&framerate=15#video=mjpeg',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Hi-Res-Grab-Parameter
|
||||||
|
const HIRES_MIN_WIDTH = 1000; // akzeptiere nur Frames ≥1000px breit (sonst noch der 640er)
|
||||||
|
const HIRES_WARMUP_MS = 1200; // Producer-Start + Kamera-Belichtung abwarten
|
||||||
|
const HIRES_TRIES = 6; // so oft frame.jpeg pollen
|
||||||
|
const HIRES_GAP_MS = 500; // Pause zwischen den Versuchen
|
||||||
|
|
||||||
// Stabile Snapshot-Schnittstelle für das Homing-Projekt.
|
// Stabile Snapshot-Schnittstelle für das Homing-Projekt.
|
||||||
//
|
//
|
||||||
// GET /api/snapshot → JSON-Liste der Kameras
|
// GET /api/snapshot → JSON-Liste der Kameras
|
||||||
// GET /api/snapshot/cam0 → aktueller Frame (640×480, go2rtc passthrough)
|
// GET /api/snapshot/cam0 → aktueller Frame in Live-Auflösung (640×480)
|
||||||
// GET /api/snapshot/cam0/hires → einmaliger Hi-Res-Frame (1280×960, Blackout ~1–2 s)
|
// GET /api/snapshot/cam0/hires → einmaliger Hi-Res-Frame (1280×960)
|
||||||
//
|
//
|
||||||
// Hi-Res-Ablauf:
|
// Hi-Res-Ablauf (go2rtc-intern, KEIN zweiter Prozess auf dem Gerät):
|
||||||
// 1. go2rtc-Stream temporär löschen → Gerät wird freigegeben
|
// 1. go2rtc-Quelle per PATCH kurz auf 1280×960 umschalten
|
||||||
// 2. FFmpeg one-shot direkt auf /dev/videoX → 1280×960 MJPEG
|
// 2. Producer-Start + Belichtung abwarten
|
||||||
// 3. go2rtc-Stream wiederherstellen → Live-Video läuft wieder
|
// 3. Frame über go2rtc /api/frame.jpeg holen (nur akzeptieren wenn wirklich 1280)
|
||||||
|
// 4. Quelle per PATCH zurück auf 640×480 (immer, auch im Fehlerfall)
|
||||||
//
|
//
|
||||||
|
// Warum so: Eine USB-Kamera kann nur EINMAL geöffnet werden. Ein externer FFmpeg
|
||||||
|
// würde mit go2rtc um das Gerät konkurrieren (go2rtc gewinnt durch den Live-Viewer
|
||||||
|
// → "device busy"). Indem nur go2rtc das Gerät hält, gibt es keine Konkurrenz.
|
||||||
function createSnapshotRouter(go2rtcUrl) {
|
function createSnapshotRouter(go2rtcUrl) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -50,7 +62,63 @@ function createSnapshotRouter(go2rtcUrl) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Standard-Snapshot (Stream-Auflösung, sofort) ─────────────────────────────
|
// ── Hi-Res-Snapshot (1280×960, go2rtc-intern) ────────────────────────────────
|
||||||
|
// Vor /:id registrieren ist nicht nötig (andere Pfadtiefe), aber explizit sauber.
|
||||||
|
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 → 1280×960`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. go2rtc-Quelle auf Hi-Res umschalten (go2rtc behält die Geräte-Hoheit)
|
||||||
|
const p1 = await fetch(streamApiUrl(go2rtcUrl, id, cfg.hiresUrl), { method: 'PATCH' });
|
||||||
|
console.log(`[snapshot][${id}] PATCH → hires: HTTP ${p1.status}`);
|
||||||
|
|
||||||
|
// 2. Producer-Start + Kamera-Belichtung abwarten
|
||||||
|
await sleep(HIRES_WARMUP_MS);
|
||||||
|
|
||||||
|
// 3. Frame holen, bis er wirklich in Hi-Res ankommt
|
||||||
|
const jpeg = await grabHiresFrame(go2rtcUrl, id);
|
||||||
|
const width = jpegWidth(jpeg);
|
||||||
|
console.log(`[snapshot][${id}] Frame ${jpeg.length} bytes, ${width}px breit`);
|
||||||
|
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'image/jpeg',
|
||||||
|
'Content-Length': jpeg.length,
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
'X-Camera-Id': id,
|
||||||
|
'X-Resolution': `${width}px`,
|
||||||
|
'X-Timestamp': new Date().toISOString(),
|
||||||
|
});
|
||||||
|
res.end(jpeg);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[snapshot][${id}] hires-Fehler:`, err.message);
|
||||||
|
if (!res.headersSent) res.status(500).json({ error: err.message });
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// 4. IMMER zurück auf Live-Auflösung – auch bei Fehler
|
||||||
|
try {
|
||||||
|
const p2 = await fetch(streamApiUrl(go2rtcUrl, id, cfg.liveUrl), { method: 'PATCH' });
|
||||||
|
console.log(`[snapshot][${id}] PATCH → live zurück: HTTP ${p2.status}`);
|
||||||
|
} catch (restoreErr) {
|
||||||
|
console.error(`[snapshot][${id}] Live-Wiederherstellung fehlgeschlagen:`, restoreErr.message);
|
||||||
|
}
|
||||||
|
hiresLock = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Standard-Snapshot (Live-Auflösung, sofort) ───────────────────────────────
|
||||||
router.get('/:id', async (req, res) => {
|
router.get('/:id', async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
try {
|
try {
|
||||||
@@ -74,72 +142,6 @@ function createSnapshotRouter(go2rtcUrl) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Hi-Res-Snapshot (Blackout ~1–2 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.
|
|
||||||
// go2rtc-API: PUT /api/streams?name=<stream>&src=<quelle-url-encoded>
|
|
||||||
// Quelle steht im `src`-Query-Param (URL-encoded), NICHT im Body.
|
|
||||||
const putRes = await fetch(buildPutUrl(go2rtcUrl, id, cfg.streamUrl), { method: 'PUT' });
|
|
||||||
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(buildPutUrl(go2rtcUrl, id, cfg.streamUrl), { method: 'PUT' });
|
|
||||||
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;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,62 +151,59 @@ function sleep(ms) {
|
|||||||
return new Promise(r => setTimeout(r, ms));
|
return new Promise(r => setTimeout(r, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
// go2rtc-API zum (Wieder-)Anlegen eines Streams:
|
// go2rtc-API zum Ändern der Quelle eines bestehenden Streams:
|
||||||
// PUT /api/streams?name=<stream-name>&src=<quelle-uri-url-encoded>
|
// PATCH /api/streams?name=<stream>&src=<quelle-uri-url-encoded>
|
||||||
// Beide Werte als Query-Param. `src` ist die QUELLE (nicht der Name) — go2rtc
|
// `src` ist die QUELLE (URL-encoded), `name` der Stream-Name. Kein Body.
|
||||||
// liest sie NICHT aus dem Body.
|
function streamApiUrl(go2rtcUrl, name, srcUrl) {
|
||||||
function buildPutUrl(go2rtcUrl, name, streamUrl) {
|
|
||||||
return `${go2rtcUrl}/api/streams`
|
return `${go2rtcUrl}/api/streams`
|
||||||
+ `?name=${encodeURIComponent(name)}`
|
+ `?name=${encodeURIComponent(name)}`
|
||||||
+ `&src=${encodeURIComponent(streamUrl)}`;
|
+ `&src=${encodeURIComponent(srcUrl)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Startet FFmpeg, liest genau einen MJPEG-Frame aus stdout, gibt ihn als Buffer zurück.
|
// Holt von go2rtc so lange frame.jpeg, bis ein echter Hi-Res-Frame (≥HIRES_MIN_WIDTH
|
||||||
function captureOneFrame(device, size, timeoutMs = 8000) {
|
// breit) ankommt. Verhindert, dass noch der zwischengespeicherte 640er-Frame oder
|
||||||
return new Promise((resolve, reject) => {
|
// ein schwarzer Warmup-Frame zurückgegeben wird.
|
||||||
const args = [
|
async function grabHiresFrame(go2rtcUrl, id) {
|
||||||
'-hide_banner', '-loglevel', 'error',
|
let best = null;
|
||||||
'-f', 'v4l2',
|
for (let i = 0; i < HIRES_TRIES; i++) {
|
||||||
'-input_format', 'mjpeg',
|
try {
|
||||||
'-video_size', size,
|
const r = await fetch(`${go2rtcUrl}/api/frame.jpeg?src=${encodeURIComponent(id)}`);
|
||||||
'-framerate', '15',
|
if (r.ok) {
|
||||||
'-i', device,
|
const buf = Buffer.from(await r.arrayBuffer());
|
||||||
// Erste ~15 Frames verwerfen: die USB-Kamera liefert direkt nach dem Öffnen
|
if (jpegWidth(buf) >= HIRES_MIN_WIDTH) return buf; // echter Hi-Res-Frame
|
||||||
// noch unbelichtete (schwarze) Frames – Auto-Belichtung/Weissabgleich brauchen
|
if (!best || buf.length > best.length) best = buf; // bestes Fallback merken
|
||||||
// einen Moment. Ohne das kommt das "1KB leer/schwarz"-Bild.
|
|
||||||
'-vf', 'select=gte(n\\,15)',
|
|
||||||
'-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`));
|
|
||||||
}
|
}
|
||||||
});
|
} catch { /* Netzfehler → retry */ }
|
||||||
|
await sleep(HIRES_GAP_MS);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`kein ${HIRES_MIN_WIDTH}px-Frame nach ${HIRES_TRIES} Versuchen ` +
|
||||||
|
`(PATCH→hires erfolgreich? Producer-Restart langsam? Warmup erhöhen)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
proc.on('error', err => {
|
// Liest die Bildbreite aus den JPEG-Headern (SOF-Marker) – ohne externe Library.
|
||||||
clearTimeout(timer);
|
// Gibt 0 zurück, wenn nichts gefunden wird.
|
||||||
reject(err);
|
function jpegWidth(buf) {
|
||||||
});
|
if (!buf || buf.length < 4 || buf[0] !== 0xff || buf[1] !== 0xd8) return 0;
|
||||||
});
|
let i = 2;
|
||||||
|
while (i + 9 < buf.length) {
|
||||||
|
if (buf[i] !== 0xff) { i++; continue; }
|
||||||
|
let marker = buf[i + 1];
|
||||||
|
while (marker === 0xff && i + 1 < buf.length) { i++; marker = buf[i + 1]; } // Füllbytes
|
||||||
|
// Standalone-Marker ohne Längenfeld
|
||||||
|
if (marker === 0xd8 || marker === 0xd9 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0x01) {
|
||||||
|
i += 2; continue;
|
||||||
|
}
|
||||||
|
// SOF-Marker tragen die Dimensionen (DHT=C4, JPG=C8, DAC=CC ausgenommen)
|
||||||
|
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
||||||
|
return buf.readUInt16BE(i + 7); // FF Cx, len(2), precision(1), height(2), width(2)
|
||||||
|
}
|
||||||
|
const len = buf.readUInt16BE(i + 2);
|
||||||
|
if (len < 2) return 0;
|
||||||
|
i += 2 + len;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createSnapshotRouter };
|
module.exports = { createSnapshotRouter };
|
||||||
|
|||||||
Reference in New Issue
Block a user