Files
appRobotWebcam/src/hwencode.js
2026-06-07 17:00:43 +02:00

119 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 };