aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-19 13:45:56 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-19 13:45:56 +0200
commitf8223293a211a87c92b1fed80f5ca53660f6b26c (patch)
tree02beb40e677389ade10c15d6040b19d67bb2dd93
parenteca01c7f970d2d3ab2298f934da9776fe01179c6 (diff)
downloadmeshbay-f8223293a211a87c92b1fed80f5ca53660f6b26c.tar.gz
fix(node): the node decides which files it will transcode, not the player
`BROWSER_INCOMPATIBLE_AUDIO_EXTS` named the two formats no browser decodes and was read by nothing. The player asked for `.wma` and `.mpc` and no others, and `music-player.js` called itself "kept in sync with the node's" constant — so the entire restriction lived in the caller, and a member's own message is not the caller. What that let through: this converts a whole file and holds a transcode slot shared with video streaming while it runs. One message naming a two-hour film spends minutes of the operator's CPU and a slot every other viewer is queued behind. `AUDIO_TRANSCODE_MAX_BYTES` catches the result, after the work is done; only the extension catches the work. The client comment now says what it is — an optimisation that saves asking for a refusal — rather than implying it is the rule. Two tests: a film is refused before ffmpeg is reached, and a WMA still passes, because a gate that refuses everything is not a gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-player.js9
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py19
-rw-r--r--packages/meshbay-node/tests/test_audio_transcode.py36
3 files changed, 61 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
index 7c81f4e..5f2e33c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
@@ -36,9 +36,12 @@ const MIME_BY_EXT = {
wav: 'audio/wav', aac: 'audio/aac', m4a: 'audio/mp4',
};
-// Kept in sync with the node's BROWSER_INCOMPATIBLE_AUDIO_EXTS
-// (webrtc_server.py) — both name the same two formats no mainstream
-// browser's <audio> element decodes natively.
+// The same two formats as the node's BROWSER_INCOMPATIBLE_AUDIO_EXTS
+// (webrtc_server.py), which is the one that decides: the node refuses a
+// transcode request for anything else. This is here so the player does not
+// ask for one it knows will be refused — not because it enforces the rule.
+// It used to be the only thing that did, and a member's own message never
+// passed through it.
const NEEDS_TRANSCODE_RE = /\.(wma|mpc)$/i;
function guessMime(name) {
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 d2ec1de..ba8a3e4 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -4039,6 +4039,25 @@ class WebRTCPeerSession:
if not entry:
self._send({"type": "error", "detail": "File not found"})
return
+
+ # The gate `BROWSER_INCOMPATIBLE_AUDIO_EXTS` exists for, applied where it
+ # costs something. Nothing on the node read it: the player asks for these
+ # two extensions and no others, and `music-player.js` described itself as
+ # "kept in sync with the node's" constant — so the whole restriction lived
+ # in the caller, and a member's own message is not the caller.
+ #
+ # What that let through: this converts a *whole file* and holds a
+ # transcode slot shared with video streaming while it runs. Pointed at a
+ # two-hour film it spends minutes of the operator's CPU and a slot every
+ # other viewer is queued behind. `AUDIO_TRANSCODE_MAX_BYTES` catches the
+ # result, after the work; only this catches the work.
+ if Path(entry.name).suffix.lower() not in BROWSER_INCOMPATIBLE_AUDIO_EXTS:
+ self._send({
+ "type": "error",
+ "detail": "This file does not need transcoding — play it directly.",
+ "code": "transcode_not_applicable",
+ })
+ return
file_path, refusal = await off_disk(ctx["roots"], _locate, ctx["roots"], entry)
if refusal is not None:
self._send({"type": "error", "detail": refusal})
diff --git a/packages/meshbay-node/tests/test_audio_transcode.py b/packages/meshbay-node/tests/test_audio_transcode.py
index 3e6015d..a6557f0 100644
--- a/packages/meshbay-node/tests/test_audio_transcode.py
+++ b/packages/meshbay-node/tests/test_audio_transcode.py
@@ -191,3 +191,39 @@ async def test_multi_chunk_cached_blob_reassembles_correctly(tmp_path, media_cac
assert len(chunk_msgs) == total_chunks
reassembled = _reassemble_file_chunks(session.sent, gek, blob_hash)
assert reassembled == blob
+
+
+async def test_only_the_two_formats_that_need_it_are_transcoded(tmp_path, media_cache):
+ """
+ The gate that was written down and never applied.
+
+ `BROWSER_INCOMPATIBLE_AUDIO_EXTS` was read by nobody: the player asked only
+ for `.wma` and `.mpc`, and the node converted whatever file id it was given.
+ A member's own message is not the player, and this conversion is whole-file
+ while holding a transcode slot shared with video streaming — so one message
+ naming a two-hour film spends minutes of the operator's CPU and a slot every
+ other viewer is queued behind. The size cap catches the result; only this
+ catches the work.
+ """
+ clip = tmp_path / "feature.mkv"
+ clip.write_bytes(b"not really a film, and never opened")
+ session, file_id = _session(tmp_path, clip, generate_gek(), media_cache)
+
+ await session._do_audio_transcode_request({"file_id": file_id})
+
+ (msg,) = session.sent
+ assert msg["type"] == "error"
+ assert msg["code"] == "transcode_not_applicable"
+
+
+@pytest.mark.skipif(not _HAVE_FFMPEG, reason="ffmpeg/ffprobe not installed")
+async def test_the_two_formats_that_do_need_it_still_pass(tmp_path, media_cache):
+ """The gate must admit what it exists for; a refusal of everything is not a gate."""
+ clip = tmp_path / "clip.wma"
+ _make_wma_clip(clip)
+ session, file_id = _session(tmp_path, clip, generate_gek(), media_cache)
+
+ await session._do_audio_transcode_request({"file_id": file_id})
+
+ assert [m for m in session.sent if m.get("type") == MNP.AUDIO_TRANSCODE_RESP], (
+ f"a WMA file was refused: {session.sent}")