Standbild

This commit is contained in:
chk
2026-06-07 10:42:28 +02:00
parent faccbf55ce
commit d3e45262ce
6 changed files with 155 additions and 19 deletions

View File

@@ -250,7 +250,7 @@ class CameraSwitch extends EventEmitter {
async grabHires(opts = {}) {
// Shortcut: keine Format-Umschaltung wenn Live- und Hires-Auflösung identisch
if (this.liveSize === this.hiresSize) {
return this.getFrame();
return this.grabSnapshot();
}
const hiresW = parseInt(this.hiresSize.split('x')[0], 10);
@@ -278,7 +278,7 @@ class CameraSwitch extends EventEmitter {
// 2. hires-FFmpeg starten, warmlaufen lassen (settleFrames), besten Frame greifen.
// minWidth lehnt etwaige Übergangs-Frames in falscher Auflösung ab.
const jpeg = await this._captureHires({ minSize, minWidth, settleFrames, maxWaitMs });
const jpeg = await this._captureAt(this.hiresSize, this.hiresFps, this.hiresEncode, { minSize, minWidth, settleFrames, maxWaitMs });
const gotW = readJpegWidth(jpeg) ?? '?';
console.log(`[cam ${this.id}] HD OK ${jpeg.length} bytes, Breite=${gotW}px (Soll: ${hiresW}px, ${Date.now() - t0}ms)`);
return jpeg;
@@ -291,6 +291,36 @@ class CameraSwitch extends EventEmitter {
}
}
// ── Einzelbild-Snapshot (für /api/snapshot, inkl. Snapshot-Modus) ──────────
// Läuft Live → aktuelles Frame (schnell, kein Geräte-Neustart). Sonst:
// • streamEnabled (Live an, aber Frame noch nicht da) → getFrame() (on-demand).
// • stream:false (Snapshot-Modus) → one-shot open/grab/close an liveSize. Das
// Gerät bleibt dazwischen geschlossen → spart CPU/USB; der Viewer pollt das
// alle paar Sekunden. streamEnabled bleibt aus (kein Dauer-Stream).
async grabSnapshot() {
if (this.latest) return this.latest; // Live läuft → sofort
if (this.streamEnabled) return this.getFrame(); // Live an, Frame kommt gleich
if (this.lock) throw new Error('Gerät belegt (Grab läuft)');
this.lock = true;
if (this.restartTimer) { clearTimeout(this.restartTimer); this.restartTimer = null; }
this.state = 'grabbing';
try {
const w = parseInt(this.liveSize.split('x')[0], 10) || 0;
return await this._captureAt(this.liveSize, this.liveFps, this.encode, {
minSize: 1000,
minWidth: w ? Math.floor(w * 0.8) : 0,
settleFrames: 3,
maxWaitMs: 6000,
});
} finally {
this.state = 'stopped';
this.lock = false;
// Falls inzwischen wieder Live gewünscht ist (stream:true + Verbraucher):
if (this.streamEnabled && (!this.onDemand || this.subscribers > 0)) this._spawnLive();
}
}
// Beendet den aktuellen Prozess und resolved erst nach dessen 'close' (FD frei).
_killCurrentAndWait(timeoutMs = 4000) {
return new Promise((resolve) => {
@@ -306,7 +336,9 @@ class CameraSwitch extends EventEmitter {
});
}
_captureHires({ minSize, minWidth, settleFrames, maxWaitMs }) {
// Generischer one-shot Capture an beliebiger Auflösung (open → settle → grab → close).
// Genutzt von grabHires (hiresSize) und grabSnapshot (liveSize, Snapshot-Modus).
_captureAt(size, fps, encode, { minSize, minWidth, settleFrames, maxWaitMs }) {
return new Promise((resolve, reject) => {
const args = [
'-hide_banner', '-loglevel', 'warning',
@@ -314,9 +346,9 @@ class CameraSwitch extends EventEmitter {
'-probesize', '5000000',
'-analyzeduration', '1000000',
'-f', 'v4l2', '-input_format', 'mjpeg',
'-video_size', this.hiresSize, '-framerate', String(this.hiresFps),
'-video_size', size, '-framerate', String(fps),
'-i', this.device,
...videoOutArgs(this.hiresEncode), '-f', 'mpjpeg', 'pipe:1',
...videoOutArgs(encode), '-f', 'mpjpeg', 'pipe:1',
];
let p;
try {

View File

@@ -54,8 +54,9 @@ function createSnapshotRouter(switches, cameras) {
const sw = switches[req.params.id];
if (!sw) return res.status(404).json({ error: `Unbekannte Kamera: ${req.params.id}` });
try {
// getFrame() startet die Kamera bei Bedarf on-demand und wartet auf ein frisches Bild
const frame = await sw.getFrame();
// grabSnapshot(): liefert das Live-Frame falls vorhanden, sonst (Snapshot-Modus,
// stream:false) ein one-shot Bild öffnet das Gerät kurz und schliesst es wieder.
const frame = await sw.grabSnapshot();
res.set({
'Content-Type': 'image/jpeg',
'Content-Length': frame.length,