// Encoder-Fähigkeiten — GEMESSEN, nicht behauptet (KONZEPT § 4.2, // Regeln aus caps.py, dort am echten HandBrake abgenommen): // // - `--help`, Abschnitt `-e, --encoder`: NUR was DIESE Maschine wirklich // benutzen kann (HandBrake probiert Hardware beim Start selbst an). // - `--preset-list`: ALLE mitgelieferten Preset-NAMEN (Struktur ist die // Einrückung — Namen stehen mit genau vier Leerzeichen). // - Hardware nach FAMILIE unterscheiden (nvenc/qsv/vce/vaapi), sonst // läuft AMD unter einem Intel-Namen (Commander-PC: RX 9070 XT). import { werkzeugSpawn } from '../werkzeuge/aufruf' import { textVon } from '../rip/parser' const HB_ENCODER_KOPF = /^-e,\s*--encoder\b/ /** Video-Encoder aus `--help` — der Block endet an der nächsten Option. */ export function parseEncoderListe(hilfeText: string): string[] { const encoder: string[] = [] let imBlock = false for (const zeile of (hilfeText ?? '').split(/\r?\n/)) { const inhalt = zeile.trim() if (!imBlock) { if (HB_ENCODER_KOPF.test(inhalt)) imBlock = true continue } if (inhalt.length === 0) continue if (inhalt.startsWith('-')) break encoder.push(inhalt.split(/\s+/)[0]) } return encoder } // Reihenfolge = Anzeige-Reihenfolge; AV1 in Hardware sichtbar machen. const HW_FAMILIEN: ReadonlyArray = [ ['nvenc', 'nvenc'], ['qsv', 'qsv'], ['vce', 'vce'], ['vaapi', 'vaapi'], ] /** HandBrake-Encoder-Namen → Backend-Kennungen fürs UI. */ export function leiteBackendsAb(hbEncoder: readonly string[]): string[] { const namen = new Set(hbEncoder.map((e) => e.toLowerCase())) const gefunden: string[] = [] if ([...namen].some((n) => n.startsWith('x264'))) gefunden.push('cpu-x264') if ([...namen].some((n) => n.startsWith('x265'))) gefunden.push('cpu-x265') if ([...namen].some((n) => n.startsWith('svt_av1'))) gefunden.push('cpu-av1') for (const [kennzeichen, backend] of HW_FAMILIEN) { const treffer = [...namen].filter((n) => n.includes(kennzeichen)) if (treffer.length === 0) continue gefunden.push(backend) if (treffer.some((n) => n.includes('av1'))) gefunden.push(`${backend}-av1`) } return gefunden } /** Preset-Namen aus `--preset-list`: genau vier Leerzeichen Einrückung. * Kategorien (0), Beschreibungen (8) und Logzeilen fallen so weg. */ export function parsePresetListe(text: string): string[] { const presets: string[] = [] for (const zeile of (text ?? '').split(/\r?\n/)) { if (zeile.trim().length === 0) continue const einrueckung = zeile.length - zeile.replace(/^ +/, '').length if (einrueckung === 4) presets.push(zeile.trim()) } return presets } function lauf(programm: string, args: string[], timeoutMs: number): Promise { return new Promise((fertig) => { const kind = werkzeugSpawn(programm, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) const stuecke: Buffer[] = [] kind.stdout.on('data', (s: Buffer) => stuecke.push(s)) kind.stderr.on('data', (s: Buffer) => stuecke.push(s)) const zeiger = setTimeout(() => kind.kill(), timeoutMs) kind.on('error', () => { clearTimeout(zeiger) fertig('') }) kind.on('close', () => { clearTimeout(zeiger) fertig(textVon(Buffer.concat(stuecke))) }) }) } export interface EncoderAuskunft { encoder: string[] backends: string[] presets: string[] } /** Fragt HandBrake selbst — leer, wenn es nicht da ist (kein Raten). */ export async function encoderMessen(programm: string): Promise { if (programm.length === 0) return { encoder: [], backends: [], presets: [] } const [hilfe, presetListe] = await Promise.all([ lauf(programm, ['--help'], 30_000), lauf(programm, ['--preset-list'], 30_000), ]) const encoder = parseEncoderListe(hilfe) return { encoder, backends: leiteBackendsAb(encoder), presets: parsePresetListe(presetListe) } }