test: four-rung suite that asserts on decoded pixels, not log lines
130 tests, ~20s, 94% coverage. Four rungs, each proving something the one below it cannot: registry admission, fan-out, signal authorization and teardown, no I/O wire camelCase round-trip, malformed frames, WebSocket lifecycle e2e real capture -> encode -> SRTP -> decode -> painted frames realdisplay the actual X11 capture backend on a private Xvfb display The e2e rung drives Chromium's fake media device, which produces a genuine moving 1280x720 monitor stream, so the assertion is that a watcher's <video> has a size and that its pixels change over time -- a frozen or never-started peer connection fails the suite, which a "signal arrived" assertion would not have caught. Findings encoded as flags and comments, because each cost a debugging cycle: the headless shell has no media stack at all (use channel="chromium"); mediaDevices is undefined off a secure context (serve over 127.0.0.1); and --use-fake-ui-for-media-stream breaks real X11 capture by approving the request without ever selecting a source, so the realdisplay suite selects the source explicitly instead. The realdisplay fixture strips WAYLAND_DISPLAY and DBUS_SESSION_BUS_ADDRESS from the browser environment. Without that, a headed Chromium reaches xdg-desktop-portal, mirrors the developer's actual desktop and raises a permission dialog on their real screen -- which is exactly what happened before this was isolated. Skipped unless MIRROR_REALDISPLAY=1. Also guards the frontend's structure: scripts must parse as ES modules, every id a script looks up must exist in its page, ids must not collide across pages that share one stylesheet, and nothing may load from a CDN. LLM-Generated: true
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
"""The Linux-native capture path: a real X11 screen, no fake media devices.
|
||||
|
||||
The rest of the suite drives Chromium's fake capture device, which is
|
||||
deterministic but synthetic. This module is the counterweight: it starts its own
|
||||
Xvfb server, runs a *headed* Chromium on it, and asks that browser to mirror the
|
||||
whole monitor through Chromium's real X11 capture backend. A second Chromium
|
||||
process — a different machine as far as WebRTC cares — watches it.
|
||||
|
||||
Skipped unless ``MIRROR_REALDISPLAY=1``, because it needs an X server binary and
|
||||
a headed browser::
|
||||
|
||||
MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from playwright.sync_api import expect, sync_playwright
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.e2e,
|
||||
pytest.mark.realdisplay,
|
||||
pytest.mark.timeout(240),
|
||||
pytest.mark.skipif(shutil.which("Xvfb") is None, reason="Xvfb is not installed"),
|
||||
pytest.mark.skipif(
|
||||
os.environ.get("MIRROR_REALDISPLAY", "") not in {"1", "true", "yes"},
|
||||
reason="set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend",
|
||||
),
|
||||
]
|
||||
|
||||
# Deliberately not 16:9: Chromium's fake device reports 1280x720 no matter what,
|
||||
# so an aspect ratio of exactly 1280/800 is a fingerprint of the real screen.
|
||||
XVFB_GEOMETRY = "1280x800x24"
|
||||
XVFB_ASPECT = 1280 / 800
|
||||
FAKE_DEVICE_SIZE = (1280, 720)
|
||||
|
||||
# Flags learned the hard way, and worth keeping written down:
|
||||
#
|
||||
# * No `--use-fake-device-for-media-stream`: that is exactly what we are trying
|
||||
# not to use here.
|
||||
# * No `--use-fake-ui-for-media-stream` either. It short-circuits the picker
|
||||
# *before* the X11 backend selects a source, and every capture then dies with
|
||||
# `NotReadableError: Could not start video source` in ~55ms. Choosing the
|
||||
# source with `--auto-select-desktop-capture-source` grants and selects in one
|
||||
# step, which is what makes unattended desktop capture work.
|
||||
# * `--ozone-platform=x11` keeps Chromium on its native X11 capturer instead of
|
||||
# xdg-desktop-portal/PipeWire. Combined with `browser_env()` this makes a
|
||||
# permission dialog on the developer's own session impossible.
|
||||
REAL_CAPTURE_ARGS = [
|
||||
"--auto-select-desktop-capture-source=Entire screen",
|
||||
"--ozone-platform=x11",
|
||||
"--autoplay-policy=no-user-gesture-required",
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--no-proxy-server",
|
||||
]
|
||||
|
||||
# Never reachable: without a session bus and without a Wayland socket there is no
|
||||
# portal to ask, so these tests cannot interrupt a human.
|
||||
UNREACHABLE_SESSION_VARS = (
|
||||
"WAYLAND_DISPLAY",
|
||||
"DBUS_SESSION_BUS_ADDRESS",
|
||||
"XDG_CURRENT_DESKTOP",
|
||||
"XDG_BACKEND",
|
||||
)
|
||||
|
||||
|
||||
def browser_env(display: str | None = None) -> dict[str, str]:
|
||||
"""An environment that can only reach the display this test owns."""
|
||||
env = {key: value for key, value in os.environ.items() if key not in UNREACHABLE_SESSION_VARS}
|
||||
env["XDG_SESSION_TYPE"] = "x11"
|
||||
env["MIRROR_ICE_SERVERS"] = ""
|
||||
if display is None:
|
||||
env.pop("DISPLAY", None)
|
||||
else:
|
||||
env["DISPLAY"] = display
|
||||
return env
|
||||
|
||||
|
||||
# Starts a pixel-sampling loop without blocking, so the test can drive motion on
|
||||
# the captured screen while the watcher is measuring it.
|
||||
PROBE = """() => {
|
||||
const video = document.querySelector('.tile video');
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 96;
|
||||
canvas.height = 60;
|
||||
const ctx = canvas.getContext('2d', {willReadFrequently: true});
|
||||
window.__probe = (async () => {
|
||||
const seen = new Set();
|
||||
for (let i = 0; i < 14; i++) {
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
try { ctx.drawImage(video, 0, 0, 96, 60); } catch {}
|
||||
const d = ctx.getImageData(0, 0, 96, 60).data;
|
||||
let h = 0;
|
||||
for (let j = 0; j < d.length; j += 4) {
|
||||
h = ((h * 31) + d[j] + d[j + 1] * 3 + d[j + 2] * 7) >>> 0;
|
||||
}
|
||||
seen.add(h);
|
||||
}
|
||||
return {distinct: seen.size, width: video.videoWidth, height: video.videoHeight};
|
||||
})();
|
||||
return 'started';
|
||||
}"""
|
||||
|
||||
|
||||
def _free_display() -> str:
|
||||
for number in range(90, 120):
|
||||
path = f"/tmp/.X11-unix/X{number}"
|
||||
if os.path.exists(path):
|
||||
continue
|
||||
with socket.socket(socket.AF_UNIX) as probe:
|
||||
try:
|
||||
probe.connect(path)
|
||||
except OSError:
|
||||
return f":{number}"
|
||||
else:
|
||||
continue
|
||||
raise RuntimeError("no free X display in :90..:119")
|
||||
|
||||
|
||||
def _socket_path(display: str) -> str:
|
||||
"""`:90` -> `/tmp/.X11-unix/X90`, the file whose existence means "ready"."""
|
||||
return f"/tmp/.X11-unix/X{display.lstrip(':')}"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def x_display():
|
||||
display = _free_display()
|
||||
ready = _socket_path(display)
|
||||
server = subprocess.Popen(
|
||||
["Xvfb", display, "-screen", "0", XVFB_GEOMETRY, "-nolisten", "tcp"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
deadline = time.time() + 20
|
||||
while time.time() < deadline:
|
||||
if os.path.exists(ready):
|
||||
break
|
||||
if server.poll() is not None: # pragma: no cover
|
||||
raise RuntimeError(f"Xvfb exited immediately with code {server.returncode}")
|
||||
time.sleep(0.15)
|
||||
else: # pragma: no cover
|
||||
server.terminate()
|
||||
raise RuntimeError(f"Xvfb never created {ready}")
|
||||
yield display
|
||||
server.terminate()
|
||||
try:
|
||||
server.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired: # pragma: no cover
|
||||
server.kill()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def browsers(x_display):
|
||||
"""Two Chromium *processes* from one Playwright driver.
|
||||
|
||||
The sharer is headed and lives on the Xvfb display, so what it captures is a
|
||||
genuine desktop. The watcher is headless: it never touches the display, which
|
||||
is exactly why it can be a separate process.
|
||||
"""
|
||||
previous = os.environ.get("DISPLAY")
|
||||
os.environ["DISPLAY"] = x_display
|
||||
try:
|
||||
with sync_playwright() as playwright:
|
||||
sharer = playwright.chromium.launch(
|
||||
channel="chromium",
|
||||
headless=False,
|
||||
args=REAL_CAPTURE_ARGS,
|
||||
env=browser_env(x_display),
|
||||
)
|
||||
watcher = playwright.chromium.launch(
|
||||
channel="chromium",
|
||||
headless=True,
|
||||
args=[
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--no-proxy-server",
|
||||
"--ozone-platform=x11",
|
||||
],
|
||||
env=browser_env(),
|
||||
)
|
||||
try:
|
||||
yield {"sharer": sharer, "watcher": watcher}
|
||||
finally:
|
||||
sharer.close()
|
||||
watcher.close()
|
||||
finally:
|
||||
if previous is not None:
|
||||
os.environ["DISPLAY"] = previous
|
||||
else: # pragma: no cover
|
||||
os.environ.pop("DISPLAY", None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def share_page(browsers, base_url):
|
||||
page = browsers["sharer"].new_context(viewport={"width": 1200, "height": 760}).new_page()
|
||||
page.goto(f"{base_url}/mirror", wait_until="domcontentloaded")
|
||||
expect(page.locator("#net")).to_have_attribute("data-state", "online", timeout=20_000)
|
||||
yield page
|
||||
page.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def watch_page(browsers, base_url):
|
||||
page = browsers["watcher"].new_context().new_page()
|
||||
page.goto(f"{base_url}/watch", wait_until="domcontentloaded")
|
||||
expect(page.locator("#net")).to_have_attribute("data-state", "online", timeout=20_000)
|
||||
yield page
|
||||
page.close()
|
||||
|
||||
|
||||
class TestRealX11Capture:
|
||||
def test_the_real_desktop_is_captured_not_a_synthetic_one(self, base_url, share_page):
|
||||
errors: list[str] = []
|
||||
share_page.on("pageerror", lambda error: errors.append(str(error)))
|
||||
|
||||
share_page.locator("#start").click()
|
||||
expect(share_page.locator("body")).to_have_attribute("data-live", "on", timeout=40_000)
|
||||
|
||||
settings = share_page.evaluate(
|
||||
"""() => {
|
||||
const track = document.querySelector('#preview').srcObject.getVideoTracks()[0];
|
||||
return {label: track.label, ...track.getSettings()};
|
||||
}"""
|
||||
)
|
||||
assert settings["displaySurface"] == "monitor", f"not a monitor capture: {settings}"
|
||||
assert settings["label"], "a real capture always names its source"
|
||||
assert not settings["label"].startswith("screen:-"), (
|
||||
f"{settings['label']} is Chromium's synthetic capture device"
|
||||
)
|
||||
size = (settings["width"], settings["height"])
|
||||
assert size != FAKE_DEVICE_SIZE, (
|
||||
f"got exactly {size}: that is Chromium's fake capture device, not the X11 screen"
|
||||
)
|
||||
assert settings["width"] >= 640, f"implausibly small capture: {settings}"
|
||||
# Chromium scales a monitor capture to the capturing window, so compare
|
||||
# the aspect ratio: 1280x800 in, 1200x750 out is the real screen.
|
||||
assert abs(settings["width"] / settings["height"] - XVFB_ASPECT) < 0.06, (
|
||||
f"{size} does not match the {XVFB_GEOMETRY} desktop aspect "
|
||||
f"({XVFB_ASPECT:.3f}); this is not our Xvfb screen"
|
||||
)
|
||||
|
||||
# The roster must carry the real geometry, not a made-up one.
|
||||
time.sleep(0.6)
|
||||
mirror = httpx.get(f"{base_url}/api/streams", timeout=5).json()["mirrors"][0]
|
||||
assert (mirror["width"], mirror["height"]) == size
|
||||
assert mirror["displaySurface"] == "monitor"
|
||||
assert errors == [], f"uncaught browser errors: {errors}"
|
||||
|
||||
def test_a_real_desktop_reaches_a_second_machine_and_moves(self, base_url, browsers):
|
||||
share_page = (
|
||||
browsers["sharer"].new_context(viewport={"width": 1200, "height": 760}).new_page()
|
||||
)
|
||||
share_page.goto(f"{base_url}/mirror", wait_until="domcontentloaded")
|
||||
expect(share_page.locator("#net")).to_have_attribute("data-state", "online", timeout=20_000)
|
||||
share_page.locator("#start").click()
|
||||
expect(share_page.locator("body")).to_have_attribute("data-live", "on", timeout=40_000)
|
||||
|
||||
watch_page = browsers["watcher"].new_context().new_page()
|
||||
watch_page.goto(f"{base_url}/watch", wait_until="domcontentloaded")
|
||||
expect(watch_page.locator("#net")).to_have_attribute("data-state", "online", timeout=20_000)
|
||||
expect(watch_page.locator(".tile[data-state='live']")).to_have_count(1, timeout=60_000)
|
||||
|
||||
# Start measuring on the watcher, then paint: cycling the captured
|
||||
# machine's background is what a real presenter's screen does constantly.
|
||||
assert watch_page.evaluate(PROBE) == "started"
|
||||
for colour in ("#c81e1e", "#0b0d10", "#1e6ec8", "#0b0d10", "#1ec85a"):
|
||||
share_page.evaluate("(colour) => { document.body.style.background = colour; }", colour)
|
||||
time.sleep(0.28)
|
||||
result = watch_page.evaluate("() => window.__probe")
|
||||
|
||||
assert result["width"] >= 640 and result["height"] >= 400, (
|
||||
f"the watcher decoded no picture: {result}"
|
||||
)
|
||||
assert abs(result["width"] / result["height"] - XVFB_ASPECT) < 0.06, result
|
||||
assert result["distinct"] >= 2, (
|
||||
f"the watcher received a frozen frame: {result} — real desktop motion "
|
||||
f"never made it across the peer connection"
|
||||
)
|
||||
|
||||
# And the sharer agrees that someone is watching its desktop.
|
||||
expect(share_page.locator("#viewer-count")).to_have_text("1", timeout=20_000)
|
||||
|
||||
# Stopping the share tears the mirror away for the watcher as well.
|
||||
share_page.locator("#stop").click()
|
||||
expect(watch_page.locator(".tile")).to_have_count(0, timeout=20_000)
|
||||
|
||||
watch_page.close()
|
||||
share_page.close()
|
||||
Reference in New Issue
Block a user