From 3f93481c542d23eec6870f9ba5e74bbc763cdc7d Mon Sep 17 00:00:00 2001 From: ducoterra Date: Mon, 2 Nov 2026 22:07:17 -0500 Subject: [PATCH] phase: 06_sharer_quality_presets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Phase 06 — final verification pass: 100% green.** - **Verified (all 6 tasks complete in working tree):** `#quality` select + `PRESETS` table; preset-driven capture request + `contentHint`; per-leg `legCap()`/`degradation` at both call sites; live `applyQuality()` with Overconstrained fallback + republish; `q` key cycle; `#stat-quality`/`#stat-codec` readouts; full e2e matrix (requests, committed params ×3, zero-cap, mid-share switch, unrescalable device, late-joiner first-offer, readout, `q`); realdisplay delivered-pixels `light` case. `test_sender_fidelity.py` **untouched and green** (regression canary). - **Gates:** `uv run ruff check . && uv run ruff format --check .` → clean (32 files); `uv run pyright` → 0 errors; `uv run pytest` → **284 passed, 5 skipped (realdisplay-gated), coverage 95%** (93s); `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` → **4 passed, 1 skipped** (no audio source on host — documented platform condition). - **Completion criteria:** all 11 ✓ — sharp default reproduces old request; light ≤960×600@15fps/motion/round(cap×0.35)/maintain-framerate with moving viewer frames; mid-share re-prices with `peers.size` unchanged, no renegotiation, roster matches `getSettings()`; refused-rescale toasts "next share"; `q` cycles + toasts, text-field guard holds; readout exact + codec shape, both reset to `—` idle; realdisplay: light decoded ≤990px (measured 320×200), sharp clears `SHARP_FLOOR_PX`; no new env/wire/Python/asset; suite 100% green, coverage >90%. - **Docs:** README.md reviewed in full against code — Quality table, `q` key, env-var table, routes (`/share`), kiosk commands, key table, doc paths all consistent; **no fixes needed**. AGENTS.md — verified, no stale statements, no changes. PLAN.md untouched (diff = 0). - **Reported (per phase, not edited):** PLAN.md §6 line 204 still says "per-leg `maxBitrate` cap (default **12 Mbit/s**)" — code has defaulted to **25 Mbit/s** since phase 02; needs a user-approved one-line amendment. - **Next pending phase:** none — 06 is the last phase in `todo/`; the roadmap is complete. --- .../06_sharer_quality_presets/00_phase.md | 121 ++++ .../01_quality_selector_and_preset_table.md | 119 ++++ .../02_capture_constraints_from_the_preset.md | 80 +++ .../03_sender_parameters_from_the_preset.md | 63 ++ .../04_apply_live_and_republish.md | 71 ++ .../05_quality_and_codec_readout.md | 58 ++ .../06_preset_e2e_suite_and_docs.md | 81 +++ ...06_sharer_quality_presets__00_phase.a1.err | 0 .../06_sharer_quality_presets__00_phase.a1.md | 8 + ...rer_quality_presets__00_phase.a1.md.status | 1 + ...arer_quality_presets__00_phase.a1.validate | 36 + ...1_quality_selector_and_preset_table.a1.err | 0 ...01_quality_selector_and_preset_table.a1.md | 13 + ...ity_selector_and_preset_table.a1.md.status | 1 + ...lity_selector_and_preset_table.a1.validate | 34 + ...capture_constraints_from_the_preset.a1.err | 0 ..._capture_constraints_from_the_preset.a1.md | 20 + ...e_constraints_from_the_preset.a1.md.status | 1 + ...re_constraints_from_the_preset.a1.validate | 34 + ...3_sender_parameters_from_the_preset.a1.err | 0 ...03_sender_parameters_from_the_preset.a1.md | 20 + ...er_parameters_from_the_preset.a1.md.status | 1 + ...der_parameters_from_the_preset.a1.validate | 34 + ...resets__04_apply_live_and_republish.a1.err | 0 ...presets__04_apply_live_and_republish.a1.md | 12 + ...__04_apply_live_and_republish.a1.md.status | 1 + ...s__04_apply_live_and_republish.a1.validate | 34 + ...esets__05_quality_and_codec_readout.a1.err | 0 ...resets__05_quality_and_codec_readout.a1.md | 21 + ..._05_quality_and_codec_readout.a1.md.status | 1 + ...__05_quality_and_codec_readout.a1.validate | 34 + ...esets__06_preset_e2e_suite_and_docs.a1.err | 0 ...resets__06_preset_e2e_suite_and_docs.a1.md | 18 + ..._06_preset_e2e_suite_and_docs.a1.md.status | 1 + ...__06_preset_e2e_suite_and_docs.a1.validate | 36 + README.md | 36 +- SECURITY.md | 10 +- src/mirror/static/app.css | 2 +- src/mirror/static/mirror.html | 12 + src/mirror/static/share.js | 371 +++++++++-- tests/e2e/conftest.py | 14 +- tests/e2e/test_quality_presets.py | 619 ++++++++++++++++++ tests/e2e/test_x11_capture.py | 99 +++ tests/test_app.py | 44 +- 44 files changed, 2107 insertions(+), 54 deletions(-) create mode 100644 .agents/phases/complete/06_sharer_quality_presets/00_phase.md create mode 100644 .agents/phases/complete/06_sharer_quality_presets/01_quality_selector_and_preset_table.md create mode 100644 .agents/phases/complete/06_sharer_quality_presets/02_capture_constraints_from_the_preset.md create mode 100644 .agents/phases/complete/06_sharer_quality_presets/03_sender_parameters_from_the_preset.md create mode 100644 .agents/phases/complete/06_sharer_quality_presets/04_apply_live_and_republish.md create mode 100644 .agents/phases/complete/06_sharer_quality_presets/05_quality_and_codec_readout.md create mode 100644 .agents/phases/complete/06_sharer_quality_presets/06_preset_e2e_suite_and_docs.md create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__00_phase.a1.err create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__00_phase.a1.md create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__00_phase.a1.md.status create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__00_phase.a1.validate create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__01_quality_selector_and_preset_table.a1.err create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__01_quality_selector_and_preset_table.a1.md create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__01_quality_selector_and_preset_table.a1.md.status create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__01_quality_selector_and_preset_table.a1.validate create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__02_capture_constraints_from_the_preset.a1.err create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__02_capture_constraints_from_the_preset.a1.md create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__02_capture_constraints_from_the_preset.a1.md.status create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__02_capture_constraints_from_the_preset.a1.validate create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__03_sender_parameters_from_the_preset.a1.err create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__03_sender_parameters_from_the_preset.a1.md create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__03_sender_parameters_from_the_preset.a1.md.status create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__03_sender_parameters_from_the_preset.a1.validate create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__04_apply_live_and_republish.a1.err create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__04_apply_live_and_republish.a1.md create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__04_apply_live_and_republish.a1.md.status create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__04_apply_live_and_republish.a1.validate create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__05_quality_and_codec_readout.a1.err create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__05_quality_and_codec_readout.a1.md create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__05_quality_and_codec_readout.a1.md.status create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__05_quality_and_codec_readout.a1.validate create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__06_preset_e2e_suite_and_docs.a1.err create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__06_preset_e2e_suite_and_docs.a1.md create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__06_preset_e2e_suite_and_docs.a1.md.status create mode 100644 .agents/reports/06_sharer_quality_presets/06_sharer_quality_presets__06_preset_e2e_suite_and_docs.a1.validate create mode 100644 tests/e2e/test_quality_presets.py 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** `