Convert the three open TODO items into .agents/phases/todo/ directories (Protocol B append — PLAN.md, AGENTS.md and existing phases untouched, so PLAN.md's roadmap table does not list these phases): - 01_keyboard_kiosk_control (TODO.md:3) — mouse-free /watch and /mirror: key router, j/k/Enter/f/g/n/p/1-9/m/?, focus-visible controls, help overlay. - 02_sharp_capture (TODO.md:4) — capture screen pixels instead of window pixels, maintain-resolution per leg, 12 -> 25 Mbit/s default cap, real-desktop size assertion on the viewer. - 03_audio_share (TODO.md:5) — one accumulating MediaStream per tile (the ontrack overwrite dropped the first track), staged-only unmute policy, honest audio states on /mirror, viewer-side inbound-audio assertion. Every task cites its TODO source line. Locked with the user: 25 Mbit/s default, only the staged mirror can be audible, full key map. Note for the user: PLAN.md section 6 still says the per-leg cap defaults to 12 Mbit/s; phase 02 task 03 changes that and this commit does not edit PLAN.md. LLM-Generated: true
3.9 KiB
3.9 KiB
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:
- 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. - 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. - 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.jsuntouched).
Tasks
01_capture_resolution_constraints.md— ask the picker for the screen's real pixels (screen × devicePixelRatio) instead of the window's.02_sender_fidelity.md— per-legRTCRtpSenderparameters:maintain-resolution, re-applied after every renegotiation.03_bitrate_default_and_surface.md— raise the per-leg cap default to 25 Mbit/s and keep server, client fallback,.env.exampleand README honest about it.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
realdisplaysuite 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.pydefault) 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 realdisplayasserts the viewer's decoded width is ≥ 90% of the Xvfb desktop width (today it only asserts the aspect ratio)./api/configreportsmaxBitrateBps: 25000000by default andnet.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 pytestandMIRROR_REALDISPLAY=1 uv run pytest -m realdisplayboth 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 editPLAN.mditself).