phase: 02_sharp_capture
Build and Push Containers / test (push) Successful in 46s
Build and Push Containers / build-and-push-app (push) Successful in 8s

**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:
2026-11-01 21:41:28 -05:00
parent 1e816da5a8
commit 1da466a412
36 changed files with 938 additions and 26 deletions
@@ -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).