diff --git a/.agents/phases/complete/06_sharer_quality_presets/00_phase.md b/.agents/phases/complete/06_sharer_quality_presets/00_phase.md new file mode 100644 index 0000000..1f4d8b7 --- /dev/null +++ b/.agents/phases/complete/06_sharer_quality_presets/00_phase.md @@ -0,0 +1,121 @@ +# Phase 06 — The sharer picks the quality/CPU trade + +**Source:** chat — "I'm sharing my screen and it's taking a lot of cpu resources. Anything we can +do about that?" Measured on the reporting machine while it shared: one LibreWolf content process at +**~105% CPU**, hot threads `WebrtcWorker #105 ×9, #107, #108, #109` — i.e. the *sender-side video +encoder*, not the page and not the Python server (A3: media never touches the server). The same +machine drives a 2560×1600 panel and a 3840×2160 primary, so `pickSurface()` asks for ~8.3 Mpx at +30 fps and phase 02 then told the encoder it may never buy relief with resolution. +**Story:** n/a +**Context:** `PLAN.md` §2 A3 (mesh, one `RTCPeerConnection` per viewer → one encode per leg), A9/A10 +(hand-written ES modules, no new assets or dependencies), A12 (env config; this phase adds none), +§6 ("Mesh upload = N × bitrate per sharer"), §7 item 4 (capacity & fairness — deliberately *not* +this phase); `AGENTS.md` §5/§6 (media asserted on the viewer, `node --check`, element/id guards); +`src/mirror/static/share.js` (`pickSurface`, `applySenderQuality`, `addPeer`, `sampleStats`, the +share-page key router), `src/mirror/static/mirror.html` (the Capture panel), `src/mirror/config.py` +(`max_bitrate_bps` → `/api/config`), `.agents/phases/complete/02_sharp_capture` + +## Objective + +`/mirror` grows a **Quality** control — `Sharp · Balanced · Light` — so a sharer can spend pixels, +frame rate and bitrate for *their* machine instead of inheriting one global server default. Each +preset maps to a capture pixel budget + frame rate, a per-leg bitrate factor, a `contentHint` and a +`degradationPreference`, applies live where the engine allows, and is readable in the panel +alongside the codec actually in use. + +`Sharp` **is** today's behaviour and stays the default, so nothing a completed phase proved gets +worse; the new presets only ever ask for less. + +## Dependencies +- `02_sharp_capture` (complete) — the code this phase parameterises (`pickSurface`'s size request, + `applySenderQuality`, `sendEncodings`) and the sharpness floor it must not regress: + `tests/e2e/test_sender_fidelity.py` asserts `degradationPreference === "maintain-resolution"` and + `maxBitrate == /api/config.maxBitrateBps` on the **default** preset, with two legs. +- `01_keyboard_kiosk_control` (complete) — the share page's own key router (`s`/`x`/`Esc`), which + the new `q` joins under the same contract. +- `05_autohide_stage_controls` (complete) — the current `tests/e2e/` two-context fixture style this + phase's suite copies. + +## Tasks +1. `01_quality_selector_and_preset_table.md` — the `#quality` select in the Capture panel plus the + `PRESETS` table in `share.js`; no capture or encoder behaviour changes yet +2. `02_capture_constraints_from_the_preset.md` — `pickSurface()` builds its size/frame-rate request + (and the track's `contentHint`) from the selected preset +3. `03_sender_parameters_from_the_preset.md` — per-leg `maxBitrate` factor and + `degradationPreference` come from the preset, at both call sites +4. `04_apply_live_and_republish.md` — a preset change while live re-parameterises every existing + leg and re-applies the capture constraints without a renegotiation; `q` cycles it +5. `05_quality_and_codec_readout.md` — `#stat-quality` / `#stat-codec` in the panel, fed by the + existing 1 s stats tick +6. `06_preset_e2e_suite_and_docs.md` — the full preset matrix in the browser, a `realdisplay` + delivered-pixels case, and the README/SECURITY consistency pass + +## Scope boundaries (explicit) + +- **No new environment variable, no new wire field, no Python behaviour change.** + `MIRROR_MAX_BITRATE` stays the per-leg ceiling and each preset's factor is applied to it, so one + server knob still governs the fleet. A12/A5/A6 untouched. +- **No watcher-count fairness.** Dividing the cap by viewer count would change + `test_two_live_legs_report_maintain_resolution_and_the_cap`, i.e. the behaviour a completed phase + asserts. That is PLAN §7 item 4 and needs the user's decision first, so it is a separate phase. +- **No codec preference (`setCodecPreferences`).** Measured on the reporting machine, VA-API on both + GPUs exposes `AV1Profile0 : VAEntrypointEncSlice` and **no** H.264/HEVC encode entrypoint, and + Firefox's VA-API encode path is H.264-only — so from LibreWolf there is no hardware encoder to + prefer. A codec-preference phase needs a measured problem on the Chromium side. +- **No memory of the choice** (no `localStorage`): a silently persisted `Light` is a worse wall for + the next person than one extra click. +- **No change to the default.** Default `sharp` reproduces phase 02 byte for byte. + +## Testing & Quality +- Unit/integration: the media behaviour is frontend — per `PLAN.md` §5 it is asserted **in the + browser**, on the **viewer's** `