diff --git a/src/cameraSwitch.js b/src/cameraSwitch.js index 6c435eb..c6c26ef 100644 --- a/src/cameraSwitch.js +++ b/src/cameraSwitch.js @@ -232,15 +232,19 @@ class CameraSwitch extends EventEmitter { // 1. Live-FFmpeg beenden, auf Prozess-Ende warten (= Device-FD frei) await this._killCurrentAndWait(); - // Kurze Pause: v4l2-Buffer der Kamera können noch Frames der alten Live- - // Auflösung enthalten (z.B. 640×480-Rest wenn hires 1920×1080 fordert). - // 300 ms genügen damit die Kamera den Format-Reset abschliessen kann. - await sleep(300); + // 2. Kamera resetten: kurz warten, dann zwischenformat öffnen, wieder warten. + await sleep(800); + const warmupSize = this._chooseWarmupSize(); + if (warmupSize) { + console.log(`[cam ${this.id}] HD: Zwischenformat ${warmupSize} zum Kamera-Reset`); + await this._warmupFormat(warmupSize); + await sleep(500); + } this.state = 'grabbing'; console.log(`[cam ${this.id}] HD: Live gestoppt nach ${Date.now() - t0}ms, Gerät frei → ${this.hiresSize}-Grab (minWidth=${minWidth})`); - // 2. hires-FFmpeg starten, warmlaufen lassen, besten Frame greifen + // 3. hires-FFmpeg starten, warmlaufen lassen, besten Frame greifen const jpeg = await this._captureHires({ 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)`); @@ -269,10 +273,60 @@ class CameraSwitch extends EventEmitter { }); } + _chooseWarmupSize() { + const liveW = parseInt(this.liveSize.split('x')[0], 10); + const hiresW = parseInt(this.hiresSize.split('x')[0], 10); + if (Number.isNaN(liveW) || Number.isNaN(hiresW)) return null; + if (liveW < 1280 && hiresW >= 1280) return '1280x720'; + return null; + } + + _warmupFormat(size) { + return new Promise((resolve) => { + const args = [ + '-hide_banner', '-loglevel', 'warning', + '-fflags', 'nobuffer', + '-f', 'v4l2', '-input_format', 'mjpeg', + '-video_size', size, '-framerate', String(this.hiresFps), + '-i', this.device, + '-frames:v', '4', '-f', 'null', '-' + ]; + let p; + try { + p = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'ignore'] }); + } catch (_e) { + return resolve(); + } + this.proc = p; + this.stopping = false; + + let finished = false; + const done = () => { + if (finished) return; + finished = true; + if (p) { + try { p.kill('SIGTERM'); } catch (_e) {} + } + resolve(); + }; + + p.stderr.on('data', (c) => { + const s = c.toString(); + if (/error|busy|invalid|no such|cannot|denied/i.test(s)) console.error(`[cam ${this.id}] warmup ffmpeg: ${s.trim()}`); + }); + p.on('error', done); + p.on('close', done); + setTimeout(done, 1600); + }); + } + _captureHires({ minSize, minWidth, settleFrames, maxWaitMs }) { return new Promise((resolve, reject) => { const args = [ '-hide_banner', '-loglevel', 'warning', + '-fflags', 'nobuffer', + '-probesize', '5000000', + '-analyzeduration', '1000000', '-f', 'v4l2', '-input_format', 'mjpeg', '-video_size', this.hiresSize, '-framerate', String(this.hiresFps), '-i', this.device,