From d22ce0ba5aebfff5ffaea97982472f876af5da22 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 18 Sep 2026 14:37:08 +0200 Subject: feat: decode and re-encode video on the GPU where there is one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 1080p film is decoded by whoever watches it and re-encoded by the node when no browser can decode the source. Both were on the CPU, and on an Atom or Celeron mini-PC neither reaches real time — which is what `transcode_incompatible_video` exists to refuse. This adds the mechanism that makes refusing it unnecessary. Node — `hwaccel.py`: VA-API on Linux, Quick Sync or NVENC on Windows, established by encoding 1080p and reading the file back with ffprobe. Nothing is accepted that does not produce the exact profile and level `stream_init` announces, since the client checks that string before it trusts a byte: an encoder that wrote another level would make the node's own codec string a lie, and ffmpeg takes `-level 4.1` and `-level 41` from h264_qsv without saying which it understood. Three modes per stream — hardware decode and encode, hardware encode alone, libx264 — demoted per source codec, because a GPU that decodes HEVC may have no decoder for MPEG-4 Part 2 and only asking it finds out. A mode that fails is detected on an empty stdout before `stream_init` goes out, so the viewer sees one working stream and never an error. Client — Chromium ships VA-API off on Linux. It is enabled where a render node and a driver are present, then verified through `navigator.mediaCapabilities`: a no moves to the next GL backend on the next launch and an exhausted list drops the switches, so a renamed feature cannot pass for a feature that is on and `--ignore-gpu-blocklist` cannot survive on a machine it did not help. Feature lists now merge rather than overwrite — `appendSwitch` replaces the value, and a second caller would have silently cancelled the mDNS switch aiortc depends on. Packaging — the drivers are weak dependencies on all four formats, so a machine without a GPU installs exactly as before. `dpkg -i` and `rpm -ivh` ignore weak deps; `packaging/README.md` now says so. Windows needs no driver: the bundled ffmpeg already carries h264_qsv and h264_nvenc, and a re-pin that dropped them would cost every low-power Windows node its hardware encoding silently. AMD on Windows (AMF) and macOS (VideoToolbox) are named gaps, not oversights: neither could be tried anywhere in this project, and both re-encode in software as before. Co-Authored-By: Claude Opus 5 --- packages/meshbay-node/tests/test_hwaccel.py | 171 +++++++++++++++++++++ .../tests/test_stream_video_transcode.py | 48 ++++++ 2 files changed, 219 insertions(+) create mode 100644 packages/meshbay-node/tests/test_hwaccel.py (limited to 'packages/meshbay-node/tests') diff --git a/packages/meshbay-node/tests/test_hwaccel.py b/packages/meshbay-node/tests/test_hwaccel.py new file mode 100644 index 0000000..a79d784 --- /dev/null +++ b/packages/meshbay-node/tests/test_hwaccel.py @@ -0,0 +1,171 @@ +""" +The GPU re-encoding plan, and the one thing that must stay true across both +encoders. + +`hwaccel.py` exists so that a low-power node can re-encode a browser-hostile +video at all — see its module docstring. What is tested here is the part that +holds whether or not the machine running the tests has a GPU: the shape of the +plan, what a demotion does, and the agreement between the codec string the node +announces and the arguments each encoder is actually given. + +The end-to-end fallback — a hardware mode that fails, the stream playing anyway +— is in `test_stream_video_transcode.py`, because it needs a real ffmpeg and a +real file. +""" + +import re +import sys +from pathlib import Path + +import pytest +from meshbay_node import hwaccel +from meshbay_node.transport import webrtc_server + +from conftest import needs_subprocess + +VAAPI = next(c for c in hwaccel.CANDIDATES if c.name == "vaapi") +FAKE = hwaccel.Encoder(candidate=VAAPI, variant="standard", extra=(), + device="/dev/dri/renderD128") + + +@pytest.fixture(autouse=True) +def _clean(): + hwaccel._reset_for_tests() + yield + hwaccel._reset_for_tests() + hwaccel.set_enabled(True) + + +async def test_no_working_encoder_means_the_software_path_and_nothing_else(monkeypatch): + """ + The guarantee this module owes every node that has no GPU, no driver, or a + driver that fails its test encode: one mode, the one that was always there. + A second mode would mean a second ffmpeg spawn and a second wait on a + machine that can never benefit from either. + """ + monkeypatch.setattr(hwaccel, "encoder", _fixed(None)) + + assert await hwaccel.modes_for("hevc") == [hwaccel.SW] + + +async def test_the_plan_always_ends_in_software(monkeypatch): + monkeypatch.setattr(hwaccel, "encoder", _fixed(FAKE)) + + assert await hwaccel.modes_for("hevc") == [hwaccel.HW, hwaccel.HWENC, hwaccel.SW] + + +async def test_a_demotion_is_remembered_for_that_source_codec_only(monkeypatch): + """ + iHD decodes HEVC and has no MPEG-4 Part 2 decoder at all, so "the GPU + cannot do this one" is a fact about the pair, not about the machine. + Demoting the machine would cost every HEVC film the acceleration that one + Xvid file proved was unavailable to *it*. + """ + monkeypatch.setattr(hwaccel, "encoder", _fixed(FAKE)) + hwaccel.demote("mpeg4", hwaccel.HW, "no decoder") + + assert await hwaccel.modes_for("mpeg4") == [hwaccel.HWENC, hwaccel.SW] + assert await hwaccel.modes_for("hevc") == [hwaccel.HW, hwaccel.HWENC, hwaccel.SW] + + +async def test_the_operator_switch_turns_off_the_probe_itself(monkeypatch): + """`hardware_video_encode = false` must cost no subprocess, not merely go + unused: the probe is a test encode, and a node whose operator said no + should never spawn it.""" + spawned = [] + monkeypatch.setattr(hwaccel, "_test_encode", + lambda *a, **k: spawned.append(a) or None) + hwaccel.set_enabled(False) + + assert await hwaccel.encoder() is None + assert await hwaccel.modes_for("hevc") == [hwaccel.SW] + assert spawned == [] + + +def test_every_encoder_produces_what_the_node_announces(): + """ + `stream_init` carries `avc1.640029` and the client puts it through + MediaSource.isTypeSupported before it trusts a byte of the stream. The + string is not a label chosen next to libx264 — it is a claim about the + bytes, and it has to stay true for whichever encoder produced them. + + So the claim is read out of the streaming code and decoded (profile_idc + 0x64 = 100 = High, level_idc 0x29 = 41 = 4.1) rather than restated here, + and every mode is checked against it. Changing the arguments of one encoder + without the other, or changing either without the announced string, fails. + """ + source = Path(webrtc_server.__file__).read_text(encoding="utf-8") + announced = set(re.findall(r"avc1\.([0-9a-f]{6})", source)) + assert announced == {"640029"}, ( + "the streaming path announces a codec string this test does not know " + f"how to check: {announced}") + + (code,) = announced + profile_idc, constraints, level_idc = (int(code[i:i + 2], 16) for i in (0, 2, 4)) + assert (profile_idc, constraints, level_idc) == (100, 0, 41), ( + "the announced string no longer says High/4.1, so the arguments below " + "are no longer what it claims") + + for mode in (hwaccel.SW, hwaccel.HW, hwaccel.HWENC): + args = hwaccel.codec_args(mode, FAKE) + assert args[args.index("-profile:v") + 1] == "high", \ + f"{mode} does not encode the High profile the node announces" + assert args[args.index("-level") + 1] == "4.1", \ + f"{mode} does not encode at the level the node announces" + + +def test_the_software_mode_never_touches_the_gpu(): + """The fallback has to be a real fallback: if `sw` carried a VA-API + argument, the mode that exists for machines where the GPU failed would + fail with it.""" + for args in (hwaccel.input_args(hwaccel.SW, FAKE), + hwaccel.codec_args(hwaccel.SW, FAKE)): + joined = " ".join(args) + assert "vaapi" not in joined and "hwupload" not in joined, joined + assert hwaccel.input_args(hwaccel.HW, None) == [] + assert "libx264" in hwaccel.codec_args(hwaccel.HW, None) + + +def _fixed(value): + async def _f(): + return value + return _f + + +@needs_subprocess +async def test_the_probe_accepts_only_what_the_node_announces(): + """The acceptance test is neither always true nor always false. + + Both halves are run with libx264, which is on any machine that can run + these tests, so what is being checked is the judgement itself rather than + anybody's GPU: the same encoder is accepted when it writes High at level + 4.1 and rejected when it writes level 3.1. Without this, an encoder whose + arguments were spelled in a way ffmpeg accepts but misreads would pass the + probe and then announce a level it had not produced. + """ + def _software(label: str, extra: tuple[str, ...]) -> hwaccel.Candidate: + return hwaccel.Candidate( + name=label, encoder="libx264", platforms=(sys.platform,), + hwaccel="none", hw_filter="null", sw_filter="format=yuv420p", + variants=((label, extra),)) + + right = _software("as announced", ("-preset", "veryfast", "-crf", "30")) + assert await hwaccel._test_encode(right, None, right.variants[0][1]) is None + + wrong = _software("a level lower", + ("-preset", "veryfast", "-crf", "30", "-level", "3.1")) + why = await hwaccel._test_encode(wrong, None, wrong.variants[0][1]) + assert why and "31" in why, ( + f"an encoder that wrote another level must be rejected, and say so: {why!r}") + + +@needs_subprocess +async def test_a_missing_encoder_is_rejected_rather_than_raised(): + """ffmpeg exits non-zero for an encoder it does not have; nothing here may + turn that into an exception, because it happens on every machine that lacks + one of the three candidates — which is most of them.""" + absent = hwaccel.Candidate( + name="not a thing", encoder="h264_definitely_not", platforms=(sys.platform,), + hwaccel="none", hw_filter="null", sw_filter="null", variants=(("x", ()),)) + + assert await hwaccel._test_encode(absent, None, ()) is not None diff --git a/packages/meshbay-node/tests/test_stream_video_transcode.py b/packages/meshbay-node/tests/test_stream_video_transcode.py index b165af4..0fd12c6 100644 --- a/packages/meshbay-node/tests/test_stream_video_transcode.py +++ b/packages/meshbay-node/tests/test_stream_video_transcode.py @@ -35,6 +35,7 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.crypto import generate_gek from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes +from meshbay_node import hwaccel from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.transport.webrtc_server import WebRTCPeerSession, _probe_video @@ -271,3 +272,50 @@ async def test_the_operator_can_refuse_the_re_encode_and_says_so(tmp_path): f"the refusal must point at the setting that caused it: {detail!r}") assert not [m for m in session.sent if m.get("type") == "stream_init"], ( "a stream_init went out with no codec string the client could check") + + +@needs_mp3 +async def test_a_hardware_encoder_that_does_not_work_costs_the_viewer_nothing( + tmp_path, monkeypatch): + """A VA-API mode that fails falls through to libx264 and the film plays. + + This is the failure that cannot be prevented by probing: `hwaccel.py` + proves the *encoder* with a test encode at startup, and nothing proves the + GPU can decode a particular source until it is asked to — iHD has no + MPEG-4 Part 2 decoder at all, which is exactly the file used here. + + The encoder is faked onto `/dev/null` rather than skipped for want of a + GPU, so this runs identically on a machine with one and on a machine + without: both hardware modes fail instantly, and what is tested is that + the viewer sees one working stream and not an error. The demotions are + checked too — a library of Xvid files must not pay for the first one's two + dead spawns again and again. + """ + clip = tmp_path / "clip.avi" + _make_mpeg4_clip(clip) + gek = generate_gek() + session, file_id = _session(clip, gek) + + hwaccel._reset_for_tests() + + async def _broken_encoder(): + vaapi = next(c for c in hwaccel.CANDIDATES if c.name == "vaapi") + return hwaccel.Encoder(candidate=vaapi, variant="standard", extra=(), + device="/dev/null") + + monkeypatch.setattr(hwaccel, "encoder", _broken_encoder) + try: + await session._stream_video_inner({"file_id": file_id, "start": 0, "credits": 0}) + + assert not [m for m in session.sent if m.get("type") == "error"], ( + "a hardware encoder that does not work must cost the viewer nothing") + remuxed = _reassemble(session.sent, gek, file_id) + out_path = tmp_path / "out.mp4" + out_path.write_bytes(remuxed) + assert _output_video_codec(out_path) == "h264", \ + "the software fallback must still deliver a playable H264 stream" + + assert ("mpeg4", hwaccel.HW) in hwaccel._demoted + assert ("mpeg4", hwaccel.HWENC) in hwaccel._demoted + finally: + hwaccel._reset_for_tests() -- cgit v1.2.3