Files
mirror/tests/e2e/test_quality_presets.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

620 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""End-to-end: the capture request and the per-leg encoder parameters follow
the selected quality preset.
`pickSurface()` asks the capture device for the *selected* preset's pixel
budget and frame rate instead of one hardcoded 4K/30 request, and `start()`
records which preset the live capture belongs to. The capture assertions read
the page's own request back through `window.__probe` (AGENTS.md §6):
`lastRequested` is the size the request named, `qualityKey` the preset it
came from.
The per-leg assertions read the *committed* sender parameters back from the
sharer's real RTCPeerConnection legs through the same `__probe` handle:
each leg encodes under `round(config.maxBitrateBps * preset.factor)` and the
preset's `degradationPreference`, read at call time — so a preset chosen
before a viewer joins lands on that leg's first offer. Media rules still
govern: pixel assertions are made on the *viewer's* `<video>`.
Delivered *capture* pixels are deliberately not asserted here: with
`--use-fake-device-for-media-stream` the synthetic device is a fixed
1280x720 and its clamping is not the behaviour under test (PLAN §5). Where
a real capturer runs (`MIRROR_REALDISPLAY=1`, the `realdisplay` suite) the
delivered pixels are asserted instead.
The late-joining-viewer case proves the *first offer* of a leg that comes
up under a preset chosen before that viewer joined carries the preset's cap:
a new `RTCPeerConnection` is wrapped on the sharer page (the same pattern the
other e2e suites use for their `RTCPeerConnection` probes) and the parameters
are read back at `setLocalDescription` — before `negotiate()`'s
post-negotiation re-apply has run, so the only code that could have written
them is `addPeer()`'s `sendEncodings`.
"""
from __future__ import annotations
import re
import time
import pytest
from playwright.sync_api import expect
from .conftest import _start, _stop, frames_matching, start_sharing, stop_sharing
pytestmark = [pytest.mark.e2e, pytest.mark.timeout(180)]
@pytest.fixture
def device(base_url, browser, request):
"""Open a page as its own device (isolated context, fresh peer id).
`url` targets a different server than the session's (the zero-cap
fixture); the default is the session server, which is what the capture-
request tests use.
"""
opened = []
def _open(path: str = "/", errors: list | None = None, url: str | None = None):
context = browser.new_context(viewport={"width": 1100, "height": 720})
opened.append(context)
request.addfinalizer(context.close)
page = context.new_page()
if errors is not None:
page.on("pageerror", lambda error: errors.append(f"pageerror: {error}"))
page.goto(f"{url or base_url}{path}", wait_until="domcontentloaded")
expect(page.locator("#net")).to_have_attribute("data-state", "online", timeout=10_000)
return page
return _open
@pytest.fixture
def zero_cap_url():
"""A real `mirror serve` with `MIRROR_MAX_BITRATE=0` — the "uncapped"
setting of the WebRTC API, which must not turn into a 0 bps leg."""
url, process = _start(extra_env={"MIRROR_MAX_BITRATE": "0"})
yield url
_stop(process)
# Reads the video sender's committed parameters back from the sharer's live
# legs (copied per suite from test_sender_fidelity.py — one copy per suite is
# the convention). `state` is the leg's connectionState; `maxBitrate` is the
# cap the leg is actually encoding under (undefined when none was written —
# it reads back as null — which is exactly the "uncapped" state). If the
# server had written a 0 bps cap it would read back as 0, and the zero-cap
# test's `is None` assertion is what keeps the two apart.
SENDER_PROBE = """() => {
const probe = window.__probe;
if (!probe) return null;
return [...probe.peers.values()].map((peer) => {
const sender = peer.pc.getSenders().find(
(s) => s.track && s.track.kind === "video"
);
if (!sender) return null;
const params = sender.getParameters();
const encodings = params.encodings || [];
return {
state: peer.pc.connectionState,
degradationPreference: params.degradationPreference,
maxBitrate: encodings.length ? encodings[0].maxBitrate : null,
};
});
}"""
def _wait_for(sharer, want: int, pred, timeout: float = 45.0) -> list:
"""Poll the sharer's legs until `want` of them satisfy `pred` (or give up)."""
legs: list = []
deadline = time.time() + timeout
while time.time() < deadline:
legs = sharer.evaluate(SENDER_PROBE) or []
if len(legs) == want and all(leg and pred(leg) for leg in legs):
return legs
time.sleep(0.5)
return legs
# The page's own request, the preset it came from, the track's contentHint
# (a pure read-back of what start() set — not a delivered pixel), and the
# inputs the page used: screen.width / devicePixelRatio are read *from the
# page* — under Playwright `screen` is the emulated viewport, so hardcoding
# a display size would make this environment-dependent.
REQUEST_PROBE = """() => {
const probe = window.__probe;
if (!probe) return null;
const stream = document.querySelector('#preview').srcObject;
const track = stream && stream.getVideoTracks()[0];
return {
requested: probe.lastRequested,
qualityKey: probe.qualityKey,
contentHint: track ? track.contentHint : null,
screenWidth: screen.width,
screenHeight: screen.height,
dpr: window.devicePixelRatio || 1,
};
}"""
# The pixel ceilings and the per-preset contentHint, the mirror of `PRESETS`
# in share.js: the request is min(screen * dpr, cap). This suite asserts the
# *request* the page built; the table itself is guarded by test_app.py's
# three-preset agreement test.
PRESETS_UNDER_TEST = {
"sharp": {"cap": (3840, 2160), "contentHint": "detail"},
"balanced": {"cap": (1920, 1200), "contentHint": "detail"},
"light": {"cap": (960, 600), "contentHint": "motion"},
}
def _bitrate_format(bps: float) -> str:
"""The mirror of `bitrateFormat` in net.js — the panel's own renderer,
so the test can recompute the exact line the panel must print."""
if not bps or bps < 0:
return "—"
if bps >= 1e6:
return f"{bps / 1e6:.1f} Mbps"
return f"{round(bps / 1e3)} kbps"
class TestQualityPresets:
def test_each_preset_asks_the_capture_for_its_own_budget(self, device):
"""Each preset drives the capture request: `sharp` is the old
hardcoded 4K cap (the no-regression argument), `balanced` 1080p-class,
`light` a small and cheap budget — and the page records which preset
the live capture belongs to."""
errors: list = []
for key, preset in PRESETS_UNDER_TEST.items():
cap_w, cap_h = preset["cap"]
sharer = device("/mirror", errors)
sharer.select_option("#quality", key)
start_sharing(sharer)
result = sharer.evaluate(REQUEST_PROBE)
assert result, "the __probe handle is missing"
assert result["qualityKey"] == key, result
# The page's own arithmetic, recomputed from the page's own inputs:
# min(round(screen * dpr), cap).
expected_w = min(round(result["screenWidth"] * result["dpr"]), cap_w)
expected_h = min(round(result["screenHeight"] * result["dpr"]), cap_h)
assert result["requested"]["width"] == expected_w, result
assert result["requested"]["height"] == expected_h, result
# Text-first presets take `detail`; `light` is glanceable, so
# motion wins over glyph crispness.
assert result["contentHint"] == preset["contentHint"], result
assert errors == [], f"uncaught browser errors: {errors}"
def test_a_light_leg_encodes_at_the_presets_share(self, device, base_url):
"""A leg that comes up under `light` encodes at the preset's share
of the server's ceiling and with the preset's degradation preference —
both read from the preset *at call time*: `light` was selected before
sharing started, so the viewer's leg must carry them from its first
offer, and the wall must still be moving (a cheaper picture must not
be a dead one)."""
import httpx
cap = httpx.get(f"{base_url}/api/config", timeout=5).json()["maxBitrateBps"]
assert cap > 0
errors: list = []
sharer = device("/mirror", errors)
sharer.select_option("#quality", "light")
start_sharing(sharer)
viewer = device("/watch", errors)
expect(viewer.locator(".tile[data-state='live']")).to_have_count(1, timeout=25_000)
legs = _wait_for(sharer, 1, lambda leg: leg["state"] == "connected")
assert legs and legs[0]["state"] == "connected", legs
assert legs[0]["maxBitrate"] == round(cap * 0.35), legs
assert legs[0]["degradationPreference"] == "maintain-framerate", legs
# The cheaper picture still has to be a live one.
frames = frames_matching(viewer, ".tile video")
assert frames["distinct"] >= 2, f"a light leg froze: {frames}"
assert errors == [], f"uncaught browser errors: {errors}"
def test_each_preset_commits_its_sender_parameters(self, device, base_url):
"""The full committed-parameter matrix, `sharp` first so the
no-regression leg is the first thing it proves: each preset's live
leg commits `round(cap * factor)` and the preset's
`degradationPreference` — read at call time, so the preset selected
before sharing lands on the leg from its first offer — and the wall
stays moving on the viewer in every case (a cheaper picture must not
be a dead one). `sharp` is phase 02's behaviour verbatim: full cap,
`maintain-resolution`.
"""
import httpx
cap = httpx.get(f"{base_url}/api/config", timeout=5).json()["maxBitrateBps"]
assert cap > 0
# The committed-parameter mirror of PRESETS_UNDER_TEST: the factor the
# server's ceiling is scaled by and the degradation preference the
# encoder commits to, per preset. `sharp` first: full cap and
# maintain-resolution is the unchanged phase 02 behaviour.
committed = {
"sharp": (1.0, "maintain-resolution"),
"balanced": (0.6, "balanced"),
"light": (0.35, "maintain-framerate"),
}
for key, (factor, degradation) in committed.items():
errors: list = []
sharer = device("/mirror", errors)
sharer.select_option("#quality", key)
start_sharing(sharer)
viewer = device("/watch", errors)
expect(viewer.locator(".tile[data-state='live']")).to_have_count(1, timeout=25_000)
legs = _wait_for(sharer, 1, lambda leg: leg["state"] == "connected")
assert legs and legs[0]["state"] == "connected", (key, legs)
assert legs[0]["maxBitrate"] == round(cap * factor), (key, legs)
assert legs[0]["degradationPreference"] == degradation, (key, legs)
# The picture still has to be a live one in every preset.
frames = frames_matching(viewer, ".tile video")
assert frames["distinct"] >= 2, f"the {key} leg froze: {frames}"
assert errors == [], f"uncaught browser errors on {key}: {errors}"
# Tear the whole iteration down before the next preset: the
# contexts outlive the iteration, and the wall auto-subscribes
# *every live viewer* to *every new mirror* — an uncleaned
# viewer would stack a second (third, ...) leg on the next
# preset's sharer and break the one-leg expectation. Stop the
# share first, so the viewer's tile (and its peer connection)
# is torn down before its page goes away.
stop_sharing(sharer)
viewer.close()
sharer.close()
def test_a_zero_cap_stays_uncapped_under_light(self, device, zero_cap_url):
"""`MIRROR_MAX_BITRATE=0` is a real setting — "uncapped" in the
WebRTC API. `legCap()` must return 0 and no `maxBitrate` may be
written: an uncapped leg must not become a 0 bps leg. The preset's
degradation preference still lands, and the picture still moves.
"""
import httpx
config = httpx.get(f"{zero_cap_url}/api/config", timeout=5).json()
assert config["maxBitrateBps"] == 0, config
errors: list = []
sharer = device("/mirror", errors, url=zero_cap_url)
sharer.select_option("#quality", "light")
start_sharing(sharer)
viewer = device("/watch", errors, url=zero_cap_url)
expect(viewer.locator(".tile[data-state='live']")).to_have_count(1, timeout=25_000)
legs = _wait_for(sharer, 1, lambda leg: leg["state"] == "connected")
assert legs and legs[0]["state"] == "connected", legs
# No maxBitrate written at all: an uncapped leg, not a 0 bps one.
assert legs[0]["maxBitrate"] is None, legs
assert legs[0]["degradationPreference"] == "maintain-framerate", legs
frames = frames_matching(viewer, ".tile video")
assert frames["distinct"] >= 2, f"the uncapped leg froze: {frames}"
assert errors == [], f"uncaught browser errors: {errors}"
def test_changing_quality_mid_share_re_prices_the_live_leg(self, device, base_url):
"""A preset change made while a leg is live re-prices that leg —
``setParameters`` commits without an offer, so the CPU/egress
change is instant and no second RTCPeerConnection is needed for it —
and the viewer keeps decoding moving frames across the switch. The
roster must tell the truth in *both* outcomes: its width/height
equal the track's own ``getSettings()``, whatever the engine
settled on (the synthetic device clamps; delivered pixels are the
``realdisplay`` suite's, task 06)."""
import httpx
cap = httpx.get(f"{base_url}/api/config", timeout=5).json()["maxBitrateBps"]
assert cap > 0
errors: list = []
sharer = device("/mirror", errors)
start_sharing(sharer) # the default preset: sharp
viewer = device("/watch", errors)
expect(viewer.locator(".tile[data-state='live']")).to_have_count(1, timeout=25_000)
legs = _wait_for(sharer, 1, lambda leg: leg["state"] == "connected")
assert legs and legs[0]["state"] == "connected", legs
# The switch, mid-share, through the control the user has.
sharer.select_option("#quality", "light")
# The single live leg re-prices in place: ``setParameters`` commits
# within ~5 s of the change, and the connection state returns to /
# stays ``connected`` (a leg that comes up fresh for the new size
# reads ``legCap()`` at its first offer, so the cap lands either way).
def repriced(leg):
return leg["state"] == "connected" and leg["maxBitrate"] == round(cap * 0.35)
re = _wait_for(sharer, 1, repriced, timeout=5.0)
# No second RTCPeerConnection: the roster of live legs is still the
# one the viewer was subscribed on.
assert sharer.evaluate("() => window.__probe.peers.size") == 1
assert re and re[0]["maxBitrate"] == round(cap * 0.35), re
assert re[0]["state"] == "connected", re
# The switch did not freeze anybody.
frames = frames_matching(viewer, ".tile video")
assert frames["distinct"] >= 2, frames
# The roster tells the truth: whatever the engine settled on, the
# published width/height equal the track's own settings. Do not
# assert a *delivered* size here — the synthetic device clamps, and
# task 06 owns delivered pixels on a real desktop.
def track_settings():
return sharer.evaluate(
"() => { const s = document.querySelector('#preview').srcObject;"
" const t = s && s.getVideoTracks()[0];"
" return t ? t.getSettings() : null; }"
)
def roster_agrees():
settings = track_settings()
if not settings or not settings.get("width"):
return False, None, settings
mirror = httpx.get(f"{base_url}/api/streams", timeout=5).json()["mirrors"]
assert len(mirror) == 1, mirror
mirror = mirror[0]
agrees = mirror["width"] == settings["width"] and mirror["height"] == settings["height"]
return agrees, mirror, settings
deadline = time.time() + 10.0
agrees, mirror, settings = roster_agrees()
while not agrees and time.time() < deadline:
time.sleep(0.5)
agrees, mirror, settings = roster_agrees()
assert agrees, f"roster does not match the track's own settings: {mirror!r} vs {settings!r}"
assert errors == [], f"uncaught browser errors: {errors}"
def test_a_device_that_cannot_rescale_still_changes_rate(self, device, base_url):
"""A display that cannot rescale answers the full constraint with the
named ``OverconstrainedError``: the retry applies the frame rate
alone, the leg is still re-priced, the viewer still moves, the
toast says the size lands on the next share (it does not pretend
the capture rescaled), and the roster stays truthful."""
import httpx
cap = httpx.get(f"{base_url}/api/config", timeout=5).json()["maxBitrateBps"]
assert cap > 0
errors: list = []
sharer = device("/mirror", errors)
start_sharing(sharer)
viewer = device("/watch", errors)
expect(viewer.locator(".tile[data-state='live']")).to_have_count(1, timeout=25_000)
legs = _wait_for(sharer, 1, lambda leg: leg["state"] == "connected")
assert legs and legs[0]["state"] == "connected", legs
# Simulate the display: a constraint that carries a size fails with
# the named error; the frame-rate-only retry passes. (This is the
# real-world behaviour the retry exists for — a capturer that holds
# its size — and nothing else in the path is stubbed.)
sharer.evaluate(
"() => { const s = document.querySelector('#preview').srcObject;"
" const t = s.getVideoTracks()[0];"
" const orig = t.applyConstraints.bind(t);"
" t.applyConstraints = (c) => (c && c.width)"
" ? Promise.reject(new DOMException('size cannot be applied', 'OverconstrainedError'))"
" : orig(c); }"
)
sharer.select_option("#quality", "light")
# The toast lands with the settle (before the leg is measured
# again), so it is read first: the preset is named, and because
# the size was never applied the line says it lands next share.
toast = sharer.locator("#toast")
expect(toast).to_contain_text("Light", timeout=5_000)
text = toast.text_content() or ""
assert "next share" in text, text
# The leg is still re-priced, and the switch did not freeze anybody.
def repriced(leg):
return leg["state"] == "connected" and leg["maxBitrate"] == round(cap * 0.35)
re = _wait_for(sharer, 1, repriced, timeout=5.0)
assert re and re[0]["maxBitrate"] == round(cap * 0.35), re
frames = frames_matching(viewer, ".tile video")
assert frames["distinct"] >= 2, frames
# The roster tells the truth about whatever the device delivered.
def track_settings():
return sharer.evaluate(
"() => { const s = document.querySelector('#preview').srcObject;"
" const t = s && s.getVideoTracks()[0];"
" return t ? t.getSettings() : null; }"
)
def roster_agrees():
settings = track_settings()
if not settings or not settings.get("width"):
return False, None, settings
mirror = httpx.get(f"{base_url}/api/streams", timeout=5).json()["mirrors"]
assert len(mirror) == 1, mirror
mirror = mirror[0]
agrees = mirror["width"] == settings["width"] and mirror["height"] == settings["height"]
return agrees, mirror, settings
deadline = time.time() + 10.0
agrees, mirror, settings = roster_agrees()
while not agrees and time.time() < deadline:
time.sleep(0.5)
agrees, mirror, settings = roster_agrees()
assert agrees, f"roster does not match the track's own settings: {mirror!r} vs {settings!r}"
assert errors == [], f"uncaught browser errors: {errors}"
def test_a_viewer_who_joins_after_a_switch_gets_the_new_cap(self, device, base_url):
"""The `addPeer` path, not just the re-apply path.
A preset change mid-share re-prices the *existing* leg (task 04's
re-apply). This proves the other half: a viewer who subscribes
*after* the switch gets a leg whose **first offer** already carries
the new cap — `legCap()` is read at call time in `addPeer()` and
written into `sendEncodings` before any negotiation exists. The
sharer's `RTCPeerConnection` is wrapped (the same pattern the other
e2e suites use for their peer-connection probes) so the parameters
are read back at `setLocalDescription`, i.e. before `negotiate()`'s
post-negotiation re-apply has run: the only code that could have
written them is `addPeer()`'s `sendEncodings`.
"""
import httpx
cap = httpx.get(f"{base_url}/api/config", timeout=5).json()["maxBitrateBps"]
assert cap > 0
errors: list = []
sharer = device("/mirror", errors)
start_sharing(sharer) # the default preset: sharp
first = device("/watch", errors)
expect(first.locator(".tile[data-state='live']")).to_have_count(1, timeout=25_000)
legs = _wait_for(sharer, 1, lambda leg: leg["state"] == "connected")
assert legs and legs[0]["state"] == "connected", legs
assert legs[0]["maxBitrate"] == cap, legs
# The switch, mid-share: the existing leg re-prices via the re-apply.
sharer.select_option("#quality", "light")
def repriced(leg):
return leg["state"] == "connected" and leg["maxBitrate"] == round(cap * 0.35)
re = _wait_for(sharer, 1, repriced, timeout=5.0)
assert re and re[0]["maxBitrate"] == round(cap * 0.35), re
# From here on, every new RTCPeerConnection on the sharer page is
# the late joiner's leg: the wrapper records the committed parameters
# at setLocalDescription time, before negotiate()'s re-apply.
assert sharer.evaluate(
"""() => {
window.__firstOffers = [];
const RealPC = window.RTCPeerConnection;
window.RTCPeerConnection = function (config) {
const pc = new RealPC(config);
const orig = pc.setLocalDescription.bind(pc);
pc.setLocalDescription = (d) => orig(d).then(() => {
const sender = pc.getSenders().find(
(s) => s.track && s.track.kind === 'video'
);
const params = sender ? sender.getParameters() : {};
const enc = (params.encodings || [])[0] || {};
window.__firstOffers.push({
maxBitrate: 'maxBitrate' in enc ? enc.maxBitrate : null,
degradationPreference: params.degradationPreference || null,
});
return pc;
});
return pc;
};
window.RTCPeerConnection.prototype = RealPC.prototype;
window.RTCPeerConnection.static = RealPC.static;
return true;
}"""
)
late = device("/watch", errors)
expect(late.locator(".tile[data-state='live']")).to_have_count(1, timeout=25_000)
# Both legs are now live under the switched preset.
legs = _wait_for(sharer, 2, lambda leg: leg["state"] == "connected")
assert legs and all(leg["state"] == "connected" for leg in legs), legs
offers = sharer.evaluate("() => window.__firstOffers") or []
assert offers, "no first offer was captured from the late joiner's leg"
# The first offer carried the new cap: addPeer's sendEncodings, not
# the post-negotiation re-apply, which ran later on the same leg.
assert offers[-1]["maxBitrate"] == round(cap * 0.35), offers
# And the settled leg is still capped — nothing drifted after.
assert all(leg["maxBitrate"] == round(cap * 0.35) for leg in legs), legs
assert sharer.evaluate("() => window.__probe.peers.size") == 2
# The late joiner's picture is a live one, and the sharer agrees
# that two viewers are watching.
frames = frames_matching(late, ".tile video")
assert frames["distinct"] >= 2, f"the late joiner froze: {frames}"
expect(sharer.locator("#viewer-count")).to_have_text("2", timeout=10_000)
assert errors == [], f"uncaught browser errors: {errors}"
def test_the_panel_shows_the_budget_and_the_codec(self, device, base_url):
"""The panel says what the share is actually costing (phase 06 task
05): the quality budget in force — the selected preset, its capture
ceiling, its frame rate and its per-leg share of the server's cap —
and the codec the leg actually negotiated, resolved in the same
`getStats()` read the 1 s tick already makes. The codec is `—` where
the engine does not report it: the panel must never guess. Both
lines reset to `—` when the share stops."""
import httpx
cap = httpx.get(f"{base_url}/api/config", timeout=5).json()["maxBitrateBps"]
assert cap > 0
errors: list = []
sharer = device("/mirror", errors)
sharer.select_option("#quality", "balanced")
start_sharing(sharer)
# The budget line is written by start() from state: an exact match,
# not a substring — the panel must not misprice the share. The cap
# is read live from /api/config (never hardcoded) and scaled by the
# preset's factor, exactly as legCap() does.
expected = (
f"balanced · ≤1920×1200 @24fps · " # noqa: RUF001 (the panel line itself carries the multiplication sign)
f"{_bitrate_format(round(cap * 0.6))} /viewer"
)
expect(sharer.locator("#stat-quality")).to_have_text(expected, timeout=10_000)
viewer = device("/watch", errors)
expect(viewer.locator(".tile[data-state='live']")).to_have_count(1, timeout=25_000)
legs = _wait_for(sharer, 1, lambda leg: leg["state"] == "connected")
assert legs and legs[0]["state"] == "connected", legs
# The codec line is written once per 1 s stats tick (no extra
# getStats() call): let a tick run after the leg is up, then read it.
time.sleep(2.5)
codec = (sharer.locator("#stat-codec").text_content() or "").strip()
# The shape, not the codec: which one the engine negotiates is not a
# contract. `—` is the honest floor where the engine does not expose
# the outbound codec; a name must be the short form
# (`video/VP8` → `VP8`), never a guess.
assert codec == "—" or re.fullmatch(r"[A-Z0-9/]{2,16}", codec), codec
# The share is still a live one while the panel reports on it.
frames = frames_matching(viewer, ".tile video")
assert frames["distinct"] >= 2, frames
# Stopping resets both lines: `—`, not a stale reading.
stop_sharing(sharer)
expect(sharer.locator("#stat-quality")).to_have_text("—", timeout=10_000)
expect(sharer.locator("#stat-codec")).to_have_text("—", timeout=10_000)
assert errors == [], f"uncaught browser errors: {errors}"
def test_q_cycles_the_preset_and_says_so(self, device):
"""``q`` cycles the preset on the share page (sharp → balanced →
light → sharp) and toasts the new one, naming it and its budget;
a ``q`` typed into ``#name`` is text, not a command, and never
changes the select (the router's text-field guard)."""
errors: list = []
page = device("/mirror", errors)
toast = page.locator("#toast")
page.keyboard.press("q")
expect(page.locator("#quality")).to_have_value("balanced", timeout=5_000)
assert page.evaluate("() => window.__probe.qualityKey") == "balanced"
expect(toast).to_contain_text("Balanced", timeout=5_000)
text = toast.text_content() or ""
assert "≤1920×1200" in text, text # noqa: RUF001 (the toast itself carries the multiplication sign)
# Nothing is shared yet: the capture size lands on the next share,
# and the line says so rather than pretending it rescaled.
assert "next share" in text, text
page.keyboard.press("q")
expect(page.locator("#quality")).to_have_value("light")
expect(toast).to_contain_text("Light", timeout=5_000)
page.keyboard.press("q")
expect(page.locator("#quality")).to_have_value("sharp")
expect(toast).to_contain_text("Sharp", timeout=5_000)
# A ``q`` typed into #name is text, not a command: the select stays.
page.locator("#name").click()
page.keyboard.type("q")
expect(page.locator("#quality")).to_have_value("sharp")
assert errors == [], f"uncaught browser errors: {errors}"