phase: 02_sharp_capture
**Phase 02 final verification pass — complete, 100% green.** **Verified (all 4 tasks in `complete/02_sharp_capture/`):** - Task 1: `pickSurface()` requests `screen × devicePixelRatio` (capped 3840×2160) instead of window size - Task 2: `degradationPreference: "maintain-resolution"` + per-leg `maxBitrate`, re-applied after `setLocalDescription` *and* on `connected` (both call sites asserted in `test_app.py`) - Task 3: default cap raised to 25 Mbit/s — `config.py` constant, `net.js` offline fallback, `.env.example`, README, and the drift-guard test all agree - Task 4: realdisplay sharpness floor (viewer decoded width ≥ 1152px = 90% of Xvfb 1280px) in `test_sharp_picture_reaches_the_wall` **Gates (all green):** - `uv run ruff check . && uv run ruff format --check .` → clean - `uv run pyright` → 0 errors - `uv run pytest` → 231 passed, 3 skipped (realdisplay), coverage 95% - `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` → 3 passed on a real Xvfb + headed Chromium **Completion criteria:** (1) 90% width floor asserted on viewer's decoded pixels — PASS · (2) `/api/config` `maxBitrateBps: 25000000` default, net.js fallback agrees — PASS · (3) `#stat-resolution` matches viewer-decoded size incl. documented window downscale (1100×620, 1280×720 fake-device tests) — PASS · (4) both suites green, aspect-ratio guard (`1280/800` ± 0.06 + `≠ 1280×720`) still distinguishes real from fake capture — PASS · (5) README Configuration documents `25000000` — PASS; `PLAN.md` §6 "default 12 Mbit/s" left untouched as instructed — **flag for the user to amend**. **Docs:** README — fixed suite timing `~20s → ~50s` (measured 49s); all other claims verified (CLI flags, dev extra, config table, kiosk key table vs. `watch.js`/`share.js` routers, kiosk commands vs. `docs/kiosk-silverblue.md`). AGENTS.md — same stale timing fixed, nothing else stale (all referenced test names/files exist). **Notable:** `docs/kiosk-silverblue.md` + its scripts use `mirror.reeselink.com` while README/SECURITY.md/Containerfile use `mirror.reeseapps.com` — a pre-existing cross-file mismatch I could not resolve from the repo (both in same-day commits; live-infra fact). Left untouched; user should confirm the live kiosk URL. No live changes made. **Next pending phase:** `03_audio_share`.
This commit is contained in:
@@ -10,6 +10,28 @@ 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -35,11 +57,18 @@ pytestmark = [
|
||||
),
|
||||
]
|
||||
|
||||
# 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.
|
||||
# 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:
|
||||
#
|
||||
@@ -240,8 +269,11 @@ class TestRealX11Capture:
|
||||
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.
|
||||
# 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"
|
||||
@@ -276,8 +308,10 @@ class TestRealX11Capture:
|
||||
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 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, (
|
||||
@@ -294,3 +328,85 @@ class TestRealX11Capture:
|
||||
|
||||
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}"
|
||||
|
||||
Reference in New Issue
Block a user