Files
mirror/tests/e2e/test_x11_capture.py
T
ducoterra 3f93481c54
Build and Push Containers / test (push) Successful in 1m12s
Build and Push Containers / build-and-push-app (push) Successful in 9s
phase: 06_sharer_quality_presets
**Phase 06 — final verification pass: 100% green.**

- **Verified (all 6 tasks complete in working tree):** `#quality` select + `PRESETS` table; preset-driven capture request + `contentHint`; per-leg `legCap()`/`degradation` at both call sites; live `applyQuality()` with Overconstrained fallback + republish; `q` key cycle; `#stat-quality`/`#stat-codec` readouts; full e2e matrix (requests, committed params ×3, zero-cap, mid-share switch, unrescalable device, late-joiner first-offer, readout, `q`); realdisplay delivered-pixels `light` case. `test_sender_fidelity.py` **untouched and green** (regression canary).
- **Gates:** `uv run ruff check . && uv run ruff format --check .` → clean (32 files); `uv run pyright` → 0 errors; `uv run pytest` → **284 passed, 5 skipped (realdisplay-gated), coverage 95%** (93s); `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` → **4 passed, 1 skipped** (no audio source on host — documented platform condition).
- **Completion criteria:** all 11 ✓ — sharp default reproduces old request; light ≤960×600@15fps/motion/round(cap×0.35)/maintain-framerate with moving viewer frames; mid-share re-prices with `peers.size` unchanged, no renegotiation, roster matches `getSettings()`; refused-rescale toasts "next share"; `q` cycles + toasts, text-field guard holds; readout exact + codec shape, both reset to `—` idle; realdisplay: light decoded ≤990px (measured 320×200), sharp clears `SHARP_FLOOR_PX`; no new env/wire/Python/asset; suite 100% green, coverage >90%.
- **Docs:** README.md reviewed in full against code — Quality table, `q` key, env-var table, routes (`/share`), kiosk commands, key table, doc paths all consistent; **no fixes needed**. AGENTS.md — verified, no stale statements, no changes. PLAN.md untouched (diff = 0).
- **Reported (per phase, not edited):** PLAN.md §6 line 204 still says "per-leg `maxBitrate` cap (default **12 Mbit/s**)" — code has defaulted to **25 Mbit/s** since phase 02; needs a user-approved one-line amendment.
- **Next pending phase:** none — 06 is the last phase in `todo/`; the roadmap is complete.
2026-11-02 22:07:17 -05:00

679 lines
31 KiB
Python

"""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
Measured on the reference Linux host (Xvfb 1280x800x24, headed Chromium,
phase 02, 2026-11-01) — requested vs delivered, per sharer window size:
=================== ============ ============ =====================
sharer window requested delivered viewer decodes
=================== ============ ============ =====================
1200x760 1200x760 1200x750 1200x750 (93.75% of desktop)
1024x700 1024x700 1024x640 1024x640 (80% -> fails the floor)
800x600 800x600 800x500 800x500 (62.5% -> fails the floor)
=================== ============ ============ =====================
* ``requested`` is ``screen x devicePixelRatio``; under Playwright ``screen``
tracks the browser window, so the request can never outgrow the window here.
* Chromium *still* clamps a monitor capture to the capturing window even with
an explicit ``ideal`` size named — which agrees with PLAN §5 ("Chromium
scales a monitor capture to the capturing window"). Removing the phase 02
size constraints (frameRate only) delivered the identical 1200x750 for a
1200x760 window, so on this host the floor discriminates by *window* size,
not by the constraints. On real hardware ``screen`` is the true display
resolution, so a small window on a large display is the case the 90% floor
and the share page's 75% toast are for.
Audio on the real capture path is environment-gated, and the gap is written
down here rather than imagined away. ``--auto-select-desktop-capture-source=
Entire screen`` selects the screen without ticking the picker's "share audio"
box, and a host without an audio device has no track to offer at all — on
such hosts ``test_a_real_desktop_shares_audio_when_the_host_offers_any``
finds no audio track in the captured stream and *skips with that reason*
(never a stubbed peer, never an xfailed-into-green failure). Where a host
does expose an audio source, the test asserts the viewer's inbound-rtp audio
bytes increase across two samples and the picture still decodes distinct
frames, the same way the fake-device suite does.
"""
from __future__ import annotations
import os
import re
import shutil
import socket
import subprocess
import sys
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: an aspect ratio of exactly 1280/800 is a fingerprint
# of the real screen. (The fake device, where it is used, defaults to 1280x720
# and, once a size is requested, clamps to the capturing window — PLAN §5.)
XVFB_GEOMETRY = "1280x800x24"
XVFB_ASPECT = 1280 / 800
FAKE_DEVICE_SIZE = (1280, 720)
# The sharpness floor: 90% of the desktop width (1152 of 1280 px). An exact
# 100% match is environment-specific (compositor, fractional DPI, window
# chrome), but a window-clamped capture of a small share page lands at 62-93%
# of the desktop — that is where the fuzziness a person sees lives.
XVFB_WIDTH = 1280
SHARP_FLOOR_PX = int(XVFB_WIDTH * 0.9)
# 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';
}"""
# The one global a test is allowed to use (AGENTS.md §6): collect the
# RTCPeerConnection instances watch.js creates so a test can read the
# viewer's inbound-rtp stats. The e2e suite (test_audio_share.py) installs
# the same wrapper; it is repeated here because the e2e convention is one
# copy per suite file. On the module's shared watch page `window.__probe`
# is the frame-sampling promise instead — the two uses never share a page.
PC_PROBE_INIT = """
window.__probe = {pcs: []};
const RealPC = window.RTCPeerConnection;
window.RTCPeerConnection = function (config) {
const pc = new RealPC(config);
window.__probe.pcs.push(pc);
return pc;
};
window.RTCPeerConnection.prototype = RealPC.prototype;
window.RTCPeerConnection.static = RealPC.static;
"""
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}"
# Measured after phase 02: the 1200x760 window requests 1200x760
# (Playwright emulates screen == window) and Chromium still clamps the
# monitor capture to the capturing window: 1280x800 desktop in, 1200x750
# out, aspect 1.6000 preserved. So compare the aspect ratio, not
# absolute pixels (PLAN §5).
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"] >= SHARP_FLOOR_PX, (
f"the watcher decoded a {result['width']}px-wide picture: the floor is "
f"{SHARP_FLOOR_PX}px (90% of the {XVFB_GEOMETRY} desktop); below it the "
f"wall shows a window-clamped capture, not the screen: {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()
def test_sharp_picture_reaches_the_wall(self, base_url, share_page, watch_page):
"""The size floor that makes \"sharper\" an assertion.
The viewer's decoded width must be at least 90% of the Xvfb desktop
(1152 of 1280 px), the picture must keep moving in the same probe (a
large-but-frozen picture must not pass), and the roster must not lie
about what is on the wire: the published ``/api/streams`` width/height
sit within one rounding step of both the sharer's
``track.getSettings()`` and the viewer's decoded size.
Measured on this host (phase 02, 2026-11-01): the 1200x760 share
window requests 1200x760 (Playwright emulates ``screen`` == viewport)
and Chromium clamps the monitor capture to the capturing window, so the
viewer decodes 1200x750 — 93.75% of the desktop, over the floor.
Removing the phase 02 size constraints delivered the identical 1200x750
here (the request cannot outgrow the window under Playwright), while a
1024x700 window delivered 1024x640 (80%) — under the floor. On real
hardware ``screen`` is the true display resolution, so a small window
on a large display is the case this floor and the share page's 75%
toast are for.
"""
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];
const s = track.getSettings();
return {width: s.width, height: s.height, surface: s.displaySurface};
}"""
)
assert settings["surface"] == "monitor", f"not a monitor capture: {settings}"
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"] >= SHARP_FLOOR_PX, (
f"the watcher decoded a {result['width']}px-wide picture: the floor is "
f"{SHARP_FLOOR_PX}px (90% of the {XVFB_GEOMETRY} desktop); below it the "
f"wall is a window-clamped capture, not the screen: {result}"
)
assert abs(result["width"] / result["height"] - XVFB_ASPECT) < 0.06, (
f"{result} does not match the {XVFB_GEOMETRY} desktop aspect "
f"({XVFB_ASPECT:.3f}); this is not our Xvfb screen"
)
assert result["distinct"] >= 2, (
f"the watcher received a frozen frame: {result} — a large-but-still "
f"picture must not pass a sharpness test"
)
# The roster must not lie about what is on the wire: the published
# size sits within one rounding step of the sharer's delivered
# settings *and* the viewer's decoded size.
time.sleep(0.6)
mirror = httpx.get(f"{base_url}/api/streams", timeout=5).json()["mirrors"][0]
for source, (w, h) in {
"the sharer's track.getSettings()": (settings["width"], settings["height"]),
"the viewer's decoded size": (result["width"], result["height"]),
}.items():
assert abs(mirror["width"] - w) <= 1, (
f"roster width {mirror['width']} vs {w} from {source}: the roster must "
f"not lie about what is on the wire"
)
assert abs(mirror["height"] - h) <= 1, (
f"roster height {mirror['height']} vs {h} from {source}: the roster must "
f"not lie about what is on the wire"
)
# And the sharer agrees that someone is watching its desktop.
expect(share_page.locator("#viewer-count")).to_have_text("1", timeout=20_000)
assert errors == [], f"uncaught browser errors: {errors}"
def test_a_light_preset_delivers_a_small_picture(self, share_page, watch_page):
"""The delivered-pixel proof that the preset reaches the *real*
capturer, not just the request object.
``#quality = light`` is selected **before** the share starts, so the
request — under Playwright ``screen`` is the emulated viewport, so
the 1200x760 share window asks ``min(viewport, 960x600)`` =
960x600 — goes to Chromium's real X11 backend. The sharer's track
settings must honour the budget (at least 80% of the 960 request
wide, never above it): that is the proof the request reached the
real capturer and was not clamped away to the desktop or the
window. The viewer's decoded width must be at or under the cap —
the phase's delivered-pixels criterion — *and below the sharp
path's floor*: if a ``light`` share delivered a sharp-size picture,
the budget never reached the wire. The aspect guard keeps proving
the picture is *our* desktop, not somebody else's window.
The sharp path's floor (``SHARP_FLOOR_PX``) is deliberately
**not** asserted here: it stays the sharp test's, and a small
picture is exactly what a ``light`` share should deliver — the
small picture that is still the whole desktop.
Measured on this host (Xvfb 1280x800x24, phase 06): the capturer
delivered 960x600 (track settings, ``frameRate`` 15), and the
viewer decoded 320x200 — VP8 honouring ``light``'s
``maintain-framerate`` at ``round(25 Mbit/s x 0.35)`` = 8.75
Mbit/s, i.e. the encoder's resolution-degradation policy (the same
leg encodes 960x600 under ``maintain-resolution``); the encoder
may sacrifice resolution, which is the preset's contract, so the
decoded-width floor below guards a *collapsed* picture (>= a
quarter of the request), not the encoder's honest ladder.
"""
errors: list[str] = []
share_page.on("pageerror", lambda error: errors.append(str(error)))
share_page.select_option("#quality", "light")
share_page.locator("#start").click()
expect(share_page.locator("body")).to_have_attribute("data-live", "on", timeout=40_000)
# The preset is what the live capture belongs to (a pure parameter
# read, not a pixel claim — the pixels are the viewer's, below).
assert share_page.evaluate("() => window.__probe.qualityKey") == "light"
# The request reached the *real* capturer and was not clamped away:
# the track the X11 backend settled on honours the light budget —
# at least 80% of the 960 request wide (the capturer did not drop
# the size), never above it (the capturer did not ignore it and
# deliver the 1200x750 window-clamped desktop).
settings = share_page.evaluate(
"""() => {
const track = document.querySelector('#preview').srcObject.getVideoTracks()[0];
const s = track.getSettings();
return {width: s.width, height: s.height, surface: s.displaySurface};
}"""
)
assert settings["surface"] == "monitor", f"not a monitor capture: {settings}"
assert settings["width"] <= 990, f"the capturer ignored the light budget: {settings}"
assert settings["width"] >= 768, f"the capturer did not honour the request: {settings}"
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")
# Delivered pixels follow the preset: the decoded picture is at or
# under the light cap, and below the sharp path's floor (a
# sharp-size picture from a light share would mean the budget
# never reached the wire). Measured 320x200 on this host — the
# encoder's maintain-framerate degradation, see the docstring.
assert result["width"] <= 990, (
f"the watcher decoded a {result['width']}px-wide picture: the light budget is "
f"<= 960px, so a wider decode means the budget did not reach the wire: {result}"
)
assert result["width"] < SHARP_FLOOR_PX, (
f"the watcher decoded a {result['width']}px-wide picture: that is at or above "
f"the sharp floor ({SHARP_FLOOR_PX}px), so a light share delivered a sharp-size "
f"picture: {result}"
)
assert result["width"] >= 240, (
f"the watcher decoded a {result['width']}px-wide picture: a quarter of the 960px "
f"request must arrive at least, or the picture collapsed: {result}"
)
assert abs(result["width"] / result["height"] - XVFB_ASPECT) < 0.06, (
f"{result} does not match the {XVFB_GEOMETRY} desktop aspect "
f"({XVFB_ASPECT:.3f}); this is not our Xvfb screen"
)
assert result["distinct"] >= 2, (
f"the watcher received a frozen frame: {result} — a small-but-still picture "
f"must not pass a delivered-pixel test"
)
expect(share_page.locator("#viewer-count")).to_have_text("1", timeout=20_000)
assert errors == [], f"uncaught browser errors: {errors}"
def test_a_real_desktop_shares_audio_when_the_host_offers_any(self, base_url, browsers):
"""The realdisplay counterpart of the audio-arrival assertion.
Where the host exposes an audio source to the real capture path, the
viewer's ``inbound-rtp`` audio bytes must strictly increase across two
samples (Opus frames over SRTP) and the picture must still decode
distinct frames. ``--auto-select-desktop-capture-source=Entire
screen`` selects the screen without ticking the picker's "share
audio" box, and a host without an audio device has no track to offer
at all — in that case the test *skips with the reason written down*
(module docstring), never asserts on an absent track, and never
stubs the peer connection.
"""
errors: list[str] = []
share_page = (
browsers["sharer"].new_context(viewport={"width": 1200, "height": 760}).new_page()
)
share_page.on("pageerror", lambda error: errors.append(str(error)))
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)
# The honest gate: does the captured stream actually carry an audio
# track? This is what the browser's real capture path delivered.
has_audio = share_page.evaluate(
"""() => {
const so = document.querySelector('#preview').srcObject;
return !!(so && so.getAudioTracks().length);
}"""
)
if not has_audio:
share_page.locator("#stop").click()
share_page.close()
pytest.skip(
"the host offers no audio source to the real capture path "
f"(platform={sys.platform}; --auto-select-desktop-capture-source="
"'Entire screen' does not tick 'share audio', and this host "
"delivers no audio track); see the module docstring"
)
watcher = browsers["watcher"].new_context()
watcher.add_init_script(PC_PROBE_INIT)
watch_page = watcher.new_page()
watch_page.on("pageerror", lambda error: errors.append(str(error)))
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)
# The deliberate gesture: stage, then unmute.
watch_page.keyboard.press("Enter")
expect(watch_page.locator("body")).to_have_class(re.compile("staged"), timeout=5_000)
watch_page.keyboard.press("m")
assert (
watch_page.evaluate("() => !document.querySelector('#watch-stage video').muted") is True
)
def audio_inbound() -> dict:
"""One ``inbound-rtp`` audio sample from the viewer's leg."""
return watch_page.evaluate(
"""async () => {
const pcs = (window.__probe && window.__probe.pcs) || [];
const pc = pcs.find((p) => p.connectionState === 'connected');
if (!pc) return {pc: false};
const stats = await pc.getStats();
let audio = null;
stats.forEach((r) => {
if (r.type === 'inbound-rtp' && r.kind === 'audio') audio = r;
};
if (!audio) return {pc: true, entry: null};
const energy = audio.totalAudioEnergy;
const level = audio.audioLevel;
return {
pc: true,
entry: {
bytes: audio.bytesReceived || 0,
energy: Number.isFinite(energy) ? energy : null,
level: Number.isFinite(level) ? level : null,
},
};
}"""
)
first: dict = {}
deadline = time.time() + 30
while time.time() < deadline:
first = audio_inbound()
if first.get("entry"):
break
time.sleep(0.5)
assert first.get("entry"), (
f"the viewer's connected leg has no inbound-rtp audio entry: {first}"
)
time.sleep(4.0)
second = audio_inbound()
assert second.get("entry"), f"the inbound-rtp audio entry vanished: {second}"
assert second["entry"]["bytes"] > first["entry"]["bytes"], (
f"inbound audio bytes did not strictly increase: {first} -> {second}"
)
# The picture is asserted in the same test: audio arriving while the
# picture froze would be a different bug. One self-contained sample
# loop — no global (on this page `window.__probe` belongs to the
# peer-connection collector).
frames = watch_page.evaluate(
"""async () => {
const video = document.querySelector('#watch-stage video');
const canvas = document.createElement('canvas');
canvas.width = 96; canvas.height = 60;
const ctx = canvas.getContext('2d', {willReadFrequently: true});
const seen = new Set();
for (let i = 0; i < 10; 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 {width: video.videoWidth, distinct: seen.size};
}"""
)
assert frames["width"] >= SHARP_FLOOR_PX, (
f"the watcher decoded a {frames['width']}px-wide picture: {frames}"
)
assert frames["distinct"] >= 2, f"the watcher received a frozen frame: {frames}"
# Clean up before the module-scoped fixtures serve the next test.
watch_page.close()
watcher.close()
share_page.locator("#stop").click()
share_page.close()
assert errors == [], f"uncaught browser errors: {errors}"