From 0b9b85bf2d434177968dfc6e27555d54d703ff7e Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 9 Sep 2026 17:16:29 +0200 Subject: fix(node): a video the browser cannot decode is re-encoded, not refused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Streaming an Xvid/MP3 .avi answered "Unsupported video codec" — a refusal, on a file ffmpeg re-encodes at about six times playback speed on the machine that reported it. Nothing about the source was wrong. The node simply never reached its own re-encode path. `probe_video` maps a source codec to an MSE codec string and knows four: h264, hevc, vp9, av1. Everything else returns None, because there is no MediaSource decoder in any mainstream browser to give a string to — MPEG-4 Part 2 (Xvid, DivX), MPEG-2, VC-1, WMV, Theora. `_stream_video_inner` read that None as a verdict on the file and refused, while the re-encode sitting twenty lines below it was gated on `raw_video_codec in BROWSER_INCOMPATIBLE_VIDEO_CODECS` — a set containing "hevc" and nothing else. So the whole ffmpeg fallback existed, worked, and was unreachable for every codec that most needed it. The setting that governs the fallback has documented the intended behaviour since it was introduced: draft-v6 §2.11 says `transcode_incompatible_video` covers "HEVC *and other browser-incompatible video codecs*". Only HEVC was ever wired up. Two questions were being answered by one value, and they are separated now. "Is there a video stream at all" is the only thing this path genuinely cannot serve, and the only refusal left. "Can it be copied" needs both an MSE string to put in `stream_init` and a codec browsers decode; a source failing either is re-encoded. The operator's opt-out keeps meaning what it says, and it no longer means the same thing for every source, because it cannot: HEVC has a codec string, so `transcode_incompatible_video = false` falls back to a copy and the viewer's own decoder decides (unchanged). MPEG-4 Part 2 has none, so there is nothing to fall back to — a `stream_init` with no codec string is one the client refuses before the first byte — and the stream is refused naming the setting. "Unsupported video codec" is what sent this report to the file, and the file was fine. Verified against the reported file end to end: ffprobe reports mpeg4/mp3 720x404, the decision comes out `can_copy=False`, and the pipeline's exact argv produces H264 High level 4.1 plus stereo AAC-LC — matching the `avc1.640029,mp4a.40.2` that `stream_init` advertises and that the client puts through MediaSource.isTypeSupported byte for byte. test_stream_hevc_transcode.py becomes test_stream_video_transcode.py: it was always about the policy rather than about one codec, and it now carries both halves of it, with a synthetic Xvid/MP3 .avi built the same way as the HEVC clip. Its module-level skip on libx265 went with it — an ffmpeg without x265 still encodes MPEG-4 Part 2, so that marker was skipping the reported defect entirely on any box without it; it now gates the HEVC cases alone. Three cases added, checked against the unfixed source. Hub and node suites 2269 passed, 4 skipped. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019GXmScYB1uR29YCt74si9J --- .../tests/test_stream_hevc_transcode.py | 155 --------------------- 1 file changed, 155 deletions(-) delete mode 100644 packages/meshbay-node/tests/test_stream_hevc_transcode.py (limited to 'packages/meshbay-node/tests/test_stream_hevc_transcode.py') diff --git a/packages/meshbay-node/tests/test_stream_hevc_transcode.py b/packages/meshbay-node/tests/test_stream_hevc_transcode.py deleted file mode 100644 index 7d831ee..0000000 --- a/packages/meshbay-node/tests/test_stream_hevc_transcode.py +++ /dev/null @@ -1,155 +0,0 @@ -""" -HEVC video is transcoded to H264 for streaming, never copied — unlike the -codecs media_probe.py's BROWSER_INCOMPATIBLE_VIDEO_CODECS excludes. - -Found live: a real HEVC/EAC3 WEB-DL streamed fine over MNP (ffprobe/VLC play -it) but the browser reported "Codec not supported for streaming: -hev1.1.6.L93.B0,mp4a.40.2" from MediaSource.isTypeSupported — Chrome has no -HEVC decoder on most non-Apple platforms. "-c:v copy" on an incompatible -codec is not a mux failure the way EAC3 audio is (test_stream_audio_ -transcode.py); ffmpeg happily remuxes it, and the browser is the one that -then refuses it, silently, at playback rather than at stream_init. - -These tests spawn real ffmpeg/ffprobe against small synthetic files (lavfi -test sources, ~1s), the same style as test_stream_audio_transcode.py. -""" - -import shutil -import subprocess -from pathlib import Path - -import pytest -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.indexer.group_index import GroupIndex -from meshbay_node.transport.webrtc_server import WebRTCPeerSession, _probe_video - -from conftest import needs_subprocess, one_root - -_HAVE_FFMPEG = shutil.which("ffmpeg") and shutil.which("ffprobe") -_HAVE_HEVC_ENCODER = _HAVE_FFMPEG and b"libx265" in subprocess.run( - ["ffmpeg", "-hide_banner", "-encoders"], capture_output=True).stdout -pytestmark = [ - pytest.mark.asyncio, - pytest.mark.skipif(not _HAVE_HEVC_ENCODER, reason="ffmpeg/libx265 not installed"), - needs_subprocess, -] - - -def _make_hevc_clip(path: Path) -> None: - """~1s of HEVC video + AAC audio — a minimal stand-in for a real HEVC WEB-DL.""" - subprocess.run( - ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", - "-f", "lavfi", "-i", "testsrc=size=320x240:rate=25:duration=1", - "-f", "lavfi", "-i", "sine=frequency=440:duration=1:sample_rate=48000", - "-c:v", "libx265", "-preset", "ultrafast", "-c:a", "aac", - str(path)], - check=True, capture_output=True, - ) - - -def _session(video_path: Path, gek: bytes, *, transcode_incompatible_video: bool = True): - import blake3 - file_bytes = video_path.read_bytes() - file_id = blake3.blake3(file_bytes).hexdigest() - - sk_node = Ed25519PrivateKey.generate() - index = GroupIndex(group_id="g" * 32, sk_node=sk_node, gek=gek) - from meshbay_common.protocol import IndexEntry - index.add_entry(IndexEntry( - id=file_id, name=video_path.name, path=video_path.parent.name, - size=len(file_bytes), type="video", added_at=0)) - - session = WebRTCPeerSession.__new__(WebRTCPeerSession) - session._ctx = { - "roots": one_root(video_path.parent), - "index": index, - "gek": gek, - "sk_node": sk_node, - "max_concurrent_streams": 4, - "transcode_incompatible_video": transcode_incompatible_video, - } - session._group_id = None - session._user_id = "tester" - session._stream_stopped = False - session._stream_keepalives = 0 - session.sent = [] - session._send = session.sent.append - session._audit = lambda *a, **k: None - return session, file_id - - -def _reassemble(sent: list[dict], gek: bytes, file_id: str) -> bytes: - file_hash = bytes.fromhex(file_id) - segments = sorted( - (m for m in sent if m.get("type") == "stream_data"), - key=lambda m: m["segment_index"]) - out = b"" - for m in segments: - key = chunk_key_aes(gek, file_hash, m["segment_index"]) - out += decrypt_chunk_aes(key, m["nonce"], m["ct"]) - return out - - -def _output_video_codec(path: Path) -> str: - probe = subprocess.run( - ["ffprobe", "-v", "error", "-select_streams", "v:0", - "-show_entries", "stream=codec_name", "-of", "csv=p=0", str(path)], - check=True, capture_output=True, text=True) - return probe.stdout.strip() - - -async def test_hevc_video_is_transcoded_to_h264_by_default(tmp_path): - clip = tmp_path / "clip.mkv" - _make_hevc_clip(clip) - gek = generate_gek() - session, file_id = _session(clip, gek) - - await session._stream_video_inner({"file_id": file_id, "start": 0, "credits": 0}) - - errors = [m for m in session.sent if m.get("type") == "error"] - assert not errors, f"streaming must not fail: {errors}" - - init = next(m for m in session.sent if m.get("type") == "stream_init") - assert init["codec"].startswith("avc1."), ( - "the reported codec must be the transcoded H264 string, never the " - f"source's hev1 string a browser cannot decode: {init['codec']}") - - 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 bytes on the wire must actually be H264, not just the reported label" - - -async def test_hevc_transcode_can_be_disabled_by_the_operator(tmp_path): - clip = tmp_path / "clip.mkv" - _make_hevc_clip(clip) - gek = generate_gek() - session, file_id = _session(clip, gek, transcode_incompatible_video=False) - - 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"] - init = next(m for m in session.sent if m.get("type") == "stream_init") - assert init["codec"].startswith("hev1."), ( - "with the fallback disabled, the source is copied as-is and the " - f"original HEVC codec string must be reported unchanged: {init['codec']}") - - remuxed = _reassemble(session.sent, gek, file_id) - out_path = tmp_path / "out.mp4" - out_path.write_bytes(remuxed) - assert _output_video_codec(out_path) == "hevc", \ - "with the fallback disabled, the wire bytes must still be copied HEVC" - - -async def test_probe_video_reports_raw_codec_name_for_hevc(tmp_path): - clip = tmp_path / "clip.mkv" - _make_hevc_clip(clip) - - codec, duration, has_audio, width, height, raw_codec = await _probe_video(str(clip)) - - assert raw_codec == "hevc" - assert codec is not None and codec.startswith("hev1.") -- cgit v1.2.3