Files
mirror/.agents/phases/todo/02_sharp_capture/00_phase.md
T
ducoterra 32c6bc272b chore(agent): phase roadmap from TODO.md, 3 phases
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
2026-11-01 19:41:24 -05:00

3.9 KiB
Raw Blame History

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).