summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/tests/test_e2e_local.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-15 10:17:29 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-15 10:17:29 +0200
commita0be6bfcac07a256949082e935e1be06753ab872 (patch)
treec6f531254e0abf8303080ba6287cf1fc405a72c1 /packages/meshbay-cli/tests/test_e2e_local.py
parent2f90f4b60cd50f7e9649daaf8e76a120e608cff6 (diff)
downloadmeshbay-a0be6bfcac07a256949082e935e1be06753ab872.tar.gz
feat(cli): stream — a film or a song played in a media player
`meshbay-cli stream PATH` serves one video or sound file over HTTP on 127.0.0.1 for VLC, mpv or any player that reads HTTP; `--player PROGRAM` starts the player, and closing it stops the stream, as Ctrl+C does. The file goes as the node stores it: no remux, no transcode, no stream slot on the node. Byte ranges are answered from leaseless file_req chunks decrypted in memory, eight ahead and the last 24 kept. Loopback only, a random token in the address, a foreign Host refused (DNS rebinding), an audio or video type with nosniff. The connection is pinged and reopened when it dies; a refusal by the node stops the stream (exit 5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-cli/tests/test_e2e_local.py')
-rw-r--r--packages/meshbay-cli/tests/test_e2e_local.py78
1 files changed, 78 insertions, 0 deletions
diff --git a/packages/meshbay-cli/tests/test_e2e_local.py b/packages/meshbay-cli/tests/test_e2e_local.py
index fb251bd..c385bb6 100644
--- a/packages/meshbay-cli/tests/test_e2e_local.py
+++ b/packages/meshbay-cli/tests/test_e2e_local.py
@@ -1172,3 +1172,81 @@ def test_chat_follow_against_the_real_node(world):
_, err = follower.communicate(timeout=20)
assert follower.returncode == 130
assert "reconnecting" not in err, err
+
+
+@pytest.mark.skipif(not (shutil.which("ffmpeg") and shutil.which("ffprobe")
+ and shutil.which("cvlc")),
+ reason="ffmpeg, ffprobe and VLC are needed")
+def test_stream_a_film_to_real_players(world, tmp_path):
+ """A film put on the real node, then streamed: ffprobe reads it, ffmpeg seeks
+ near its end, a foreign Host is refused, Ctrl+C stops it; then VLC started
+ with --player plays it to the end and, closing, ends the stream."""
+ import signal
+ _cli_signed_in(world, "cli-stream")
+ film = tmp_path / "Night Drive.mkv"
+ subprocess.run(["ffmpeg", "-v", "error", "-f", "lavfi", "-i",
+ "testsrc=duration=12:size=640x360:rate=25", "-f", "lavfi", "-i",
+ "sine=frequency=440:duration=12", "-vf", "noise=alls=30:allf=t",
+ "-c:v", "libx264", "-b:v", "3M",
+ "-g", "50", "-c:a", "aac", "-shortest", str(film)], check=True)
+ assert film.stat().st_size > 3 * 1024 * 1024 # several of the node's chunks
+ r = cli(world, "put", str(film), f"{_handle()}/share", home="cli-stream")
+ # After a test that withdrew the node's copy of the keys, this machine is a
+ # new device: approved as the application would approve it.
+ if r.returncode == 5 and "Waiting for this machine to be approved" in r.stderr:
+ code = re.search(r"code ([0-9A-Z]{4}-[0-9A-Z]{4})", r.stderr).group(1)
+ asyncio.run(world["app"].approve(code))
+ r = cli(world, "put", str(film), f"{_handle()}/share", home="cli-stream")
+ assert r.returncode == 0, r.stderr
+ path = f"{_handle()}/share/Night Drive.mkv"
+ deadline = time.monotonic() + 60 # once the node has indexed it
+ while "Night Drive.mkv" not in cli(world, "ls", f"{_handle()}/share",
+ home="cli-stream").stdout:
+ assert time.monotonic() < deadline, "the film was never indexed"
+ time.sleep(1)
+
+ env = {**os.environ, "MESHBAY_CLI_HOME": str(world["root"] / "cli-stream"),
+ "PYTHON_KEYRING_BACKEND": "keyring.backends.fail.Keyring"}
+ exe = os.environ.get("MESHBAY_CLI_E2E_BIN") or str(BIN / "meshbay-cli")
+ streamer = subprocess.Popen([exe, "--json", "stream", path], env=env,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+ try:
+ line = streamer.stdout.readline()
+ assert line, streamer.stderr.read()
+ shown = json.loads(line)
+ url = shown["url"]
+ assert shown["size"] == film.stat().st_size
+ probe = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
+ "format=duration:stream=codec_name", "-of", "json", url],
+ capture_output=True, text=True, timeout=60)
+ assert probe.returncode == 0, probe.stderr
+ info = json.loads(probe.stdout)
+ assert abs(float(info["format"]["duration"]) - 12) < 0.5
+ assert {s["codec_name"] for s in info["streams"]} == {"h264", "aac"}
+ frame = tmp_path / "frame.png"
+ seek = subprocess.run(["ffmpeg", "-v", "error", "-ss", "10", "-i", url,
+ "-frames:v", "1", str(frame)],
+ capture_output=True, text=True, timeout=60)
+ assert seek.returncode == 0 and frame.stat().st_size > 0, seek.stderr
+ port = url.split("/")[2].split(":")[1]
+ foreign = httpx.get(url, headers={"Host": f"rebound.example:{port}"}, timeout=10,
+ trust_env=False)
+ assert foreign.status_code == 403
+ finally:
+ streamer.send_signal(signal.SIGINT)
+ _, err = streamer.communicate(timeout=20)
+ assert streamer.returncode == 130, err
+ with pytest.raises(httpx.HTTPError):
+ httpx.get(url, timeout=5, trust_env=False)
+
+ player = tmp_path / "vlc-to-the-end"
+ log = tmp_path / "vlc.log"
+ player.write_text("#!/bin/sh\nexec cvlc --play-and-exit --intf dummy --vout dummy "
+ f"--aout dummy \"$1\" >{log} 2>&1\n")
+ player.chmod(0o755)
+ started = time.monotonic()
+ r = subprocess.run([exe, "stream", "--player", str(player), path], env=env,
+ capture_output=True, text=True, timeout=120)
+ assert r.returncode == 0, r.stderr + log.read_text()
+ assert "The player was closed" in r.stderr
+ assert time.monotonic() - started > 10, "VLC played the film, in real time"