**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.
679 lines
31 KiB
Python
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}"
|