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:
@@ -0,0 +1,70 @@
|
||||
# Phase 02 — A sharp picture on the wall
|
||||
|
||||
**Source:** `TODO.md` L4 — "The screen sharing should be sharper, it's a little fuzzy when sharing my display."
|
||||
**Story:** n/a
|
||||
**Context:** `.agents/PLAN.md` §5 (the browser-harness finding *"Chromium scales a monitor capture to the capturing window"*, and the rule that media is asserted on the **viewer's** `<video>`), §6 ("Mesh upload = N × bitrate per sharer … per-leg `maxBitrate` cap"); `src/mirror/static/share.js` (`pickSurface`, `addPeer`), `src/mirror/config.py`, `src/mirror/static/net.js`
|
||||
|
||||
## Objective
|
||||
|
||||
Text on a shared screen is legible on a full-HD wall. Three things make today's
|
||||
picture fuzzy, and each gets its own task:
|
||||
|
||||
1. **The capture is smaller than the screen.** `pickSurface()` requests
|
||||
`{ frameRate: { ideal: 30, max: 60 } }` and *no* dimensions, so Chromium
|
||||
captures the monitor scaled down to the size of the sharing window (PLAN §5
|
||||
records exactly this: `1280×800 → 1200×750`). A window that is half the screen
|
||||
ships a quarter of the pixels, stretched back up on the wall.
|
||||
2. **The encoder is allowed to throw resolution away.** Nothing sets
|
||||
`degradationPreference`, so the default `"balanced"` trades pixels for frame
|
||||
rate the moment the LAN hiccups — the opposite of what a screen of text needs.
|
||||
3. **12 Mbit/s is not enough for the pixels we now send.** A 1440p text-heavy
|
||||
surface at 12 Mbit/s is where macroblocking comes from.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `01_keyboard_kiosk_control` (todo) — no functional dependency, but it runs
|
||||
first and its E2E suites (`tests/e2e/test_keyboard_wall.py`) must still pass
|
||||
here; this phase changes the same two files (`share.js`, `watch.js` untouched).
|
||||
|
||||
## Tasks
|
||||
|
||||
1. `01_capture_resolution_constraints.md` — ask the picker for the screen's real
|
||||
pixels (`screen × devicePixelRatio`) instead of the window's.
|
||||
2. `02_sender_fidelity.md` — per-leg `RTCRtpSender` parameters:
|
||||
`maintain-resolution`, re-applied after every renegotiation.
|
||||
3. `03_bitrate_default_and_surface.md` — raise the per-leg cap default to
|
||||
25 Mbit/s and keep server, client fallback, `.env.example` and README honest
|
||||
about it.
|
||||
4. `04_sharpness_assertion_e2e.md` — a real-desktop assertion on the **viewer's**
|
||||
decoded size, so "sharper" stops being a feeling.
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- The media rule from `AGENTS.md` §5 / PLAN §5 governs this whole phase: assert on
|
||||
the **viewer's** `<video>` (`videoWidth`, `videoHeight`, `framesDecoded`,
|
||||
inbound bitrate), never on the sharer's preview.
|
||||
- The `realdisplay` suite is where sharpness is actually provable
|
||||
(`MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay`) because it captures a real
|
||||
desktop with a real encoder; the fake-device suite proves the constraints do not
|
||||
break the deterministic 1280×720 path.
|
||||
- Python side (`config.py` default) is unit + API tested: `tests/test_config.py`,
|
||||
`tests/test_app.py::TestJsonApi`.
|
||||
- No new external asset, no new dependency, no framework (PLAN A9/A10).
|
||||
- Coverage **>90%** on new/modified code; full gate per task:
|
||||
`uv run ruff check . && uv run ruff format --check . && uv run pyright && uv run pytest`.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` asserts the viewer's
|
||||
decoded width is ≥ 90% of the Xvfb desktop width (today it only asserts the
|
||||
aspect ratio).
|
||||
- [ ] `/api/config` reports `maxBitrateBps: 25000000` by default and `net.js`'s
|
||||
offline fallback agrees.
|
||||
- [ ] The share page's own stats (`#stat-resolution`) match what the viewer
|
||||
decodes, within the documented downscale the browser applies.
|
||||
- [ ] `uv run pytest` and `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay`
|
||||
both green; `test_…aspect ratio…` guard still distinguishes real capture
|
||||
from the fake device.
|
||||
- [ ] README "Configuration" documents the new default; the commit message flags
|
||||
that `PLAN.md` §6's "default 12 Mbit/s" figure is now stale for the user to
|
||||
amend (this phase must not edit `PLAN.md` itself).
|
||||
@@ -0,0 +1,84 @@
|
||||
# Task 01 — Capture the screen's pixels, not the window's
|
||||
|
||||
**Phase:** `02_sharp_capture` · **Source:** `TODO.md:4` — "The screen sharing should be sharper, it's a little fuzzy when sharing my display."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
Ask `getDisplayMedia()` for the surface's real pixel dimensions. Today the
|
||||
request carries a frame rate and nothing else, so Chromium hands back the monitor
|
||||
rescaled to the *browser window* — the primary cause of the fuzziness.
|
||||
|
||||
## Work
|
||||
|
||||
1. `src/mirror/static/share.js` — in `pickSurface()`, extend the `video`
|
||||
constraints object (currently `{ frameRate: { ideal: 30, max: 60 } }`) with
|
||||
dimensions computed from the display, not the window:
|
||||
|
||||
```js
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const width = Math.min(Math.round(screen.width * dpr), 3840);
|
||||
const height = Math.min(Math.round(screen.height * dpr), 2160);
|
||||
```
|
||||
|
||||
passed as `width: { ideal: width }, height: { ideal: height }` alongside the
|
||||
existing `frameRate`, keeping `selfBrowserSurface`/`surfaceSwitching`/
|
||||
`monitorTypeSurfaces` untouched.
|
||||
2. `src/mirror/static/share.js` — keep both existing `getDisplayMedia` calls in
|
||||
one place: the `audio: true` attempt and its `audio: false` retry must send the
|
||||
same video constraints, so build the `video` object once and reuse it (this is
|
||||
also what phase 03's audio task relies on; do not change the catch semantics
|
||||
here).
|
||||
3. `src/mirror/static/share.js` — after capture, compare `track.getSettings()`
|
||||
against what was asked. `publish()` already reads the settings for the panel;
|
||||
add a one-line diagnostic: when the delivered width is below ~75% of the
|
||||
requested width, `toast()` once per share —
|
||||
`"Chromium captured N×M: the browser window limits a screen capture, so make the share page bigger for a sharper wall."`
|
||||
Comment the reason (PLAN §5's finding) at the point where it is detected.
|
||||
4. `src/mirror/static/share.js` — keep `video.contentHint = "detail"` exactly
|
||||
where it is (it is the other half of the sharpness story; task 02 adds the
|
||||
sender side).
|
||||
5. Comment in `pickSurface()` stating the rule that the kiosk wall depends on:
|
||||
*a capture is only as sharp as the pixels the sharer's browser agreed to grab,
|
||||
and the request must name them.*
|
||||
|
||||
ASSUMPTION: `ideal`, never `exact`. `exact` fails or hides picker entries on
|
||||
displays/DPRs the browser cannot honour, which would break sharing outright;
|
||||
`ideal` is a preference the browser can decline while still capturing.
|
||||
ASSUMPTION: the request is capped at 3840×2160 so an 8K HiDPI sharer cannot
|
||||
starve a 1080p wall's decoder or its uplink; no new `MIRROR_*` variable is added
|
||||
for this (the existing `MIRROR_MAX_BITRATE` is the tuning knob).
|
||||
ASSUMPTION: `screen.width/height × devicePixelRatio` is used rather than
|
||||
`screen.avail*` (panels shave pixels off) and rather than enumerating
|
||||
`navigator.mediaDevices.enumerateDevices()` (which does not report resolutions
|
||||
for display surfaces).
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- E2E (`uv run pytest -m e2e`): with the deterministic fake device
|
||||
(`--use-fake-device-for-media-stream`, always 1280×720) assert the share still
|
||||
starts and the viewer still decodes `1280×720` — i.e. asking for 4K does not
|
||||
break or change the fake path, and `publish()` still reports the real settings.
|
||||
- Unit-ish guard in `tests/test_app.py::TestClientScripts` style: `share.js` must
|
||||
contain no `exact:` constraint key (regex guard), so a future edit cannot
|
||||
reintroduce a hard constraint that breaks pickers.
|
||||
- Real desktop measurement (not a hard assertion yet — task 04 turns it into one):
|
||||
log the requested vs delivered width from
|
||||
`tests/e2e/test_x11_capture.py::…::test_the_real_desktop_is_captured_not_a_synthetic_one`
|
||||
once during development and record the numbers in the commit message.
|
||||
If Chromium still clamps the capture to the window, say so in the commit message
|
||||
so `.agents/PLAN.md` §5 can be amended by the user — do not edit `PLAN.md` here.
|
||||
- Coverage: **>90%** on this task's new/modified code.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] `pickSurface()` requests explicit `width`/`height` `ideal` values derived
|
||||
from `screen × devicePixelRatio` and capped at 3840×2160.
|
||||
- [ ] Both the audio and silent `getDisplayMedia` attempts use identical video
|
||||
constraints (one shared object).
|
||||
- [ ] `uv run pytest -m e2e` green, viewer still decodes 1280×720 on the fake
|
||||
device and frames still differ (`frames_matching`).
|
||||
- [ ] `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` green, with the measured
|
||||
requested-vs-delivered numbers written into the commit message.
|
||||
- [ ] `uv run ruff check . && uv run ruff format --check . && uv run pyright`
|
||||
clean.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Task 02 — Tell the encoder to keep the pixels
|
||||
|
||||
**Phase:** `02_sharp_capture` · **Source:** `TODO.md:4` — "The screen sharing should be sharper, it's a little fuzzy when sharing my display."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
`contentHint = "detail"` says *this is text*; nothing yet tells the encoder what
|
||||
to sacrifice. With the default `degradationPreference: "balanced"` a LAN hiccup
|
||||
buys frame rate with resolution — the picture goes soft and stays soft. This task
|
||||
pins resolution per leg, and keeps the per-leg bitrate cap working through
|
||||
renegotiation.
|
||||
|
||||
## Work
|
||||
|
||||
1. `src/mirror/static/share.js` — in `addPeer()`, capture the sender returned by
|
||||
`pc.addTransceiver(track, init)` (the `catch` fallback to
|
||||
`pc.addTrack(track, state.stream)` returns a sender too — keep both paths
|
||||
producing one).
|
||||
2. `src/mirror/static/share.js` — add `applySenderQuality(peer)`:
|
||||
* find the video sender on `peer.pc.getSenders()` whose `.track.kind === "video"`;
|
||||
* read `const params = sender.getParameters()`, set
|
||||
`params.degradationPreference = "maintain-resolution"`, and set
|
||||
`params.encodings = (params.encodings?.length ? params.encodings : [{}])`
|
||||
with `maxBitrate = config.maxBitrateBps` on the **first** encoding only;
|
||||
* `await sender.setParameters(params)` inside a `try/catch` that swallows only
|
||||
what it names — Firefox has no `degradationPreference` and `setParameters`
|
||||
rejects there; comment that, since a bare `catch {}` reads like a bug
|
||||
(`AGENTS.md` §7: swallow only what you can name).
|
||||
* guard with `typeof sender.setParameters === "function"` before the call.
|
||||
3. `src/mirror/static/share.js` — call it from `pc.onnegotiationneeded` **after**
|
||||
`setLocalDescription` succeeds (so the encodings exist) and again when
|
||||
`pc.connectionState` becomes `"connected"`: `setParameters` is per-negotiation
|
||||
and a re-offer (surface switch, viewer rejoin) resets the encodings. Note in a
|
||||
comment that `sendEncodings` passed to `addTransceiver` is only the *initial*
|
||||
request.
|
||||
4. `src/mirror/static/share.js` — remove the now-duplicated `init.sendEncodings =
|
||||
[{ maxBitrate: config.maxBitrateBps }]` only if `applySenderQuality` provably
|
||||
covers the same behaviour; otherwise keep both (initial + post-negotiation) and
|
||||
say why in a comment. The cap must never disappear — `AGENTS.md`/PLAN §6 exist
|
||||
because N viewers must not melt one sharer's uplink.
|
||||
5. `src/mirror/static/share.js` — leave `video.contentHint = "detail"` alone, and
|
||||
do not touch the viewer (`watch.js`): this is a sender-side decision, the
|
||||
viewer has nothing to negotiate away (A4: the sharer owns the track set).
|
||||
|
||||
ASSUMPTION: `maintain-resolution` over `"balanced"` — this is a screen-mirroring
|
||||
tool where legible text beats smooth motion, which is the same trade the existing
|
||||
`contentHint = "detail"` already made. Frame rate will drop under congestion;
|
||||
that is the intent, and the sharer's `#stat-perf` line already shows fps so the
|
||||
trade is visible.
|
||||
ASSUMPTION: applied to the video sender only; the audio sender (phase 03) keeps
|
||||
browser defaults.
|
||||
ASSUMPTION: `maxBitrate` stays a *per-leg* cap sourced from `config.maxBitrateBps`
|
||||
(`/api/config`); no per-peer fairness logic — that is PLAN §7 item 4, a later phase.
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- E2E (`-m e2e`) with two viewers on one sharer: after both legs reach
|
||||
`"connected"`, read the sender parameters back in the page
|
||||
(`page.evaluate`) and assert `degradationPreference === "maintain-resolution"`
|
||||
and `encodings[0].maxBitrate === <config value>` for each leg. On the
|
||||
full-Chromium channel this is a real assertion, not a stub.
|
||||
- E2E: switch the shared surface (or trigger a renegotiation by adding a second
|
||||
viewer mid-share) and re-assert the parameters survived — this is the bug the
|
||||
"re-apply after negotiation" step exists for.
|
||||
- E2E regression: the two existing fan-out tests
|
||||
(`test_one_mirror_fans_out_to_several_viewers`,
|
||||
`test_several_mirrors_land_on_one_wall`) stay green and still assert distinct
|
||||
decoded frames on the viewer.
|
||||
- Guard in `tests/test_app.py`: `share.js` contains
|
||||
`"maintain-resolution"` exactly once, and `applySenderQuality` is called from
|
||||
both the negotiation path and the connected path (regex over the file, matching
|
||||
the existing test style).
|
||||
- Coverage: **>90%** on this task's new/modified code.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] Every live leg's video sender reports `degradationPreference:
|
||||
"maintain-resolution"` and the configured `maxBitrate`, read back from the
|
||||
browser in a test.
|
||||
- [ ] The parameters survive a renegotiation (asserted).
|
||||
- [ ] The per-leg bitrate cap is still applied (no leg sends more than
|
||||
`maxBitrateBps` in the outbound `getStats()` sample).
|
||||
- [ ] `uv run pytest -m e2e` green; `MIRROR_REALDISPLAY=1 uv run pytest -m
|
||||
realdisplay` green.
|
||||
- [ ] `uv run ruff check . && uv run ruff format --check . && uv run pyright`
|
||||
clean.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Task 03 — A bitrate default that fits the pixels we now send
|
||||
|
||||
**Phase:** `02_sharp_capture` · **Source:** `TODO.md:4` — "The screen sharing should be sharper, it's a little fuzzy when sharing my display."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
Tasks 01–02 make the encoder send more real pixels; 12 Mbit/s per leg is not
|
||||
enough for them. Raise the default per-leg cap to **25 Mbit/s** (user-confirmed
|
||||
for this roadmap) and keep every place that number appears in agreement — the
|
||||
server default, the client's offline fallback, `.env.example`, the README.
|
||||
|
||||
## Work
|
||||
|
||||
1. `src/mirror/config.py` — `DEFAULT_MAX_BITRATE_BPS = 25_000_000` (was
|
||||
`12_000_000`, L15). It flows to `Settings.max_bitrate_bps` →
|
||||
`client_config()["maxBitrateBps"]` unchanged; no new field.
|
||||
2. `src/mirror/static/net.js` — `loadConfig()`'s offline `fallback` object
|
||||
(L20) hardcodes `maxBitrateBps: 12000000`. Update it to `25000000`. This is the
|
||||
number a browser uses when `/api/config` is unreachable; if it drifts from the
|
||||
server it silently changes picture quality per client, and nothing would fail.
|
||||
3. `.env.example` — `MIRROR_MAX_BITRATE=25000000` (L8) with a one-line comment:
|
||||
per-viewer-leg cap, 25 Mbit/s suits a wired LAN at 1440p, lower it for
|
||||
Wi-Fi sharers.
|
||||
4. `README.md` — the "Configuration" table (~L58) row for
|
||||
`MIRROR_MAX_BITRATE`: new default and the one-sentence trade-off (Wi-Fi sharers
|
||||
with many viewers may need less, not more).
|
||||
5. `tests/test_config.py::TestSettings::test_defaults` — it currently does not pin
|
||||
the bitrate default at all (it asserts port, host, ice_servers, max_streams,
|
||||
cidrs). Add `assert settings.max_bitrate_bps == DEFAULT_MAX_BITRATE_BPS` and
|
||||
import the constant, so the next silent change is a conscious one.
|
||||
6. `tests/test_config.py::TestSettings::test_environment_wins` already covers the
|
||||
override path (`250000`) — leave it.
|
||||
|
||||
ASSUMPTION: 25 Mbit/s per leg (not 50). Chosen with the user for this roadmap:
|
||||
comfortable for 1440p text on a wired LAN while still bounding a sharer's upload
|
||||
when several viewers subscribe. `MIRROR_MAX_BITRATE` remains the escape hatch in
|
||||
both directions, so this is a default change and not a policy change.
|
||||
ASSUMPTION: the ceiling stays *per leg*, exactly as today — a 5-viewer sharer
|
||||
still gets 5 × 25 Mbit/s of headroom available. Aggregate fairness is PLAN §7
|
||||
item 4 ("Capacity & fairness") and is deliberately not smuggled in here.
|
||||
ASSUMPTION: `PLAN.md` §6's "default 12 Mbit/s" text becomes stale with this
|
||||
change; this phase must not edit `PLAN.md` (Protocol B), so the commit message
|
||||
names the stale line for the user to amend.
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- `tests/test_config.py` — the new default assertion, plus the existing
|
||||
"bogus number falls back" case still resolves to `DEFAULT_PORT`-style behaviour
|
||||
(unchanged).
|
||||
- `tests/test_app.py::TestJsonApi::test_config_advertises_the_media_defaults`
|
||||
uses the `settings` fixture (`max_bitrate_bps=2_000_000`), so it must stay
|
||||
**unchanged** — that is the point: it proves the value is honoured end to end,
|
||||
not that it equals a magic number. Add nothing there; verify it still passes.
|
||||
- New cross-file guard in `tests/test_app.py::TestClientScripts` (same regex
|
||||
style as the existing script guards): the bitrate default in
|
||||
`src/mirror/config.py` and the fallback literal in `src/mirror/static/net.js`
|
||||
must be equal — one number, two places, one test that notices.
|
||||
- Coverage: **>90%** on new/modified code (`config.py` is fully covered by
|
||||
`tests/test_config.py`).
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] `uv run python -c "from mirror.config import Settings; print(Settings.from_env().max_bitrate_bps)"`
|
||||
prints `25000000` with no environment set.
|
||||
- [ ] `curl -s localhost:8055/api/config | python -m json.tool` reports
|
||||
`maxBitrateBps: 25000000` on a bare `uv run mirror serve`.
|
||||
- [ ] `grep -n "12000000\|12_000_000"` over `src/`, `.env.example` and
|
||||
`tests/` returns nothing.
|
||||
- [ ] `uv run pytest tests/test_config.py tests/test_app.py` green.
|
||||
- [ ] README + `.env.example` document 25 Mbit/s; commit message names the stale
|
||||
`PLAN.md` §6 line.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Task 04 — Make "sharper" an assertion
|
||||
|
||||
**Phase:** `02_sharp_capture` · **Source:** `TODO.md:4` — "The screen sharing should be sharper, it's a little fuzzy when sharing my display."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
Lock the capability in with a test that fails if the picture goes soft again.
|
||||
Today's real-desktop test asserts only the **aspect ratio** — a deliberately weak
|
||||
assertion while the capture was window-sized. Now that the request names real
|
||||
pixels, assert **delivered size on the viewer**, which is the only number that
|
||||
correlates with the fuzziness a person sees.
|
||||
|
||||
## Work
|
||||
|
||||
1. `tests/e2e/test_x11_capture.py` — keep the existing aspect-ratio guard
|
||||
(`…::test_the_real_desktop_is_captured_not_a_synthetic_one`, which
|
||||
distinguishes the 1280×800 Xvfb screen from the 1280×720 fake device) and add a
|
||||
size floor to the same probe: the viewer's `videoWidth` must be at least 90% of
|
||||
`XVFB_GEOMETRY`'s width (≥ 1152 px), not the ~1200/1280 the window-clamped
|
||||
capture produced before phase 02.
|
||||
* Reuse the existing `browser_env()` (strips `WAYLAND_DISPLAY` and
|
||||
`DBUS_SESSION_BUS_ADDRESS` — **do not** add `--use-fake-ui-for-media-stream`,
|
||||
it kills the X11 capturer with `NotReadableError`) and the `browsers` /
|
||||
`share_page` / `watch_page` fixtures. Two browser **processes**, not contexts.
|
||||
* Assert the size on the **viewer's** `<video>`, plus continuing frame
|
||||
movement (`distinct > 1`) in the same probe, so a large-but-frozen picture
|
||||
cannot pass.
|
||||
2. `tests/e2e/test_x11_capture.py` — assert the *published* metadata agrees: read
|
||||
`/api/streams` over `base_url` and require the published `width`/`height` to be
|
||||
within one rounding step of the sharer's `track.getSettings()` and of the
|
||||
viewer's decoded size (the roster must not lie about what is on the wire).
|
||||
3. `tests/e2e/test_live_mirror.py` — add the fake-device counterpart: the viewer
|
||||
decodes exactly `1280×720` from the deterministic device, proving the new
|
||||
`ideal` constraints changed nothing on the synthetic path.
|
||||
4. `tests/e2e/test_live_mirror.py` — an inbound-bitrate sanity check on a staged
|
||||
tile: the viewer's `inbound-rtp.bytesReceived` grows while a share is live, and
|
||||
with the default 25 Mbit/s cap the delivered figure is recorded in the test's
|
||||
docstring (an observation, not an equality assertion — the value is
|
||||
environment-dependent and an over-tight assertion here is a flake factory).
|
||||
5. `tests/e2e/test_x11_capture.py` module docstring — record the measured
|
||||
requested-vs-delivered numbers for this machine, replacing the vague
|
||||
"Chromium scales a monitor capture to the capturing window" note in the test's
|
||||
comment with what was actually observed after phase 02. If the measurement
|
||||
contradicts `.agents/PLAN.md` §5, say so in the commit message for the user;
|
||||
this phase does not edit `PLAN.md`.
|
||||
|
||||
ASSUMPTION: the floor is 90% of the desktop width rather than 100%. Compositors,
|
||||
fractional DPI and the header bar make an exact match environment-specific; 90%
|
||||
still fails loudly on a window-clamped capture (which lands near 60–75%).
|
||||
ASSUMPTION: the size floor is asserted in the `realdisplay` suite only. The
|
||||
fake-device suite cannot prove sharpness (it always returns 1280×720), so it
|
||||
asserts non-regression instead — per `AGENTS.md` §5, media is asserted where it is
|
||||
real.
|
||||
ASSUMPTION: `MIRROR_REALDISPLAY=1` remains opt-in and CI-optional; the sharpness
|
||||
gate is therefore a *release* gate run on the Linux reference host, and the
|
||||
fake-device assertions are the *every-commit* gate.
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay -k sharp` — the new size
|
||||
floor passes on the reference Linux host and **fails** if phase 02 task 01's
|
||||
constraints are reverted (verify this by temporarily removing the constraints;
|
||||
write the observed failure in the commit message).
|
||||
- `uv run pytest -m e2e` — fake-device size + inbound-bytes assertions green.
|
||||
- `uv run pytest` full suite green; coverage gate unchanged (~94%).
|
||||
- No new browser flags beyond what `AGENTS.md` §5 already blesses.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] A real-desktop test asserts viewer-decoded width ≥ 1152 px on a 1280×800
|
||||
Xvfb desktop, and fails when the capture constraints are removed.
|
||||
- [ ] `/api/streams` metadata agrees with the decoded size.
|
||||
- [ ] The fake-device suite still decodes exactly 1280×720 with moving frames.
|
||||
- [ ] Measured numbers recorded in the test docstring + commit message; any
|
||||
`PLAN.md` §5 discrepancy reported to the user rather than edited here.
|
||||
- [ ] Full gate: `uv run ruff check . && uv run ruff format --check . && uv run
|
||||
pyright && uv run pytest && MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay`.
|
||||
Reference in New Issue
Block a user