119 lines
4.7 KiB
JavaScript
119 lines
4.7 KiB
JavaScript
'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 <img>-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 };
|