'use strict'; const express = require('express'); const fs = require('fs'); const path = require('path'); const { readJpegWidth } = require('./cameraSwitch'); // Datum → "YYYYMMDD_HHMMSS" function formatTs(date) { const p = n => String(n).padStart(2, '0'); return `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}_${p(date.getHours())}${p(date.getMinutes())}${p(date.getSeconds())}`; } // Stabile Schnittstellen für Viewer und Homing-Projekt – lesen NUR aus den // CameraSwitch-Instanzen (RAM-Puffer + Event-Stream). Kein Gerätezugriff hier, // keine go2rtc-Abhängigkeit mehr. // // GET /api/snapshot → JSON-Liste der Kameras // GET /api/snapshot/cam0 → letztes Live-JPEG (640) aus dem Puffer // GET /api/snapshot/cam0/hires → HD-JPEG (1280): Schalter pausiert Live kurz // GET /api/stream/cam0 → MJPEG multipart/x-mixed-replace (Live) function createSnapshotRouter(switches, cameras) { const router = express.Router(); // cameras = camsMeta aus server.js: [{id, name, position, stream, hires, encode, mseCodec, note}] router.get('/', (_req, res) => { res.json({ cameras: cameras.map((c) => ({ id: c.id, name: c.name, position: c.position, stream: c.stream, hires: c.hires, encode: c.encode, // 'copybsf' | 'mjpeg' | 'h264' → Viewer wählt Player mseCodec: c.mseCodec, // nur bei h264: Codec-String für MSE-addSourceBuffer url: `/api/snapshot/${c.id}`, })), }); }); // HD-Grab: delegiert an den Schalter. Der Schalter garantiert, dass Live // sauber gestoppt ist (Prozess-close), bevor 1280 startet → kein Race. router.get('/:id/hires', async (req, res) => { const sw = switches[req.params.id]; if (!sw) return res.status(404).json({ error: `Unbekannte Kamera: ${req.params.id}` }); try { const jpeg = await sw.grabHires(); res.set({ 'Content-Type': 'image/jpeg', 'Content-Length': jpeg.length, 'Cache-Control': 'no-store', 'X-Camera-Id': req.params.id, 'X-Frame-Width': String(readJpegWidth(jpeg) ?? ''), 'X-Timestamp': new Date().toISOString(), }); res.end(jpeg); } catch (err) { res.status(503).json({ error: `hires: ${err.message}` }); } }); router.get('/:id', async (req, res) => { const sw = switches[req.params.id]; if (!sw) return res.status(404).json({ error: `Unbekannte Kamera: ${req.params.id}` }); try { // 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, 'Cache-Control': 'no-store', 'X-Camera-Id': req.params.id, 'X-Timestamp': new Date().toISOString(), }); res.end(frame); } catch (err) { res.status(503).json({ error: `kein Frame: ${err.message}` }); } }); return router; } // H.264-Live-Stream als fortlaufendes fragmentiertes MP4 (video/mp4). Ein FFmpeg // (im Schalter) → Fan-out an beliebig viele Browser. Der Client speist die Bytes // per MSE in ein