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
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).
|
||||
Reference in New Issue
Block a user