'use strict'; const express = require('express'); const fs = require('fs'); const http = require('http'); const path = require('path'); const { CameraSwitch } = require('./src/cameraSwitch'); const { createSnapshotRouter, createStreamRouter, createCamerasRouter } = require('./src/snapshotService'); const PORT = parseInt(process.env.PORT ?? '8444', 10); const LIVE_SIZE = process.env.LIVE_SIZE ?? '640x480'; const LIVE_FPS = parseInt(process.env.LIVE_FPS ?? '30', 10); const HIRES_SIZE = process.env.HIRES_SIZE ?? '1280x960'; const HIRES_FPS = parseInt(process.env.HIRES_FPS ?? '15', 10); const ENCODE_MODE = process.env.ENCODE_MODE ?? 'copybsf'; // 'copybsf' (niedrige CPU) | 'mjpeg' (Re-Encode-Fallback) const ON_DEMAND = (process.env.ON_DEMAND ?? 'true') !== 'false'; // Live nur bei Verbrauchern (spart idle-CPU) const IDLE_GRACE_MS = parseInt(process.env.IDLE_GRACE_MS ?? '15000', 10); // ── cameras.json → CameraSwitch-Instanzen ───────────────────────────────────── let camerasJson; try { camerasJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'cameras.json'), 'utf8')); } catch (e) { console.error('cameras.json fehlt oder ungültig:', e.message); process.exit(1); } const camsConfig = camerasJson.cameras; if (!Array.isArray(camsConfig) || camsConfig.length === 0) { console.error('cameras.json: "cameras" muss ein nicht-leeres Array sein'); process.exit(1); } const switches = {}; const camsMeta = []; // { id, device, name, position, stream, hires, note } for (const cam of camsConfig) { if (!cam.id || !cam.device) { console.error(`cameras.json: Eintrag ohne id/device: ${JSON.stringify(cam)}`); process.exit(1); } // Per-Kamera-Felder in cameras.json überschreiben die globalen Env-Werte switches[cam.id] = new CameraSwitch({ id: cam.id, device: cam.device, liveSize: cam.liveSize ?? LIVE_SIZE, liveFps: cam.liveFps ?? LIVE_FPS, hiresSize: cam.hiresSize ?? HIRES_SIZE, hiresFps: cam.hiresFps ?? HIRES_FPS, encode: cam.encode ?? ENCODE_MODE, onDemand: ON_DEMAND, idleGraceMs: IDLE_GRACE_MS, }); camsMeta.push({ id: cam.id, device: cam.device, name: cam.name ?? cam.id, position: cam.position ?? '', stream: cam.stream !== false, hires: cam.hires !== false, note: cam.note ?? '', }); } const app = express(); // ── 1. Eigene Endpunkte ─────────────────────────────────────────────────────── app.use('/api/snapshot', createSnapshotRouter(switches, camsMeta)); app.use('/api/stream', createStreamRouter(switches)); app.use('/api/cameras', createCamerasRouter(camsMeta)); app.get('/health', (_req, res) => { res.json({ status: 'ok', cameras: camsMeta.map((c) => { const sw = switches[c.id]; return { id: c.id, name: c.name, device: c.device, state: sw?.state, hasFrame: !!sw?.latest }; }), }); }); app.get('/config.json', (_req, res) => { res.json({ cameras: camsMeta.map((c) => ({ id: c.id, name: c.name, stream: c.stream })) }); }); // ── 2. Statische Dateien ────────────────────────────────────────────────────── // no-cache: Browser MUSS index.html/viewer.js vor Nutzung revalidieren. app.use(express.static(path.join(__dirname, 'public'), { setHeaders: (res) => res.setHeader('Cache-Control', 'no-cache'), })); // ── 3. Start ────────────────────────────────────────────────────────────────── const server = http.createServer(app); server.listen(PORT, '0.0.0.0', () => { console.log(`AppRobotWebcam http://0.0.0.0:${PORT}`); console.log(` Kameras: ${camsMeta.map((c) => `${c.id}=${c.device} "${c.name}" stream=${c.stream}`).join(', ')}`); console.log(` Live: ${LIVE_SIZE}@${LIVE_FPS} · HD-Grab: ${HIRES_SIZE}@${HIRES_FPS} · Encode: ${ENCODE_MODE} · On-Demand: ${ON_DEMAND}`); console.log(` Viewer: http://0.0.0.0:${PORT}/`); // Live-Producer starten (Dauerbetrieb) Object.values(switches).forEach((sw) => sw.start()); }); const shutdown = (sig) => { console.log(`\n${sig} – shutting down`); Object.values(switches).forEach((sw) => { sw.stopping = true; if (sw.proc) { try { sw.proc.kill('SIGKILL'); } catch (_e) {} } }); server.close(() => process.exit(0)); setTimeout(() => process.exit(0), 3000); // Sicherheitsnetz }; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM'));