From fe12401e34226b9be3f1a6065b33eb7463d7995c Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sun, 26 Jul 2026 11:51:47 +0200 Subject: [PATCH] fix(worker): Hardware-Encoder und CPU-Merkmale auf Windows - plus ehrliche Transcode-Fehler Aus dem gescheiterten Test-Rip des Commanders (Job 95afdc89) und seiner UI-Durchsicht. Der Reihe nach. ## 1. Der externe Encoder wurde SEHR WOHL gesehen (Diagnose-Korrektur) Der Commander schloss aus der Meldung, das System sehe seinen PC nicht. Live gemessen sagt das Gegenteil: meta.transcode_node = tobisnicerpc@TobisNicerPC VM-Worker-Log = nur rip_disc, KEIN transcode_files 22:19:41.271 Kompression eingereiht 22:19:41.453 "Keine Roh-MKVs gefunden" <- 182 ms spaeter Das Routing hat funktioniert. Sein PC nahm die Aufgabe an und lehnte sie sofort ab, weil er /app/media/rippy/ nicht finden kann - ein Pfad, der nur INNEN im Container existiert. Der Rip war vollstaendig (79,6 GB auf der NAS). ## 2. Der Hardware-Encoder-Bug war meiner leite_backends_ab() verlangte zusaetzlich ein Geraet: /dev/dri fuer VAAPI/QSV/VCE bzw. nvidia-smi fuer NVENC. Unter WINDOWS gibt es beides nicht - der PC des Commanders (RX 9070 XT) meldete deshalb nur CPU-Encoder, obwohl HandBrakes Windows-Build vce_*/nvenc_*/qsv_* beherrscht. Der Bug traf genau den Anwendungsfall, fuer den externe Worker gedacht sind. Die Pruefung war ausserdem ueberfluessig: HandBrake probiert Hardware beim Start selbst an und listet nur Nutzbares - auf der VM belegt durch "qsv: not available on this system" bei gleichzeitig fehlendem qsv_* in --help. Jetzt ist HandBrakes Liste die einzige Auskunft. Dazu nach FAMILIE unterschieden (nvenc/qsv/vce/vaapi) statt alles in "vaapi" zu werfen - eine AMD-Karte lief unter dem Intel-Namen. Hardware-AV1 bekommt eine eigene Kennung, weil es die beste Kombination aus Tempo und Groesse ist und sonst unsichtbar bliebe. ## 3. Vektorbefehle unter Windows - gemessen, nicht "unbekannt" Es gibt dort kein /proc/cpuinfo, also meldete der Windows-Worker "Vektorbefehle unbekannt" - gerade auf der Maschine, die encodieren soll. Jetzt ueber IsProcessorFeaturePresent (kernel32, winnt.h dokumentiert) plus CPU-Name aus der Registry. Auf dem Commander-PC gegengeprueft: vorher: AMD64 Family 26 Model 68 Stepping 0 / Vektorbefehle unbekannt jetzt: AMD Ryzen 7 9700X 8-Core Processor / avx512f / 16 Kerne Damit verschwindet die AVX2-Warnung fuer diesen Worker von selbst - genau das hatte der Commander gefordert. ## 4. Ehrliche Fehlermeldung statt "Keine Roh-MKVs gefunden" Neues _erreichbarkeit_pruefen() unterscheidet jetzt drei Faelle statt einem: Pfad nicht erreichbar UND kein Mapping gesetzt (nennt RIPPY_PATH_MAP samt Beispiel), Mapping gesetzt aber greift nicht, oder Pfad einfach nicht da. Und es prueft BEIDE Pfade - im Fall des Commanders lag die Quelle auf der erreichbaren NAS, das ZIEL aber auf der VM-Platte ohne Samba; das waere erst beim Schreiben nach Stunden Rechenzeit aufgefallen. Jede Meldung sagt jetzt ausdruecklich, dass die Rohdaten NICHT verloren sind und "Neu komprimieren" genuegt. ## 5. Persoenliche Werte raus Vier Platzhalter trugen die Umgebung des Commanders (192.168.178.20, TOBIS-PC, seine Jellyfin- und Rippy-IP). In einem Repo, das weitergegeben wird, hat das nichts verloren. Nebenbei dreimal in die eigene Falle getappt: deutsche Anfuehrungszeichen in DOPPELT gequoteten Python-Strings beenden den String vorzeitig. Der Bestand nutzt dafuer einfach gequotete f-Strings - jetzt auch meine. NOCH OFFEN und wichtig: RIPPY_PATH_MAP wird von niemandem GESETZT (Installer, UI, Compose). Ein Windows-Worker kann damit grundsaetzlich nicht transcodieren. Das ist der naechste Schritt. Co-Authored-By: Claude Opus 5 --- docker/ui/src/components/StorageMounts.tsx | 2 +- docker/ui/src/components/WorkerVerwaltung.tsx | 5 +- docker/ui/src/lib/design.ts | 12 +- docker/ui/src/pages/Settings.tsx | 2 +- docker/worker/caps.py | 143 +++++++++++++++--- docker/worker/tasks.py | 92 ++++++++++- docker/worker/test_caps.py | 65 +++++--- 7 files changed, 271 insertions(+), 50 deletions(-) diff --git a/docker/ui/src/components/StorageMounts.tsx b/docker/ui/src/components/StorageMounts.tsx index 31693dd..c293b72 100644 --- a/docker/ui/src/components/StorageMounts.tsx +++ b/docker/ui/src/components/StorageMounts.tsx @@ -378,7 +378,7 @@ export default function StorageMounts() { label="Rechner (dein PC oder NAS — Name oder IP)" value={host} onChange={e => setHost(e.target.value)} - placeholder="192.168.178.20 oder TOBIS-PC" + placeholder="z. B. 192.168.1.20 oder MEIN-NAS" /> setRippyHost(e.target.value.trim())} - placeholder="z. B. 192.168.178.162" + placeholder="z. B. 192.168.1.10" />

Die IP der Maschine, auf der Rippy selbst (Docker) läuft — meist deine @@ -216,7 +216,8 @@ export default function WorkerVerwaltung() { {siehtOeffentlichAus && (

⚠️ Das sieht nach einer externen Domain aus (Reverse-Proxy) — die geht nicht. Bitte die - LAN-IP der Rippy-Maschine eintragen, z. B. 192.168.178.162. + LAN-IP der Rippy-Maschine eintragen — die beginnt im Heimnetz + fast immer mit 192.168., 10. oder 172.16.–172.31.

)} diff --git a/docker/ui/src/lib/design.ts b/docker/ui/src/lib/design.ts index fe84198..ba398b9 100644 --- a/docker/ui/src/lib/design.ts +++ b/docker/ui/src/lib/design.ts @@ -77,10 +77,20 @@ export const LOG_LEVEL_STYLES = { }, } as const +// Encoder-Backends, wie worker/caps.py sie meldet. Hardware wird seit +// 25.07.2026 nach FAMILIE unterschieden (nvenc/qsv/vce/vaapi) — vorher landete +// eine AMD-Karte unter dem Intel-Namen „vaapi", und AV1 in Hardware war gar +// nicht sichtbar, obwohl es die beste Kombination aus Tempo und Größe ist. export const ENCODER_BADGES: Record = { 'cpu-x264': { label: 'H.264 (CPU)', highlight: false }, 'cpu-x265': { label: 'H.265 (CPU)', highlight: false }, 'cpu-av1': { label: 'AV1 (CPU)', highlight: false }, + 'nvenc': { label: 'NVENC ⚡ (NVIDIA)', highlight: true }, + 'nvenc-av1': { label: 'AV1 · NVENC ⚡', highlight: true }, + 'qsv': { label: 'QuickSync ⚡ (Intel)', highlight: true }, + 'qsv-av1': { label: 'AV1 · QuickSync ⚡', highlight: true }, + 'vce': { label: 'VCE ⚡ (AMD)', highlight: true }, + 'vce-av1': { label: 'AV1 · VCE ⚡', highlight: true }, 'vaapi': { label: 'VAAPI ⚡', highlight: true }, - 'nvenc': { label: 'NVENC ⚡', highlight: true }, + 'vaapi-av1': { label: 'AV1 · VAAPI ⚡', highlight: true }, } diff --git a/docker/ui/src/pages/Settings.tsx b/docker/ui/src/pages/Settings.tsx index b23de01..90d9d02 100644 --- a/docker/ui/src/pages/Settings.tsx +++ b/docker/ui/src/pages/Settings.tsx @@ -498,7 +498,7 @@ export default function SettingsPage() { label="Server-URL" value={settings.jellyfinUrl} onChange={(e) => handleChange('jellyfinUrl', e.target.value)} - placeholder="http://192.168.178.50:8096" + placeholder="z. B. http://192.168.1.50:8096" />
diff --git a/docker/worker/caps.py b/docker/worker/caps.py index f01d2bf..b6ba212 100644 --- a/docker/worker/caps.py +++ b/docker/worker/caps.py @@ -50,16 +50,41 @@ def parse_encoder_liste(hilfe_text: str) -> list: return encoder -def leite_backends_ab(hb_encoder, dri_da: bool, nvidia_da: bool) -> list: +# Hardware-Encoder-Familien, wie HandBrake sie benennt → Backend-Kennung. +# Reihenfolge = Anzeige-Reihenfolge; AV1 zuerst, weil es das modernste ist. +HW_FAMILIEN = ( + ("nvenc", "nvenc"), # NVIDIA + ("qsv", "qsv"), # Intel QuickSync + ("vce", "vce"), # AMD VCE/VCN + ("vaapi", "vaapi"), # generisch (Linux) +) + + +def leite_backends_ab(hb_encoder) -> list: """Übersetzt die HandBrake-Encoder-Namen in die Backends, die das UI zeigt. - Zwei Ehrlichkeits-Regeln, die vorher fehlten (Befund 25.07.2026): - * Ohne HandBrake gibt es KEINE Encoder. Vorher standen `cpu-x264` und - `cpu-x265` fest verdrahtet in der Liste („immer dabei") — ein reiner - Rip-Worker ohne HandBrake behauptete damit, komprimieren zu können. - * Hardware zählt nur, wenn das Gerät da ist UND HandBrake den Encoder - überhaupt kennt. Vorher genügte `/dev/dri` — das Worker-Image kann - laut eigener Auskunft aber gar keinen Hardware-Encoder. + ## HandBrakes Liste IST die Auskunft — keine zweite Meinung nötig + + Frühere Fassung (25.07.2026) verlangte zusätzlich ein Gerät: `/dev/dri` + für VAAPI/QSV/VCE bzw. `nvidia-smi` für NVENC. Das war doppelt falsch: + + 1. **Unter Windows gibt es beides nicht.** Der Commander-PC (RX 9070 XT) + meldete deshalb nur CPU-Encoder, obwohl HandBrakes Windows-Build + `vce_*`, `nvenc_*` und `qsv_*` beherrscht. Ein Bug, der genau den + Anwendungsfall traf, für den externe Worker gedacht sind. + 2. **Die Prüfung war überflüssig.** HandBrake probiert Hardware-Encoder + beim Start selbst an und listet nur, was es wirklich benutzen kann. Auf + der Rippy-VM belegt: `--version` meldet „qsv: not available on this + system" und „Cannot load libnvidia-encode.so.1" — und genau diese + Encoder fehlen dann in `--help`. Wer die Liste hat, hat die Antwort. + + Bleibt die eine Regel, die weiter gilt: **ohne HandBrake gibt es keine + Encoder.** Vorher standen `cpu-x264`/`cpu-x265` fest verdrahtet drin, und + ein reiner Rip-Worker behauptete damit, komprimieren zu können. + + Hardware wird jetzt nach Familie unterschieden (nvenc/qsv/vce/vaapi) statt + alles in „vaapi" zu werfen — sonst kann das UI nicht sagen, welches Preset + passt, und AMD-Karten liefen unter einem Intel-Namen. """ namen = {e.lower() for e in (hb_encoder or [])} gefunden = [] @@ -69,10 +94,15 @@ def leite_backends_ab(hb_encoder, dri_da: bool, nvidia_da: bool) -> list: gefunden.append("cpu-x265") if any(n.startswith("svt_av1") for n in namen): gefunden.append("cpu-av1") - if dri_da and any(("qsv" in n) or ("vce" in n) or ("vaapi" in n) for n in namen): - gefunden.append("vaapi") - if nvidia_da and any("nvenc" in n for n in namen): - gefunden.append("nvenc") + for kennzeichen, backend in HW_FAMILIEN: + treffer = [n for n in namen if kennzeichen in n] + if not treffer: + continue + gefunden.append(backend) + # AV1 in Hardware ist die derzeit beste Kombination aus Tempo und + # Dateigröße — das muss sichtbar sein, nicht in „nvenc" verschwinden. + if any("av1" in n for n in treffer): + gefunden.append(f"{backend}-av1") return gefunden @@ -109,6 +139,72 @@ def _lies_cpuinfo() -> tuple: return modell, flags, kerne +# Merkmals-Nummern für IsProcessorFeaturePresent (Win32, winnt.h). +# Absteigend nach Stärke — der erste Treffer ist die höchste Stufe. +WIN_MERKMALE = ( + ("avx512f", 41), # PF_AVX512F_INSTRUCTIONS_AVAILABLE + ("avx2", 40), # PF_AVX2_INSTRUCTIONS_AVAILABLE + ("avx", 39), # PF_AVX_INSTRUCTIONS_AVAILABLE + ("sse4_2", 38), # PF_SSE4_2_INSTRUCTIONS_AVAILABLE +) + + +def _simd_windows() -> str: + """Höchste Vektor-Befehlsstufe unter Windows. + + Es gibt dort kein /proc/cpuinfo, deshalb meldete der Windows-Worker bis + 25.07.2026 „Vektorbefehle unbekannt" — gerade auf der Maschine, die das + Encodieren übernehmen soll, also genau da, wo die Angabe zählt. + + Gefragt wird `IsProcessorFeaturePresent` aus kernel32 (Win32-API, in + winnt.h dokumentiert). Kein Zusatzpaket, kein Registry-Gefummel, keine + Fremdprozesse. + """ + try: + import ctypes + + kernel32 = ctypes.windll.kernel32 + except (ImportError, AttributeError, OSError): + return "unbekannt" + for name, nummer in WIN_MERKMALE: + try: + if kernel32.IsProcessorFeaturePresent(nummer): + return name + except OSError: + return "unbekannt" + return "unbekannt" + + +def _modell_windows() -> str: + """Lesbarer CPU-Name unter Windows. + + platform.processor() liefert dort nur „AMD64 Family 26 Model 68 …" — die + Registry kennt den Namen, den auch der Aufkleber trägt. + """ + try: + import winreg + + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + r"HARDWARE\DESCRIPTION\System\CentralProcessor\0", + ) as schluessel: + name, _ = winreg.QueryValueEx(schluessel, "ProcessorNameString") + return " ".join(str(name).split()) + except (ImportError, OSError): + return "" + + +def cpu_merkmale() -> tuple: + """(Modell, SIMD-Stufe, Kernzahl) — plattformübergreifend, nie leer.""" + modell, flags, kerne = _lies_cpuinfo() + simd = beste_simd(flags) + if simd == "unbekannt" and platform.system() == "Windows": + simd = _simd_windows() + if not modell: + modell = _modell_windows() or platform.processor() or "unbekannt" + return modell, simd, (kerne or os.cpu_count() or 0) + + def hole_handbrake_hilfe() -> str: """`HandBrakeCLI --help` einmal abrufen (leer, wenn nicht installiert).""" if not shutil.which("HandBrakeCLI"): @@ -123,14 +219,13 @@ def hole_handbrake_hilfe() -> str: def erkenne_encoder() -> list: - """Encoder-Backends, die auf DIESER Maschine wirklich nutzbar sind.""" - hb_encoder = parse_encoder_liste(hole_handbrake_hilfe()) - dri_da = os.path.exists("/dev/dri/renderD128") - nvidia_da = bool( - shutil.which("nvidia-smi") - or os.path.exists("/usr/lib/x86_64-linux-gnu/libnvidia-encode.so.1") - ) - return leite_backends_ab(hb_encoder, dri_da, nvidia_da) + """Encoder-Backends, die auf DIESER Maschine wirklich nutzbar sind. + + Gefragt wird ausschließlich HandBrake selbst — es probiert Hardware beim + Start an und listet nur Nutzbares. Eine zusätzliche Geräteprüfung wäre + plattformabhängig (und war unter Windows falsch), siehe leite_backends_ab. + """ + return leite_backends_ab(parse_encoder_liste(hole_handbrake_hilfe())) def erkenne_ip() -> str: @@ -163,7 +258,7 @@ def werkzeug_versionen() -> dict: """ import socket - modell, flags, kerne = _lies_cpuinfo() + modell, simd, kerne = cpu_merkmale() info = { # Zuordnung im UI: hostname (für den Celery-Online-Abgleich) + IP "hostname": socket.gethostname(), @@ -171,9 +266,9 @@ def werkzeug_versionen() -> dict: # Rechenleistung fürs Software-Encodieren. Ohne diese drei Angaben war # am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf dieser Maschine # Tage statt Stunden braucht (kein AVX2, 4 Kerne). - "cpu_modell": modell or (platform.processor() or "unbekannt"), - "cpu_kerne": str(kerne or os.cpu_count() or 0), - "cpu_simd": beste_simd(flags), + "cpu_modell": modell, + "cpu_kerne": str(kerne), + "cpu_simd": simd, } hb_encoder = parse_encoder_liste(hole_handbrake_hilfe()) if hb_encoder: diff --git a/docker/worker/tasks.py b/docker/worker/tasks.py index c78094c..f56ae54 100644 --- a/docker/worker/tasks.py +++ b/docker/worker/tasks.py @@ -700,6 +700,80 @@ def scan_tracks(device_path: str): return {"status": "error", "error": str(e)} +def _ist_uebersetzt(container_pfad: str, lokal_pfad: str) -> bool: + """Hat RIPPY_PATH_MAP diesen Pfad wirklich angefasst? (pure, testbar)""" + return container_pfad != lokal_pfad + + +def _erreichbarkeit_pruefen(raw_container: str, raw_lokal: str, + final_container: str, final_lokal: str) -> str: + """Kann DIESER Worker Quelle und Ziel überhaupt sehen? Klartext oder "". + + Befund 25.07.2026 am Job 95afdc89, live: Das gezielte Routing an den + Windows-PC des Commanders funktionierte einwandfrei — der Worker nahm die + Aufgabe an und lehnte sie 182 ms später ab mit „Keine Roh-MKVs in + /app/media/rippy/… gefunden". Diese Meldung klang nach einem kaputten Rip, + obwohl der Rip vollständig war (79,6 GB lagen auf der NAS). Der Commander + schloss daraus, das System sähe den externen Encoder nicht — verständlich + und falsch. + + Die eigentliche Ursache: `/app/media/...` sind CONTAINER-Pfade. Ein nativer + Worker sieht sie nur, wenn RIPPY_PATH_MAP sie auf eine Freigabe übersetzt — + und dieses Mapping wurde von NIEMANDEM gesetzt. Es war also nicht möglich, + dass diese Kombination je funktioniert. + + Zweiter Stolperstein im selben Fall: Die Rohdaten lagen auf der NAS (für den + PC erreichbar), das ZIEL aber auf der VM-Platte (nicht erreichbar, dort + läuft kein Samba). Deshalb werden hier BEIDE Pfade geprüft, nicht nur die + Quelle — sonst scheitert es erst beim Schreiben, nach Stunden Rechenzeit. + """ + mapping = os.getenv("RIPPY_PATH_MAP", "") + fremder_worker = not os.path.isdir("/app") + + for zweck, container, lokal in ( + ("Quelle (Rohdaten)", raw_container, raw_lokal), + ("Ziel (fertige Datei)", final_container, final_lokal), + ): + if os.path.isdir(lokal): + continue + # Ziel darf fehlen, solange sein Elternordner erreichbar ist — es wird + # gleich angelegt. Nur ein unerreichbarer ELTERN-Pfad ist ein Problem. + if zweck.startswith("Ziel") and os.path.isdir(os.path.dirname(lokal) or "."): + continue + + text = [f"Dieser Worker erreicht die {zweck} nicht: {lokal}"] + if fremder_worker and not mapping: + text.append( + "Ursache: RIPPY_PATH_MAP ist auf dieser Maschine nicht gesetzt. " + f'„{container}" ist ein Pfad INNERHALB des Rippy-Containers — ein ' + 'externer Worker sieht ihn nur, wenn er auf eine Netzwerk-Freigabe ' + 'übersetzt wird.' + ) + text.append( + "Abhilfe: Freigabe auf diesem Rechner erreichbar machen und in " + "start-tray.bat setzen, z. B. " + "set RIPPY_PATH_MAP=/app/media=\\\\NAS\\rippy-media" + ) + elif fremder_worker and mapping: + text.append(f"RIPPY_PATH_MAP ist gesetzt ({mapping}), deckt diesen Pfad aber nicht ab.") + if not _ist_uebersetzt(container, lokal): + text.append( + f'„{container}" wurde von keinem Eintrag übersetzt — fehlt ein ' + 'Präfix, oder ist es ein anderes Verzeichnis als erwartet?' + ) + else: + text.append( + "Der Pfad existiert nicht. Liegt das Arbeitsverzeichnis auf einer " + "Freigabe, die gerade nicht eingehängt ist?" + ) + text.append( + 'WICHTIG: Die Rohdaten sind NICHT verloren — nach der Korrektur ' + 'genügt „Neu komprimieren", ohne die Disc erneut zu rippen.' + ) + return " ".join(text) + return "" + + @celery_app.task(bind=True, name="worker.tasks.transcode_files") def transcode_files(self, job_id: str, raw_dir: str, final_dir: str): """Stufe 2: HandBrake komprimiert die Roh-MKVs auf Arbeitsgröße. @@ -711,11 +785,27 @@ def transcode_files(self, job_id: str, raw_dir: str, final_dir: str): """ db.init_db() # Native Worker (Windows) übersetzen Container-Pfade aufs Netzlaufwerk + raw_container, final_container = raw_dir, final_dir raw_dir = pfad_lokal(raw_dir) final_dir = pfad_lokal(final_dir) + + fehler = _erreichbarkeit_pruefen(raw_container, raw_dir, final_container, final_dir) + if fehler: + ergebnis = {"status": "error", "error": fehler} + _job_abschliessen(job_id, ergebnis) + return ergebnis + quellen = sorted(glob.glob(os.path.join(raw_dir, "*.mkv"))) if not quellen: - ergebnis = {"status": "error", "error": f"Keine Roh-MKVs in {raw_dir} gefunden"} + ergebnis = { + "status": "error", + "error": ( + f"Das Verzeichnis {raw_dir} ist erreichbar, enthält aber keine " + "MKV-Datei. Der Rip hat also nichts abgelegt (oder jemand hat die " + "Datei entfernt). Die Kompression lässt sich nach einem neuen Rip " + "erneut anstoßen." + ), + } _job_abschliessen(job_id, ergebnis) return ergebnis diff --git a/docker/worker/test_caps.py b/docker/worker/test_caps.py index 5f60748..9efabc9 100644 --- a/docker/worker/test_caps.py +++ b/docker/worker/test_caps.py @@ -66,34 +66,59 @@ def test_ohne_handbrake_keine_encoder(): """Der wichtigste Fall: vorher standen cpu-x264/cpu-x265 fest verdrahtet in der Liste. Ein Rip-Worker ohne HandBrake behauptete damit, komprimieren zu können — und jeder Transcode dort endete sofort mit 'nicht installiert'.""" - assert caps.leite_backends_ab([], dri_da=True, nvidia_da=True) == [] + assert caps.leite_backends_ab([]) == [] + assert caps.leite_backends_ab(None) == [] def test_backends_aus_echter_liste(): + """Das Worker-Image der VM kann laut eigener Auskunft keine Hardware — + entsprechend steht hier nur CPU.""" encoder = caps.parse_encoder_liste(HB_HILFE_ECHT) - # Kein /dev/dri, kein NVIDIA — und HandBrake kann hier ohnehin keine Hardware - assert caps.leite_backends_ab(encoder, dri_da=False, nvidia_da=False) == [ - "cpu-x264", "cpu-x265", "cpu-av1", - ] + assert caps.leite_backends_ab(encoder) == ["cpu-x264", "cpu-x265", "cpu-av1"] -def test_hardware_nur_wenn_geraet_UND_handbrake_es_kann(): - """Befund 25.07.2026: Vorher genügte /dev/dri, um 'vaapi' zu melden. Das - Worker-Image kann laut eigener Auskunft aber gar keinen Hardware-Encoder — - Rippy hätte VAAPI versprochen und wäre beim Encodieren gescheitert.""" - ohne_hw = caps.parse_encoder_liste(HB_HILFE_ECHT) - assert "vaapi" not in caps.leite_backends_ab(ohne_hw, dri_da=True, nvidia_da=True) - assert "nvenc" not in caps.leite_backends_ab(ohne_hw, dri_da=True, nvidia_da=True) +def test_handbrakes_liste_ist_die_auskunft_ohne_geraetepruefung(): + """Regel-Wechsel 25.07.2026, weil die alte Fassung einen Bug hatte. + Vorher wurde zusätzlich ein Gerät verlangt: /dev/dri für VAAPI/QSV/VCE bzw. + nvidia-smi für NVENC. Unter WINDOWS gibt es beides nicht — der PC des + Commanders (RX 9070 XT) meldete deshalb nur CPU-Encoder, obwohl HandBrakes + Windows-Build vce_*/nvenc_*/qsv_* beherrscht. Der Bug traf genau den + Anwendungsfall, für den externe Worker gedacht sind. + + Die Prüfung war zudem überflüssig: HandBrake probiert Hardware beim Start + selbst an und listet nur Nutzbares (auf der VM belegt — 'qsv: not available + on this system', und qsv_* fehlt dann in --help). Wer die Liste hat, hat die + Antwort; eine zweite Meinung braucht es nicht. + """ mit_hw = caps.parse_encoder_liste(HB_HILFE_MIT_HARDWARE) - # Gerät fehlt → trotz Können kein Hardware-Backend - assert caps.leite_backends_ab(mit_hw, dri_da=False, nvidia_da=False) == [ - "cpu-x264", "cpu-x265", - ] - # Beides da → beides gemeldet - beides = caps.leite_backends_ab(mit_hw, dri_da=True, nvidia_da=True) - assert "vaapi" in beides # qsv_* vorhanden - assert "nvenc" in beides + backends = caps.leite_backends_ab(mit_hw) + assert "nvenc" in backends + assert "qsv" in backends + # Ohne Geräteprüfung — es gibt keine Parameter mehr, die das verhindern + assert caps.leite_backends_ab(mit_hw) == backends + + +def test_hardware_wird_nach_familie_unterschieden(): + """Vorher landete alles in „vaapi" — eine AMD-Karte lief damit unter dem + Intel-Namen, und das UI konnte nicht sagen, welches Preset passt.""" + amd = caps.leite_backends_ab(["x265", "vce_h264", "vce_h265"]) + assert "vce" in amd + assert "qsv" not in amd and "nvenc" not in amd + + intel = caps.leite_backends_ab(["x265", "qsv_h265"]) + assert "qsv" in intel and "vce" not in intel + + +def test_hardware_av1_bekommt_eigene_kennung(): + """AV1 in Hardware ist derzeit die beste Kombination aus Tempo und + Dateigröße — das darf nicht in „nvenc" verschwinden, sonst kann das UI es + nicht empfehlen.""" + backends = caps.leite_backends_ab(["x265", "nvenc_h265", "nvenc_av1"]) + assert "nvenc" in backends + assert "nvenc-av1" in backends + # Ohne AV1 in der Liste gibt es die Kennung auch nicht + assert "nvenc-av1" not in caps.leite_backends_ab(["nvenc_h265"]) def test_simd_stufe_erkennt_die_hoechste():