aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 17:16:29 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 17:16:29 +0200
commit0b9b85bf2d434177968dfc6e27555d54d703ff7e (patch)
tree7a8bf615f3e60f4bb68d1175c2e1acfce5a18d69 /packages
parent4d5a07299c792de1251622a41ce7f94870b363fa (diff)
downloadmeshbay-0b9b85bf2d434177968dfc6e27555d54d703ff7e.tar.gz
fix(node): a video the browser cannot decode is re-encoded, not refused
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GXmScYB1uR29YCt74si9J
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-node/src/meshbay_node/media_probe.py8
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py49
-rw-r--r--packages/meshbay-node/tests/test_stream_hevc_transcode.py155
-rw-r--r--packages/meshbay-node/tests/test_stream_video_transcode.py273
4 files changed, 324 insertions, 161 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/media_probe.py b/packages/meshbay-node/src/meshbay_node/media_probe.py
index e0c59e3..ad668fa 100644
--- a/packages/meshbay-node/src/meshbay_node/media_probe.py
+++ b/packages/meshbay-node/src/meshbay_node/media_probe.py
@@ -43,6 +43,14 @@ async def probe_video(
faithfully reports "hev1..." for a source this pipeline cannot actually
deliver copied.
+ **A `None` codec string means "this must be re-encoded", not "this cannot
+ be played".** Only the four codecs a browser can decode through MediaSource
+ are mapped; everything else — MPEG-4 Part 2 (Xvid, DivX), MPEG-2, VC-1,
+ WMV, Theora — has no string to report because there is no browser decoder
+ to report it to, and the streaming path answers that by re-encoding to
+ H264. It answered it by refusing until 2026-09-09, which read to the
+ operator as a broken file rather than as an unwired code path.
+
width/height come from the same ffprobe call (one extra `-show_entries`
field, no second process spawn) — resolution is deliberately never
guessed from the filename (docs/mediacenter.md §3.5).
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 507650a..a0efa83 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -5673,8 +5673,13 @@ class WebRTCPeerSession:
self._send({"type": "error", "detail": f"Probe failed: {e}"})
return
- if not codec_str:
- self._send({"type": "error", "detail": "Unsupported video codec"})
+ # No video stream at all is the only thing this path cannot serve, and
+ # it is the only thing refused here. A source with no MSE codec string
+ # is emphatically not that — it is the case the re-encode below exists
+ # for, and refusing it here (as "Unsupported video codec") is what this
+ # fixed.
+ if not raw_video_codec:
+ self._send({"type": "error", "detail": "No video stream in this file"})
return
# Where to begin. Seeking is a stream restarted somewhere else: the
@@ -5713,12 +5718,44 @@ class WebRTCPeerSession:
# decode problem as E-AC-3, just without ffmpeg also refusing to mux
# it). Transcoding audio is cheap; it does not change the cost model
# the transcode-slot semaphore is sized around.
- transcode_video = (
- raw_video_codec in BROWSER_INCOMPATIBLE_VIDEO_CODECS
- and self._ctx.get("transcode_incompatible_video", True)
- )
+ #
+ # Two kinds of source cannot be copied, and both re-encode:
+ #
+ # - one whose MSE codec string is real but that no mainstream browser
+ # decodes — HEVC, BROWSER_INCOMPATIBLE_VIDEO_CODECS;
+ # - one with **no MSE codec string at all**: MPEG-4 Part 2 (Xvid,
+ # DivX), MPEG-2, VC-1, WMV, Theora. `stream_init` has to carry a
+ # string the client puts through MediaSource.isTypeSupported, and
+ # `probe_video` returns None for these precisely because no browser
+ # has a MediaSource decoder for them, so there is none to carry.
+ # This second kind used to be refused outright with "Unsupported
+ # video codec" — which named the source's problem and not the
+ # node's answer to it, since ffmpeg re-encodes these in real time on
+ # any machine that can run this daemon. Reported live against an
+ # Xvid/MP3 .avi. `transcode_incompatible_video`'s own documentation
+ # (draft-v6 §2.11) already said "HEVC *and other browser-
+ # incompatible video codecs*"; only HEVC was ever wired up.
+ can_copy = (bool(codec_str)
+ and raw_video_codec not in BROWSER_INCOMPATIBLE_VIDEO_CODECS)
+ allow_transcode = self._ctx.get("transcode_incompatible_video", True)
+ transcode_video = not can_copy and allow_transcode
+ if not can_copy and not allow_transcode and not codec_str:
+ # The operator turned the fallback off and there is nothing to fall
+ # back *to*: a stream_init with no codec string is one the client
+ # refuses before the first byte arrives. Which of the two it is
+ # matters — "unsupported codec" sends the reader to look at the
+ # file, and the file is fine.
+ log.info("stream: %s is %s, which needs a re-encode, and "
+ "transcode_incompatible_video is off — refusing",
+ entry.name, raw_video_codec)
+ self._send({"type": "error",
+ "detail": "This video needs transcoding, which the "
+ "operator has turned off"})
+ return
map_args = ["-map", "0:v:0"]
if transcode_video:
+ log.info("stream: re-encoding %s (%s) to H264", entry.name,
+ raw_video_codec)
# -pix_fmt yuv420p: a 10-bit or 4:4:4 HEVC source (common for HDR
# WEB-DLs) fails "-profile:v high" outright otherwise — libx264's
# High profile is 8-bit 4:2:0 only. Downsampling loses nothing a
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.")
diff --git a/packages/meshbay-node/tests/test_stream_video_transcode.py b/packages/meshbay-node/tests/test_stream_video_transcode.py
new file mode 100644
index 0000000..69f4c2d
--- /dev/null
+++ b/packages/meshbay-node/tests/test_stream_video_transcode.py
@@ -0,0 +1,273 @@
+"""
+A video the browser cannot decode is re-encoded to H264, never refused and
+never copied. There are two kinds of those, and both are here.
+
+**A codec string that is real but useless.** 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 such a 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. That is what BROWSER_INCOMPATIBLE_VIDEO_CODECS names.
+
+**No codec string at all.** MPEG-4 Part 2 (Xvid, DivX), MPEG-2, VC-1, WMV and
+Theora have no MediaSource decoder anywhere, so `probe_video` maps them to
+None — there is nothing to put in `stream_init` for the client to check. Until
+2026-09-09 the streaming path read that None as "unsupported" and refused, with
+"Unsupported video codec", on files ffmpeg re-encodes in real time. Reported
+live against an episode rip in a `.avi` — mpeg4 video, mp3 audio, 720x404 —
+which the reporting machine re-encodes at about six times playback speed. The setting
+that governs it, `transcode_incompatible_video`, was documented from the start
+as covering "HEVC *and other browser-incompatible video codecs*" (draft-v6
+§2.11); only HEVC was ever wired up.
+
+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")
+_ENCODERS = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
+ capture_output=True).stdout if _HAVE_FFMPEG else b""
+# libx265 gates the HEVC cases and only those: an ffmpeg without it still
+# encodes MPEG-4 Part 2 (a built-in) and can therefore still prove the half of
+# this policy that has no codec string. Marking the whole module on it skipped
+# the reported defect on any box without x265.
+_HAVE_HEVC_ENCODER = b"libx265" in _ENCODERS
+_HAVE_MP3_ENCODER = b"libmp3lame" in _ENCODERS
+pytestmark = [
+ pytest.mark.asyncio,
+ pytest.mark.skipif(not _HAVE_FFMPEG, reason="ffmpeg/ffprobe not installed"),
+ needs_subprocess,
+]
+needs_hevc = pytest.mark.skipif(not _HAVE_HEVC_ENCODER,
+ reason="ffmpeg built without libx265")
+needs_mp3 = pytest.mark.skipif(not _HAVE_MP3_ENCODER,
+ reason="ffmpeg built without libmp3lame")
+
+
+def _make_mpeg4_clip(path: Path) -> None:
+ """~1s of Xvid-style MPEG-4 Part 2 video + MP3 audio in an AVI.
+
+ The same shape as the reported file, down to the container: ffprobe reports
+ `codec_name: mpeg4`, which is what an Xvid encoder produces and what
+ `probe_video` has no MSE string for.
+ """
+ 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", "mpeg4", "-c:a", "libmp3lame",
+ str(path)],
+ check=True, capture_output=True,
+ )
+
+
+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()
+
+
+@needs_hevc
+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"
+
+
+@needs_hevc
+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"
+
+
+@needs_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.")
+
+
+@needs_mp3
+async def test_probe_video_reports_no_codec_string_for_mpeg4(tmp_path):
+ """The other shape, and the reason the streaming path has to read it as
+ "re-encode this" rather than as a verdict on the file: there is no MSE
+ string for MPEG-4 Part 2 because no browser has a decoder to give one to.
+ The raw name is still reported, which is what the caller decides on."""
+ clip = tmp_path / "clip.avi"
+ _make_mpeg4_clip(clip)
+
+ codec, duration, has_audio, width, height, raw_codec = await _probe_video(str(clip))
+
+ assert raw_codec == "mpeg4"
+ assert codec is None, (
+ "a codec string for MPEG-4 Part 2 would be one no browser can act on")
+ assert has_audio is True
+ assert (width, height) == (320, 240)
+
+
+@needs_mp3
+async def test_a_codec_with_no_mse_string_is_transcoded_not_refused(tmp_path):
+ """The reported defect.
+
+ An Xvid/MP3 .avi answered "Unsupported video codec" — a refusal, on a file
+ ffmpeg re-encodes faster than it plays. Nothing about the source changed;
+ the node simply never reached its own re-encode path.
+ """
+ clip = tmp_path / "clip.avi"
+ _make_mpeg4_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"a source with no MSE codec string was refused instead of re-encoded: "
+ f"{errors}")
+
+ init = next(m for m in session.sent if m.get("type") == "stream_init")
+ assert init["codec"].startswith("avc1."), (
+ "stream_init must advertise the H264 the client is about to receive; "
+ f"there is no string for the source and none may be invented: {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 label")
+
+
+@needs_mp3
+async def test_the_operator_can_refuse_the_re_encode_and_says_so(tmp_path):
+ """`transcode_incompatible_video = false` still means what it says.
+
+ HEVC falls back to a copy there (the case above), because there is a codec
+ string to report. Here there is none, so a copy is not a lesser answer —
+ it is a stream_init the client refuses before the first byte. The refusal
+ has to name the setting: "Unsupported video codec" is what sent the last
+ reader to look at a file that was fine.
+ """
+ clip = tmp_path / "clip.avi"
+ _make_mpeg4_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})
+
+ errors = [m for m in session.sent if m.get("type") == "error"]
+ assert errors, "a stream that cannot be produced must be refused, not started"
+ detail = errors[0]["detail"]
+ assert "transcoding" in detail, (
+ 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")