'use strict'; // ── Hardware-Encoding-Konfiguration (Intel / AMD / Software) ────────────────── // Eine Stelle, die entscheidet WIE H.264 erzeugt wird. Der Rest des Codes kennt // nur `encode: 'h264'` (pro Kamera) – WELCHER Encoder (GPU-Vendor) genutzt wird, // ist eine Maschinen-Eigenschaft und kommt global aus dem Env (server.js). // // GPU=intel → VAAPI (h264_vaapi) ← Default-Encoder, läuft auf Intel UHD 630 // GPU=amd → VAAPI (h264_vaapi) ← läuft auf AMD 680M (VCN), gleiche API // GPU=auto → VAAPI (Annahme: /dev/dri vorhanden) // GPU=none → libx264 (Software-Fallback, zum Testen ohne GPU; hohe CPU) // // VAAPI ist die gemeinsame Linux-Schnittstelle für Intel UND AMD → ein Codepfad // für beide. Optional erzwingbar: HWENC=vaapi|qsv|libx264 (qsv = nur Intel). const DEFAULT_DEVICE = '/dev/dri/renderD128'; // Normalisiert die Env-Wünsche zu einem konkreten Encoder-Backend. // vendor: 'intel' | 'amd' | 'auto' | 'none' (GPU) // encoder: 'vaapi' | 'qsv' | 'libx264' | undefined (HWENC, überschreibt vendor) // device: VAAPI-Renderknoten (HWENC_DEVICE) function resolveHwenc({ vendor = 'auto', encoder, device } = {}) { const v = String(vendor).toLowerCase(); let enc = encoder ? String(encoder).toLowerCase() : null; if (!enc) { if (v === 'none' || v === 'cpu' || v === 'software') enc = 'libx264'; else enc = 'vaapi'; // intel, amd, auto → VAAPI (gemeinsamer Pfad) } if (!['vaapi', 'qsv', 'libx264'].includes(enc)) { throw new Error(`Unbekannter HWENC '${enc}' (erlaubt: vaapi, qsv, libx264)`); } return { encoder: enc, device: device || DEFAULT_DEVICE, vendor: v }; } // profile ('constrained_baseline'|'baseline'|'main'|'high') + level-Hex → // MSE-Codec-String (z. B. 'avc1.4D401F'). Der Browser braucht das exakt für // addSourceBuffer(); der Stream-Decode selbst ist toleranter. const PROFILE_IDC = { constrained_baseline: '42E0', baseline: '4200', main: '4D40', high: '6400', }; function mseCodecString(profile = 'main', levelHex = '1F') { const idc = PROFILE_IDC[String(profile).toLowerCase()] ?? PROFILE_IDC.main; const lvl = String(levelHex).toUpperCase().padStart(2, '0'); return `avc1.${idc}${lvl}`; } // Baut die FFmpeg-Args für den Live-H.264-Pfad: // Ausgang 1 (pipe:1): fragmentiertes MP4 (fMP4) → MSE im Browser. // Ausgang 2 (pipe:3): optional gedrosseltes MJPEG → hält `latest` für // /api/snapshot am Leben (Homing-Projekt) und ermöglicht -Fallback. // // Rückgabe: { args, useFd3 } – useFd3=true ⇒ spawn mit stdio …,'pipe' (fd 3). function h264LiveArgs({ device, size, fps, hwenc, bitrate = '3M', gop, profile = 'main', fragMs = 200, jpegSnapshots = true, jpegFps = 2, jpegQ = 7, }) { const g = gop || Math.max(1, Math.round((parseInt(fps, 10) || 30) * 2)); // ~2 s const { encoder, device: vaapiDev } = hwenc; const input = [ '-hide_banner', '-loglevel', 'warning', '-fflags', 'nobuffer', '-f', 'v4l2', '-input_format', 'mjpeg', '-video_size', size, '-framerate', String(fps), '-i', device, ]; // Encoder-spezifische Geräte-Init + Upload-/Format-Filter. let hwInit = []; let encFilter; // Filter, der einen encode-fähigen Frame erzeugt let encCodec; // -c:v … if (encoder === 'vaapi') { hwInit = ['-vaapi_device', vaapiDev]; encFilter = 'format=nv12,hwupload'; encCodec = ['-c:v', 'h264_vaapi']; } else if (encoder === 'qsv') { hwInit = ['-init_hw_device', `qsv=hw:${vaapiDev}`, '-filter_hw_device', 'hw']; encFilter = 'format=nv12,hwupload=extra_hw_frames=16'; encCodec = ['-c:v', 'h264_qsv']; } else { // libx264 (Software-Fallback) encFilter = 'format=yuv420p'; encCodec = ['-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency']; } const mp4Out = [ ...encCodec, '-profile:v', profile, '-b:v', String(bitrate), '-g', String(g), '-bf', '0', '-f', 'mp4', '-movflags', '+frag_keyframe+empty_moov+default_base_moof', '-frag_duration', String(Math.max(1, Math.round(fragMs)) * 1000), 'pipe:1', ]; if (!jpegSnapshots) { return { args: [...input, ...hwInit, '-vf', encFilter, ...mp4Out], useFd3: false }; } // Mit Snapshot-Nebenausgang: Eingang splitten – ein Zweig encodet H.264, // der andere liefert gedrosseltes MJPEG für /api/snapshot + Fallback. const filterComplex = `[0:v]split=2[enc][jpg];[enc]${encFilter}[venc]`; const args = [ ...input, ...hwInit, '-filter_complex', filterComplex, '-map', '[venc]', ...mp4Out, '-map', '[jpg]', '-r', String(jpegFps), '-c:v', 'mjpeg', '-q:v', String(jpegQ), '-f', 'mpjpeg', 'pipe:3', ]; return { args, useFd3: true }; } module.exports = { resolveHwenc, mseCodecString, h264LiveArgs, DEFAULT_DEVICE };