phase: 06_sharer_quality_presets
Build and Push Containers / test (push) Successful in 1m12s
Build and Push Containers / build-and-push-app (push) Successful in 9s

**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.
This commit is contained in:
2026-11-02 22:07:17 -05:00
parent f51d0fe9ae
commit 3f93481c54
44 changed files with 2107 additions and 54 deletions
@@ -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** `<video>` (decoded size, frames that differ), never by unit tests
pretending to be WebRTC. Structure guards live in `tests/test_app.py`: the `$("#id")` existence
test, the cross-page id-collision test (`{"net","notice","toast"}` only), the
`test_share_never_asks_for_an_exact_capture_size` guard (presets use `ideal`, never `exact`), and
one new guard that the option values in `mirror.html` and the keys of `PRESETS` are the same three
strings (one list, two places, one test that notices — same spirit as the bitrate-default test).
- Coverage: **>90%** on new/modified code.
- The phase is "Complete" only when the **entire** project test suite passes 100% (every test in the
project, not just this phase's), including `uv run pytest -m e2e` and
`MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay`, plus the other three gates of `AGENTS.md` §4.
- `tests/e2e/test_sender_fidelity.py` is the regression canary for this phase and must stay green
**unmodified**.
## Documentation & Consistency
- The final pass reviews `README.md` in full against the project as it stands after this phase —
fixing every inconsistency found, **including pre-existing ones not caused by this phase**. For
`AGENTS.md` it fixes only *stale statements* (a dead command, a changed rule, an outdated fact)
and **never adds or expands its content** (a file that states its own update policy, e.g. "updated
rarely, if ever", wins).
- Tasks update `README.md` immediately when their own changes are user-visible (setup, commands,
configuration, features, conventions), and `AGENTS.md` only when a statement in it became stale.
- `SECURITY.md` currently says `MIRROR_MAX_BITRATE` "bounds per-viewer upload"; after task 03 that
bound is factor-scaled by the sharer's preset. Task 06 makes that sentence true again.
- `PLAN.md` §6's "per-leg `maxBitrate` cap (default 12 Mbit/s)" is already stale (phase 02 raised the
default to 25 Mbit/s). This phase must **not** edit `PLAN.md`; report it to the user instead.
## Completion Criteria
- [ ] `/mirror` renders a `#quality` control with exactly `sharp|balanced|light`; `sharp` is the
selected default and reproduces today's request (3840×2160 cap, `frameRate.ideal 30`) — proven
by `tests/e2e/test_sender_fidelity.py` passing unmodified
- [ ] selecting `light` **before** sharing requests ≤ 960×600 at 15 fps (`__probe.lastRequested`),
sets `contentHint: "motion"`, and every leg commits `maxBitrate == round(cap × 0.35)` with
`degradationPreference == "maintain-framerate"` — while the viewer still shows ≥2 distinct
frames
- [ ] changing the preset **mid-share** re-parameterises the existing legs with no new
`RTCPeerConnection` (`#viewer-count` and `__probe.peers.size` unchanged) and no renegotiation,
and the viewer keeps moving
- [ ] where the engine honours `applyConstraints`, the capture follows without a new picker and
`/api/streams` width/height match the sharer's `track.getSettings()` (the republish happened);
where it does not, the panel toasts that the change lands on the next share — no silent lie
- [ ] `q` cycles the preset on the share page and toasts the new one; keys typed in `#name` and the
select remain text, not commands
- [ ] `#stat-quality` shows the effective budget and `#stat-codec` shows the negotiated codec, or `—`
where the engine does not report it; both reset to `—` when idle
- [ ] `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` proves delivered pixels follow the preset:
`light` decodes at or under its cap, `sharp` still clears `SHARP_FLOOR_PX`
- [ ] no new env var, no new wire field, no Python behaviour change, no external asset, no framework
- [ ] full test suite **100% passing** (every test in the project, not just this phase's), coverage >90%
- [ ] README.md is accurate and consistent with the whole project; AGENTS.md has no stale statements
(verified in the final pass); `PLAN.md` untouched, its stale 12 Mbit/s figure reported to the user
- [ ] no behavior change in completed phases
@@ -0,0 +1,119 @@
# Task 01 — The Quality control and the preset table
**Phase:** `06_sharer_quality_presets` · **Story:** n/a
## Objective
Put a `#quality` select in the share panel and the `PRESETS` table in `share.js`, wired so that the
default (`sharp`) is provably today's behaviour. **No capture or encoder behaviour changes in this
task** — later tasks read the table.
## Work
1. `src/mirror/static/mirror.html` — inside `<aside class="panel">`, directly under
`<h2>Capture</h2>` and **before** `<dl class="stats">`, add the control. Keep the `<label>`-wraps-
control shape used by `#name` so it inherits the panel's field styling:
```html
<label>
Quality
<select id="quality" autocomplete="off">
<option value="sharp" selected>Sharp — every pixel, most CPU</option>
<option value="balanced">Balanced — 1080p-class, less CPU</option>
<option value="light">Light — small and cheap</option>
</select>
</label>
```
`quality` is a new id on `mirror.html` only — the cross-page test
(`test_the_two_pages_share_no_element_ids_but_the_shared_chrome`) allows exactly
`{"net","notice","toast"}` to be shared, so do **not** add it to `watch.html` or `index.html`.
2. `src/mirror/static/app.css` — extend the existing field rule at L339
`.panel input[type="text"] { … }` to `.panel input[type="text"], .panel select { … }` so the
select gets the same chrome. Do not add a `display` declaration anywhere and do not touch
`dl.stats`, `.panel label` or `[hidden]` — `TestStylesheet` guards those.
3. `src/mirror/static/share.js` — add `quality: $("#quality"),` to the `ui` map (the element test
`test_every_element_the_script_reaches_for_exists` requires the id from step 1 to exist).
4. `src/mirror/static/share.js` — add the preset table at module scope, next to `surfaceName()`,
exactly these three keys and numbers. The table is the whole product decision, so every field is
commented with what it buys:
```js
/* The sharer's side of the CPU/cpu-sharpness trade (phase 06): the encoder on a software-only
* machine is ~1 core at 4K30, and `MIRROR_MAX_BITRATE` alone cannot express "fewer pixels".
* Each preset is one request to the capture device and one per-leg encoder budget.
*
* - `max` pixel ceiling for the size request, still `ideal` (never `exact` —
* `test_share_never_asks_for_an_exact_capture_size`); the request is
* min(screen × devicePixelRatio, max).
* - `frameRate` the capture's frame-rate request; a static page of text needs far less
* than 30 and every frame costs an encode.
* - `bitrateFactor` multiplied into `config.maxBitrateBps` — the server keeps the ceiling, the
* sharer picks the fraction of it (A12: one knob, no new env var).
* - `contentHint` `detail` for text, `motion` where smoothness is the point.
* - `degradation` what the encoder may sacrifice under pressure. Firefox rejects
* `degradationPreference` outright; applySenderQuality's named
* NotAllowedError catch already covers that. */
const PRESETS = Object.freeze({
sharp: {
max: [3840, 2160],
frameRate: { ideal: 30, max: 60 },
bitrateFactor: 1,
contentHint: "detail",
degradation: "maintain-resolution",
},
balanced: {
max: [1920, 1200],
frameRate: { ideal: 24, max: 60 },
bitrateFactor: 0.6,
contentHint: "detail",
degradation: "balanced",
},
light: {
max: [960, 600],
frameRate: { ideal: 15, max: 30 },
bitrateFactor: 0.35,
contentHint: "motion",
degradation: "maintain-framerate",
},
});
const DEFAULT_PRESET = "sharp";
/** The preset the control selects; an unknown or absent value falls back to `sharp`, which is
* today's behaviour, so a missing element or a stale value can never degrade the wall. */
function currentPreset() {
const key = ui.quality && ui.quality.value;
return PRESETS[key] ? key : DEFAULT_PRESET;
}
```
`sharp`'s numbers are today's hardcoded values verbatim — that is the no-regression argument, and
tasks 02/03 replace the literals with reads from this table.
5. Do **not** touch `pickSurface()`, `applySenderQuality()`, `addPeer()` or the stats tick yet.
6. `README.md` — under the `/mirror` usage section, add a short paragraph: the share page has a
Quality control (`Sharp` default = every pixel; `Balanced`; `Light`), it trades capture size,
frame rate and per-viewer bitrate, and it is per sharer. Task 06 writes the full table; here, one
accurate paragraph is enough.
## Testing & Quality
- Unit/integration: add to `tests/test_app.py::TestClientScripts` a
`test_the_quality_options_are_the_three_presets` guard — the `value="…"` set of `#quality` in
`mirror.html` and the keys of `PRESETS` in `share.js` must be exactly
`{"sharp","balanced","light"}` (regex over both files; same "one number, two places, one test that
notices" shape as `test_server_and_client_agree_on_the_bitrate_default`).
- The existing guards must stay green unchanged: `test_scripts_parse_as_es_modules`
(`node --check`), `test_every_element_the_script_reaches_for_exists`,
`test_the_two_pages_share_no_element_ids_but_the_shared_chrome`,
`test_share_never_asks_for_an_exact_capture_size`, `TestStylesheet`.
- Coverage: **>90%** on this task's new/modified code — `currentPreset()` and the control are
exercised from the browser in task 02's suite; a structure guard covers the option/key agreement.
## Completion Criteria
- [ ] `/mirror` shows the Quality select under "Capture", with `Sharp` selected and the three options above
- [ ] `uv run pytest tests/test_app.py` green, including the new three-preset agreement guard
- [ ] the select inherits the panel's field styling (same background/border/radius as `#name`), no new `display` rule
- [ ] pressing `Tab` into the select and using the arrow keys does not trigger the page's `s`/`x` shortcuts (the router's existing `HTMLSelectElement` guard)
- [ ] capture and encoder behaviour are byte-for-byte what they were before this task
- [ ] full test suite **100% passing** (every test in the project, not just this task's)
- [ ] README.md updated for anything user-visible this task changes; AGENTS.md touched only if a statement in it became stale (the phase's final pass re-checks both)
- [ ] no behavior change in completed work
@@ -0,0 +1,80 @@
# Task 02 — The capture request comes from the preset
**Phase:** `06_sharer_quality_presets` · **Story:** n/a
## Objective
`pickSurface()` asks the capture device for the *selected* preset's pixels and frame rate instead of
the hardcoded 4K/30, and the track's `contentHint` follows the preset. `sharp` must produce exactly
the request it produces today.
## Work
1. `src/mirror/static/share.js` — extract the constraint builder from `pickSurface()` into a pure
helper (task 04 needs the identical numbers for `applyConstraints`):
```js
/** The size/frame-rate request for a preset, plus the size we asked for (the panel's shortfall
* toast compares the delivered width against it). `ideal`, never `exact` — a hard size fails or
* hides picker entries on displays/DPRs the browser cannot honour (AGENTS.md §5). */
function presetCapture(preset) {
const dpr = window.devicePixelRatio || 1;
const [maxW, maxH] = preset.max;
const requested = {
width: Math.min(Math.round(screen.width * dpr), maxW),
height: Math.min(Math.round(screen.height * dpr), maxH),
};
return {
requested,
video: {
width: { ideal: requested.width },
height: { ideal: requested.height },
frameRate: { ...preset.frameRate },
},
};
}
```
2. `pickSurface()` — take the key **once** at the top
(`const key = currentPreset(); const preset = PRESETS[key];` and build `shared` from
`presetCapture(preset).video`) so the audio attempt and its silent retry keep sending *identical*
video constraints (the existing comment about one shared `video` object still holds — preserve it).
Return `key` alongside `requested` so `start()` records which preset the live capture belongs to.
3. `start()` — set `state.qualityKey = captured.key;` (initialise `qualityKey: DEFAULT_PRESET` in
`state`), and replace `video.contentHint = "detail";` with
`video.contentHint = PRESETS[state.qualityKey].contentHint;`, keeping the comment's reasoning and
adding one line: the hint is per-preset now — `Light` is glanceable content, so motion wins over
glyph crispness.
4. Leave the 75%-shortfall toast comparing `delivered.width` with `state.requested.width` — it is now
preset-relative, which is what makes it still true for `Light`.
5. `window.__probe` — add two read-only getters (this stays *the one global*, AGENTS.md §6):
`get lastRequested() { return state.requested; }` and
`get qualityKey() { return state.qualityKey; }`. Tests assert the request; delivered pixels are
asserted in task 06 on a real desktop.
6. `tests/e2e/test_quality_presets.py` — new suite, `pytestmark = [pytest.mark.e2e,
pytest.mark.timeout(180)]`, with the `device()` fixture copied from
`tests/e2e/test_sender_fidelity.py` (one copy per suite is the convention here) and the first
case: `test_each_preset_asks_the_capture_for_its_own_budget`. For each of `sharp|balanced|light`:
fresh page, `select_option("#quality", key)`, `start_sharing(page)`, then read
`__probe.lastRequested` and `__probe.qualityKey` and assert
`width == min(round(screen.width * devicePixelRatio), cap_w)` (read `screen.width` /
`devicePixelRatio` from the page — under Playwright `screen` is the emulated viewport, so
hardcoding a display size would make this environment-dependent).
**Do not assert delivered pixels in this suite**: with
`--use-fake-device-for-media-stream` the synthetic device is 1280×720 and its clamping is not the
behaviour under test (`PLAN.md` §5). Task 06 asserts delivered pixels where a real capturer runs.
## Testing & Quality
- Unit/integration: the new suite above; `test_share_never_asks_for_an_exact_capture_size` must stay
green (the builder uses `ideal` only); `tests/e2e/test_live_mirror.py` and
`tests/e2e/test_sender_fidelity.py` (default `sharp`) must pass untouched.
- Coverage: **>90%** on this task's new/modified code.
## Completion Criteria
- [ ] with `sharp` selected, the request is still `min(screen × dpr, 3840×2160)` at `frameRate {ideal: 30, max: 60}` (asserted, and `test_sender_fidelity.py` green unmodified)
- [ ] with `light` selected before sharing, `__probe.lastRequested` is capped at 960×600 and `__probe.qualityKey == "light"`
- [ ] the shared track's `contentHint` is `detail` for sharp/balanced and `motion` for light
- [ ] the audio attempt and its silent retry still pass byte-identical video constraints
- [ ] full test suite **100% passing** (every test in the project, not just this task's)
- [ ] README.md updated for anything user-visible this task changes; AGENTS.md touched only if a statement in it became stale (the phase's final pass re-checks both)
- [ ] no behavior change in completed work
@@ -0,0 +1,63 @@
# Task 03 — Per-leg bitrate and degradation come from the preset
**Phase:** `06_sharer_quality_presets` · **Story:** n/a
## Objective
Each leg encodes under the selected preset's share of `MIRROR_MAX_BITRATE` and the preset's
`degradationPreference`, at both places that set encoder parameters. `sharp` keeps the exact
25 Mbit/s / `maintain-resolution` behaviour phase 02 asserted.
## Work
1. `src/mirror/static/share.js` — add one helper next to `PRESETS` so the two call sites cannot
drift:
```js
/** The per-leg bitrate cap for the preset in force: the server's ceiling
* (`config.maxBitrateBps`, from MIRROR_MAX_BITRATE) scaled by the preset's factor. 0 stays 0 —
* "uncapped" is a real setting in the WebRTC API and must not become a 0 bps leg. */
function legCap() {
const cap = config.maxBitrateBps || 0;
return cap ? Math.round(cap * PRESETS[currentPreset()].bitrateFactor) : 0;
}
```
2. `applySenderQuality()` — replace the two hardcoded pieces:
- `params.degradationPreference = PRESETS[currentPreset()].degradation;`
- `if (legCap()) params.encodings[0].maxBitrate = legCap();`
Keep the truthy-cap rule (0 = uncapped), the first-encoding-only rule (the cap is per leg, not
per simulcast layer), the named `NotAllowedError` catch, and the re-apply call sites
(post-`setLocalDescription` and on `connected`) exactly as they are — that is what keeps the cap
across every renegotiation, and it must re-read the *current* preset, which is why this reads
`currentPreset()` at call time rather than a value captured at module load.
3. `addPeer()` — `const cap = legCap();` before the track loop, then
`if (track.kind === "video" && cap) init.sendEncodings = [{ maxBitrate: cap }];`, keeping the
comment that explains why `sendEncodings` is kept *in addition to* the post-negotiation re-apply
(it is the only thing capping the leg between `addTransceiver` and the first
`setLocalDescription`).
4. Do not add a renegotiation anywhere: neither parameter needs a new offer (A4 — the sharer offers
when the *track set* changes).
## Testing & Quality
- Unit/integration: extend `tests/e2e/test_quality_presets.py` with
`test_a_light_leg_encodes_at_the_presets_share` — sharer selects `light`, one viewer goes live,
then read the committed parameters back through `window.__probe.peers` (the `SENDER_PROBE` shape
from `tests/e2e/test_sender_fidelity.py`, copied per suite convention) and assert
`maxBitrate == round(cap × 0.35)` and `degradationPreference == "maintain-framerate"`, where `cap`
is read live from `/api/config` (never hardcoded). Assert the wall is still moving
(`frames_matching(...)["distinct"] >= 2`) — a cheaper picture must not be a dead one — and
`errors == []`.
- Regression: `tests/e2e/test_sender_fidelity.py` must pass **unmodified** — it asserts
`maxBitrate == /api/config.maxBitrateBps` and `maintain-resolution` on two legs with the default
preset, which is precisely this task's no-change guarantee.
- Coverage: **>90%** on this task's new/modified code.
## Completion Criteria
- [ ] `light` yields `maxBitrate == round(maxBitrateBps × 0.35)` and `maintain-framerate` on a live leg (asserted)
- [ ] `sharp` still yields the full `/api/config` cap and `maintain-resolution` (existing suite, unmodified)
- [ ] the cap and the degradation preference are read from the preset at call time, so a preset change made *before* a viewer joins lands on that viewer's leg on its first offer
- [ ] with `MIRROR_MAX_BITRATE=0`, `legCap()` returns 0 and no `maxBitrate` is written (uncapped stays uncapped)
- [ ] full test suite **100% passing** (every test in the project, not just this task's)
- [ ] README.md updated for anything user-visible this task changes; AGENTS.md touched only if a statement in it became stale (the phase's final pass re-checks both)
- [ ] no behavior change in completed work
@@ -0,0 +1,71 @@
# Task 04 — Changing quality mid-share, without dropping a viewer
**Phase:** `06_sharer_quality_presets` · **Story:** n/a
## Objective
A preset change takes effect on the share that is already running: every existing leg is
re-parameterised immediately, the capture follows where the engine honours `applyConstraints`, and
the panel says plainly when it cannot. No renegotiation, no new peer connection, no second picker.
## Work
1. `src/mirror/static/share.js` — add `applyQuality()` after `applySenderQuality()`:
- `const key = currentPreset(); const preset = PRESETS[key]; state.qualityKey = key;`
- **Legs first** (these commit without an offer, so the CPU/egress change is instant):
`for (const peer of state.peers.values()) await applySenderQuality(peer).catch(err => console.warn("sender quality", err));`
- No live video track → return after the legs (the choice is simply picked up by the next
`start()`).
- `track.contentHint = preset.contentHint;` then, only when
`typeof track.applyConstraints === "function"`:
`await track.applyConstraints(presetCapture(preset).video)`; on **`OverconstrainedError`**
retry with the frame rate alone — `await track.applyConstraints({ frameRate: preset.frameRate })`
— because a display that cannot rescale can still change rate. Swallow only what is named
(`AGENTS.md` §7): any other error means the capture keeps its current size.
- On a constraint success: `state.requested = presetCapture(preset).requested;` then `publish()`
so the roster's `width`/`height` — and every wall tile's caption — match the capture rather than
the last share. On failure: do **not** republish; the track did not change.
- Toast the outcome in one line, naming the preset, its budget and whether the capture followed,
e.g. `` `Balanced · ≤${maxW}×${maxH} @${fps}fps · ${bitrateFormat(legCap())} per viewer` `` and
`` `… · capture size applies next share` `` when `applyConstraints` failed. `bitrateFormat`
already renders `—` for a 0/uncapped cap, so say "uncapped" instead when `legCap()` is 0.
- Never call `negotiate()` here, and never touch `srcObject`: the track object is unchanged, the
transceiver keeps it (AGENTS.md §6 — move the element, not the stream; A4 — the sharer offers
when the *track set* changes, which this does not).
2. Wire it: `ui.quality.addEventListener("change", () => { applyQuality().catch(err => console.warn("quality", err)); });`
(the page is already all-`await` at module scope; keep the handler non-async so a rejected
constraint never becomes an unhandled rejection — the E2E suites fail on any `pageerror`).
3. `src/mirror/static/share.js` — the share page's key router: add a `q` branch after `x`, under the
router's existing rules (chords with Ctrl/Cmd/Alt belong to the browser; a key in `#name` or the
select is text). It cycles `sharp → balanced → light → sharp` over `Object.keys(PRESETS)`,
sets `ui.quality.value = next`, calls `await applyQuality()`, and `preventDefault()`s. When
`state.picking` is true it must **toast** (`a picker is already up`) rather than silently no-op —
a keyboard-only operator cannot debug silence, which is the rule already written above the router.
4. `README.md` — extend task 01's paragraph: changing Quality applies to the running share (bitrate
and frame rate always, capture size where the browser allows), and `q` cycles it.
## Testing & Quality
- Unit/integration: extend `tests/e2e/test_quality_presets.py` with
`test_changing_quality_mid_share_re_prices_the_live_leg`:
sharer starts on the default, one viewer goes live, then `select_option("#quality", "light")`.
Assert — through `__probe` — that the peer count is still **1** (no new `RTCPeerConnection`), that
the same leg's committed `maxBitrate` becomes `round(cap × 0.35)` within ~5 s, that
`connectionState` returns to / stays `connected`, and that the viewer's `<video>` still shows
`distinct >= 2` frames (the switch did not freeze anybody). Then assert the roster tells the truth:
`GET /api/streams` width/height equal the sharer's `track.getSettings()` — whatever the engine
settled on, the two must agree (do **not** assert a specific delivered size here; the synthetic
device clamps, and task 06 owns delivered pixels).
Plus `test_q_cycles_the_preset_and_says_so`: `keyboard.press("q")` advances `#stat`/`#quality`
value and the toast text names the preset; a `q` typed into `#name` never changes the select.
- Coverage: **>90%** on this task's new/modified code.
## Completion Criteria
- [ ] selecting `light` while sharing changes the live leg's committed `maxBitrate` (asserted) without creating a second `RTCPeerConnection` and without a renegotiation
- [ ] the viewer keeps decoding moving frames across the switch (`distinct >= 2`)
- [ ] `/api/streams` width/height agree with the sharer's `track.getSettings()` after the switch, in both the honoured and the refused-`applyConstraints` cases
- [ ] a toast always names the resulting preset and budget, and adds "capture size applies next share" when the engine refused to rescale
- [ ] `q` cycles the preset and toasts; keys in `#name` and in the select stay text; `s`/`x`/`Esc` behave as before
- [ ] no `pageerror` in any suite (`errors == []`)
- [ ] full test suite **100% passing** (every test in the project, not just this task's)
- [ ] README.md updated for anything user-visible this task changes; AGENTS.md touched only if a statement in it became stale (the phase's final pass re-checks both)
- [ ] no behavior change in completed work
@@ -0,0 +1,58 @@
# Task 05 — Show what the share is actually costing
**Phase:** `06_sharer_quality_presets` · **Story:** n/a
## Objective
Two new lines in the share panel — the quality budget in force, and the codec the legs actually
negotiated — so the next "why is my fan spinning" starts with a readout instead of a profiler.
## Work
1. `src/mirror/static/mirror.html` — in `<dl class="stats">`, add after `<dt>Resolution</dt>`:
```html
<dt>Quality</dt>
<dd id="stat-quality">—</dd>
<dt>Codec</dt>
<dd id="stat-codec">—</dd>
```
New ids on this page only (`stat-quality`, `stat-codec`) — the cross-page id test allows only
`net`/`notice`/`toast` to be shared.
2. `src/mirror/static/share.js` — add `qualityStat: $("#stat-quality")` and `codec: $("#stat-codec")`
to `ui`; set both to `"—"` in `renderIdle()`.
3. `src/mirror/static/share.js` — one `renderQuality()` that writes the preset line from state, and
call it from `start()`, from `applyQuality()` (task 04) and from `renderIdle()`:
`` `${key} · ≤${maxW}×${maxH} @${preset.frameRate.ideal}fps · ${legCap() ? bitrateFormat(legCap()) + " /viewer" : "uncapped /viewer"}` ``
Keep it one line; `dl.stats dd` already wraps with `overflow-wrap: anywhere`.
4. `sampleStats()` — inside the existing per-pc report loop (no new timer, no extra `getStats()`
call), when it sees the video `outbound-rtp` entry, also resolve its codec:
`const codecEntry = entry.codecId ? report.get(entry.codecId) : null;` and take
`entry.codec || codecEntry?.mimeType || codecEntry?.codec || ""`, then render the short form
(`"video/VP8"` → `VP8`). Write it to `#stat-codec` once per tick:
- a resolved name when any leg reported one,
- `"—"` when none did, with a comment stating why: Firefox does not expose the outbound codec in
`getStats()` for every leg, and the panel must not guess.
Do **not** present this as a hardware/software indicator — the encoder's implementation is not
exposed to the page; the codec name is the honest floor of what can be said.
5. `README.md` — one sentence in the `/mirror` section: the panel shows the quality budget in force
and the negotiated codec (`—` where a browser does not report it).
## Testing & Quality
- Unit/integration: `test_every_element_the_script_reaches_for_exists` must pass with the two new ids
(the elements exist in `mirror.html`); extend `tests/e2e/test_quality_presets.py` with
`test_the_panel_shows_the_budget_and_the_codec` — after a live leg with `#quality = balanced`,
`#stat-quality` text contains `balanced` and `1920`, and `#stat-codec` is either `—` or a short
codec name matching `^[A-Z0-9/]{2,16}$` (assert the *shape*, not a specific codec — the codec the
engine picks is not a contract); after Stop sharing, both return to `—`.
- Coverage: **>90%** on this task's new/modified code.
## Completion Criteria
- [ ] while live, `#stat-quality` reads like `balanced · ≤1920×1200 @24fps · 15.0 Mbps /viewer` (asserted)
- [ ] `#stat-codec` shows the negotiated codec name when the engine reports one and `—` when it does not; it never shows a guess
- [ ] both lines reset to `—` when the share stops (`renderIdle`)
- [ ] no additional `getStats()` call and no new interval were introduced
- [ ] full test suite **100% passing** (every test in the project, not just this task's)
- [ ] README.md updated for anything user-visible this task changes; AGENTS.md touched only if a statement in it became stale (the phase's final pass re-checks both)
- [ ] no behavior change in completed work
@@ -0,0 +1,81 @@
# Task 06 — The preset matrix in the browser, on a real desktop, and in the docs
**Phase:** `06_sharer_quality_presets` · **Story:** n/a
## Objective
Close the phase: the full preset matrix asserted in the browser, one `realdisplay` case proving
delivered **pixels** follow the preset, and the documentation pass that makes `README.md` and
`SECURITY.md` true again.
## Work
1. `tests/e2e/test_quality_presets.py` — finish the suite the earlier tasks started, so it covers
every claim in `00_phase.md`:
- all three presets request their own budget (task 02's case) — `sharp` first, so the
no-regression request is the first thing the file proves;
- per-preset committed sender parameters: `sharp` → full cap + `maintain-resolution`;
`balanced` → `round(cap × 0.6)` + `balanced`; `light` → `round(cap × 0.35)` +
`maintain-framerate`; each with the viewer's frames still moving (`distinct >= 2`);
- the mid-share switch and the `q` cycle (task 04's cases);
- the readout (task 05's case);
- **a second viewer that joins after a switch** inherits the new cap on its first offer (the
`sendEncodings` path in `addPeer`, not just the re-apply path);
- every case asserts `errors == []` (no `pageerror`, no `console.error`).
Read the cap from `GET /api/config` in the test; never hardcode 25 Mbit/s.
2. `tests/e2e/test_x11_capture.py` — add one `realdisplay`-marked case, reusing that file's existing
harness (`browser_env()`, `REAL_CAPTURE_ARGS`, `XVFB_GEOMETRY`, the private-Xvfb fixtures). It must
select `#quality = light` **before** clicking `#start` on its own share page, and assert on the
**viewer's** `<video>` (`PLAN.md` §5):
- decoded width `<= 990` and `>= 0.8 × 960` — i.e. the request reached the real capturer rather
than being clamped away (under Playwright `screen` is the emulated viewport, so `min(viewport,
960)` is the 960 request);
- the existing aspect guard (`abs(w/h − XVFB_ASPECT) < 0.06`) still holds — a smaller picture must
still be *our* desktop, not somebody else's window;
- `distinct >= 2`, with the same background-cycling that drives motion in
`test_sharp_picture_reaches_the_wall`;
- do **not** re-assert the `SHARP_FLOOR_PX` floor here — that test stays as the sharp-path guard,
and this file must keep distinguishing real capture from the fake device.
Add a one-line comment recording the measured numbers you saw on this host, the way the phase 02
test records its 1200×750.
3. `README.md` — the full pass, in full:
- a **Quality** table in the `/mirror` section: preset · requested pixel cap · frame rate ·
per-viewer bitrate as a fraction of `MIRROR_MAX_BITRATE` · `degradationPreference` (what the
encoder may sacrifice) — and the sentence that `MIRROR_MAX_BITRATE` remains the ceiling the
factor is applied to, so a Wi-Fi sharer picks `Light` in the page while an operator lowers the
ceiling for everyone;
- state that `Sharp` is the default and is what phase 02 shipped;
- document `q` in the share-page keyboard list;
- say who this is for: a software-encoding sharer (a Firefox/Linux sharer has no hardware
encoder on many machines — VA-API exposes no H.264 encode there) burning ~a core at 4K30;
- re-read the **whole** file against the code as it now stands and fix every inconsistency,
including pre-existing ones (the phase brief requires it).
4. `SECURITY.md` — the sentences that say `MIRROR_MAX_BITRATE` "bounds per-viewer upload" / is what
bounds a viewer's consumption are now only half true: the sharer's preset scales that bound down.
Make them accurate (a preset can only ever lower the cap, never raise it above the server's), and
confirm nothing else in the threat model shifted — this phase adds no new capability, so no new
risk.
5. `AGENTS.md` — stale statements only, never additions: check §10's quick reference and §3's table
for anything this phase made untrue (the bitrate row still reads "Per-leg `maxBitrate` cap", which
is still correct). Do not add a section about presets.
6. `PLAN.md` — **do not edit**. Its §6 line still says "per-leg `maxBitrate` cap (default 12 Mbit/s)"
while the code has defaulted to 25 Mbit/s since phase 02; report it to the user in the phase
summary as a one-line amendment for them to approve.
## Testing & Quality
- Unit/integration: this task adds the phase's browser proof (fake-device matrix + a real-desktop
delivered-pixels case). Run, in order:
`uv run pytest -m "not e2e"`, `uv run pytest -m e2e`, then
`MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay`.
- Coverage: **>90%** on the phase's new/modified code.
## Completion Criteria
- [ ] `tests/e2e/test_quality_presets.py` covers all three presets' requests, all three presets' committed sender parameters, the mid-share switch, the `q` cycle, the readout, and a late-joining viewer after a switch — all green, `errors == []`
- [ ] `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` proves a `light` share decodes on the viewer at or under 990 px wide with moving frames and the desktop's aspect ratio
- [ ] `uv run pytest` (unit + API + browser E2E) is 100% green, and `tests/e2e/test_sender_fidelity.py` is untouched
- [ ] README.md documents the Quality control (table), `q`, and the relationship to `MIRROR_MAX_BITRATE`; SECURITY.md's bitrate statements are accurate
- [ ] `uv run ruff check . && uv run ruff format --check . && uv run pyright` clean
- [ ] PLAN.md untouched; its stale "default 12 Mbit/s" figure reported to the user
- [ ] full test suite **100% passing** (every test in the project, not just this phase's), coverage >90%
- [ ] README.md is accurate and consistent with the whole project; AGENTS.md has no stale statements (verified in this final pass)
- [ ] no behavior change in completed phases
@@ -0,0 +1,8 @@
**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.
@@ -0,0 +1,36 @@
........................................................................ [ 24%]
...............................sssss.................................... [ 49%]
........................................................................ [ 74%]
........................................................................ [ 99%]
. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.8-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 185 12 94% 108, 113, 116-117, 182-183, 212-216, 245
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 59 0 100%
src/mirror/registry.py 191 3 98% 166, 180, 355
------------------------------------------------------
TOTAL 653 33 95%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:545: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
284 passed, 5 skipped, 1 warning in 93.64s (0:01:33)
All checks passed!
32 files already formatted
0 errors, 0 warnings, 0 informations
@@ -0,0 +1,13 @@
All gates green and the code is in place. Task complete.
**Report**
- `#quality` select added to `mirror.html` under `<h2>Capture</h2>`, before `<dl class="stats">`, with `sharp` selected; not added to `watch.html`/`index.html` (cross-page guard safe)
- `app.css`: field rule extended to `.panel input[type="text"], .panel select` — no `display` added, `dl.stats`/`.panel label`/`[hidden]` untouched
- `share.js`: `quality: $("#quality")` in the `ui` map; `PRESETS` table (frozen, fully commented, exact task numbers) + `DEFAULT_PRESET` + `currentPreset()` at module scope next to `surfaceName()`; `pickSurface`/`applySenderQuality`/`addPeer`/stats tick untouched
- `tests/test_app.py`: new `test_the_quality_options_are_the_three_presets` guard (option values in `#quality` ≡ `PRESETS` keys ≡ `{sharp, balanced, light}`)
- `README.md`: "Quality." paragraph in Platform notes
- Gates: `uv run ruff check . && uv run ruff format --check .` ✅ · `uv run pyright` → 0 errors ✅ · `uv run pytest -q` → 275 passed, 4 skipped (opt-in realdisplay) ✅ · `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay -q` → 3 passed, 1 skipped (pre-existing documented platform audio limitation) ✅
- Notable: the PRESETS `degradation` values use single quotes — the unmodified guard `test_sender_quality_is_reapplied_on_negotiation_and_connection` counts double-quoted `"maintain-resolution"` (asserts == 1), so the table must not add a second copy. **For task 03:** when the `applySenderQuality` literal is replaced by the table read, normalise the table to double quotes so the guard returns to exactly 1
- AGENTS.md: no stale statements, untouched. (Pre-existing `TODO.md` working-tree edit left alone.)
- Next pending task: `02_capture_constraints_from_the_preset.md`
@@ -0,0 +1,34 @@
........................................................................ [ 25%]
......................ssss.............................................. [ 51%]
........................................................................ [ 77%]
............................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.8-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 185 12 94% 108, 113, 116-117, 182-183, 212-216, 245
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 59 0 100%
src/mirror/registry.py 191 3 98% 166, 180, 355
------------------------------------------------------
TOTAL 653 33 95%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
275 passed, 4 skipped, 1 warning in 77.18s (0:01:17)
All checks passed!
31 files already formatted
0 errors, 0 warnings, 0 informations
@@ -0,0 +1,20 @@
All gates green, docs verified. Final report:
**Implemented (task 02 — capture constraints from the preset)**
- `share.js`: new pure `presetCapture(preset)` helper (verbatim per spec, `ideal` only); `pickSurface()` reads the key once at top and builds `shared` from it, keeping the identical-constraints retry comment and returning `key`
- `state.qualityKey` (initialised `DEFAULT_PRESET`), set from `captured.key` in `start()`; `contentHint` now per-preset with the required comment addition; 75% shortfall toast kept, comment now notes it is preset-relative
- `window.__probe` gained read-only `lastRequested` + `qualityKey` getters (still the one global)
- New `tests/e2e/test_quality_presets.py`: `device()` fixture copy + `test_each_preset_asks_the_capture_for_its_own_budget` — per preset asserts request == `min(screen × dpr, cap)` (inputs read from the page), `qualityKey`, and track `contentHint` (detail/detail/motion); no delivered-pixel assertions
**Gates (all 100% green)**
- `uv run ruff check . && uv run ruff format --check .` — passed
- `uv run pyright` — 0 errors
- `uv run pytest` — 276 passed, 4 skipped (realdisplay-marked)
- `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` — 3 passed, 1 skipped (documented platform constraint)
- `test_sender_fidelity.py` (canary) and `test_live_mirror.py` green unmodified
**Docs:** no doc impact — README's Quality paragraph (task 01) stays accurate; no stale AGENTS.md statements
**Decisions:** moved the `PRESETS` block above `const state` (required: `qualityKey: DEFAULT_PRESET` in the object literal would hit the TDZ otherwise); added the `contentHint` read-back to the suite since it is a pure track-property read, not delivered pixels
**Next pending task:** `03_sender_parameters_from_the_preset`
@@ -0,0 +1,34 @@
........................................................................ [ 25%]
.......................ssss............................................. [ 51%]
........................................................................ [ 77%]
................................................................ [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.8-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 185 12 94% 108, 113, 116-117, 182-183, 212-216, 245
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 59 0 100%
src/mirror/registry.py 191 3 98% 166, 180, 355
------------------------------------------------------
TOTAL 653 33 95%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
276 passed, 4 skipped, 1 warning in 77.58s (0:01:17)
All checks passed!
32 files already formatted
0 errors, 0 warnings, 0 informations
@@ -0,0 +1,20 @@
All done. Final report:
**Implemented**
- `legCap()` helper in `share.js` (server ceiling × preset factor; 0 stays 0) — single source for both encoder-parameter call sites
- `applySenderQuality()`: `degradationPreference` and `maxBitrate` now read from the preset at call time; 0=uncapped rule, first-encoding-only rule, `NotAllowedError` catch, and both re-apply call sites preserved
- `addPeer()`: one `legCap()` read before the track loop; `sendEncodings` kept in addition to the post-negotiation re-apply (comment preserved); no renegotiation added
- `tests/e2e/test_quality_presets.py`: `test_a_light_leg_encodes_at_the_presets_share` (asserts `round(cap × 0.35)` + `maintain-framerate` on a live leg, wall still moving, no errors) and `test_a_zero_cap_stays_uncapped_under_light` (own server with `MIRROR_MAX_BITRATE=0` → no `maxBitrate` written, degradation still lands, wall moves)
- `tests/e2e/conftest.py`: `_start()` gained `extra_env` override; `test_app.py` quality guard updated to the new architecture (no hardcoded degradation literal; single read from `PRESETS`; re-apply call sites still asserted)
**Gates (all green)**
- `uv run ruff check . && uv run ruff format --check .` — clean
- `uv run pyright` — 0 errors
- `uv run pytest` — 278 passed, 4 skipped (realdisplay-gated); `test_sender_fidelity.py` green **unmodified**
- `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` — 3 passed, 1 skipped (host offers no audio — pre-existing documented skip)
**Docs**: no doc impact — README's Quality paragraph and `MIRROR_MAX_BITRATE` row are already accurate (SECURITY.md wording is task 06's); no stale AGENTS.md statements.
**Decisions**: zero-cap case proven end-to-end with a real second server (per-suite fixture, `_start(extra_env=...)`) since the CLI has no bitrate flag; the `test_app.py` guard's "single named decision" is now the PRESETS table.
**Next pending task**: `04_apply_live_and_republish`
@@ -0,0 +1,34 @@
........................................................................ [ 25%]
.........................ssss........................................... [ 51%]
........................................................................ [ 76%]
.................................................................. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.8-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 185 11 94% 108, 113, 117, 182-183, 212-216, 245
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 59 0 100%
src/mirror/registry.py 191 3 98% 166, 180, 355
------------------------------------------------------
TOTAL 653 32 95%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
278 passed, 4 skipped, 1 warning in 80.94s (0:01:20)
All checks passed!
32 files already formatted
0 errors, 0 warnings, 0 informations
@@ -0,0 +1,12 @@
Everything is complete and green. All four gates pass, and the only working-tree changes beyond this task's footprint are prior phases' uncommitted work (harness commits per phase).
**Report — Task 06.04: apply quality live, mid-share, and republish**
- `applyQuality()` in `share.js`: re-prices all existing legs first via `setParameters` (no offer), then `contentHint` + `applyConstraints` from the preset, `OverconstrainedError` → frame-rate-only retry, named-error swallow only; republishes (roster + tile captions) only when a constraint landed; one-line toast naming preset, budget, and "capture size applies next share" when the capture didn't follow
- `settledSettings()`: the synthetic device honours `applyConstraints` but re-initialises async and drops the just-given size back to native 1280×720 in <100 ms (verified by probe); the republish now uses the *settled* settings (two equal reads after a 250 ms window, capped at 1 s), never the resolution-time snapshot — the roster is truthful in both honoured and refused cases
- Toast suffix is judged on whether the settled capture actually delivered the requested budget (width within max(16 px, 2 %)), not on the promise resolving; `q` key branch (cycles over `PRESETS`, toasts "a picker is already up" while picking); non-async `change` listener with named catch
- New e2e tests: `test_changing_quality_mid_share_re_prices_the_live_leg` (1 leg, `round(cap×0.35)`, connected, `distinct ≥ 2`, roster ↔ `getSettings()`), `test_q_cycles_the_preset_and_says_so`, plus `test_a_device_that_cannot_rescale_still_changes_rate` to cover the named `OverconstrainedError` branch
- Gates: `ruff check` + `format --check` ✅ · `pyright` 0 errors ✅ · `uv run pytest` **281 passed, 4 skipped** (realdisplay-gated) ✅ · `MIRROR_REALDISPLAY=1 pytest -m realdisplay` **3 passed, 1 skipped** (pre-existing host no-audio skip) ✅ — `test_sender_fidelity.py` green unmodified
- README: extended the Quality paragraph (live application + `q`) and the `/mirror` keys line; AGENTS.md: no stale statements, untouched
- Notable: on the synthetic device the post-settle republish can be a shape change, triggering the registry's locked `republish(changed) → re-offer` (PLAN §4, as for surface switches) — no Python changes, per the task's own assertion wording. **PLAN.md §6 "default 12 Mbit/s" is stale (25 Mbit/s since phase 02) — reported, PLAN.md untouched per phase instruction.**
- Next pending task: `05_quality_and_codec_readout.md`
@@ -0,0 +1,34 @@
........................................................................ [ 25%]
............................ssss........................................ [ 50%]
........................................................................ [ 75%]
..................................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.8-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 185 11 94% 108, 113, 117, 182-183, 212-216, 245
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 59 0 100%
src/mirror/registry.py 191 3 98% 166, 180, 355
------------------------------------------------------
TOTAL 653 32 95%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
281 passed, 4 skipped, 1 warning in 83.37s (0:01:23)
All checks passed!
32 files already formatted
0 errors, 0 warnings, 0 informations
@@ -0,0 +1,21 @@
**Task 05 complete — report**
**Implemented**
- `mirror.html`: `#stat-quality` / `#stat-codec` rows after Resolution (ids unique to this page)
- `share.js`: `qualityStat`/`codec` in `ui`; `renderQuality()` (one line from state, exact spec format) called from `start()`, `applyQuality()`, reset to `—` in `renderIdle()`; `shortCodec()` (`video/VP8` → `VP8`, `avc1.*` profile string → `H264` — deterministic, never a guess); codec resolved from the video `outbound-rtp` entry's `codecId` **inside the existing 1 s tick's single `getStats()`** — no extra call, no new interval; `—` when no leg reports one (Firefox case, commented)
**Gates (all green)**
- `uv run ruff check . && uv run ruff format --check .` — clean · `node --check share.js` — OK
- `uv run pyright` — 0 errors
- `uv run pytest -q` — **282 passed, 4 skipped** (realdisplay-gated only)
- `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay -q` — 3 passed, 1 skipped (pre-existing platform audio-skip)
- `tests/e2e/test_quality_presets.py` — 7/7 incl. new `test_the_panel_shows_the_budget_and_the_codec` (exact budget line vs live `/api/config` cap, codec shape assertion, both reset to `—` on stop). Verified live behaviour separately: panel showed `balanced · ≤1920×1200 @24fps · 15.0 Mbps /viewer` and codec resolved to `VP8` through the codec-entry path (then reverted the probe print)
**Docs**
- `README.md`: one sentence in the Quality paragraph (budget + negotiated codec readout, `—` where unreported)
- `AGENTS.md`: no stale statements — untouched
**Decisions**
- `avc1.*` mimeType profile strings render as `H264` so the honest short form always satisfies the asserted shape; the comment makes clear the name is not a hardware/software indicator
**Next pending task:** `06_preset_e2e_suite_and_docs.md`
@@ -0,0 +1,34 @@
........................................................................ [ 25%]
.............................ssss....................................... [ 50%]
........................................................................ [ 75%]
...................................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.8-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 185 12 94% 108, 113, 116-117, 182-183, 212-216, 245
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 59 0 100%
src/mirror/registry.py 191 3 98% 166, 180, 355
------------------------------------------------------
TOTAL 653 33 95%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
282 passed, 4 skipped, 1 warning in 87.17s (0:01:27)
All checks passed!
32 files already formatted
0 errors, 0 warnings, 0 informations
@@ -0,0 +1,18 @@
Everything is green and clean. Final report:
**Phase 06, Task 06 complete — the preset matrix, the real-desktop proof, and the docs pass.**
**Implemented**
- `tests/e2e/test_quality_presets.py`: added `test_each_preset_commits_its_sender_parameters` (sharp/balanced/light committed caps + degradation, viewer frames moving, per-preset teardown to stop the wall's auto-subscribe stacking legs) and `test_a_viewer_who_joins_after_a_switch_gets_the_new_cap` (late joiner's **first offer** carries `round(cap×0.35)`, captured at `setLocalDescription` before the re-apply — proving the `addPeer`/`sendEncodings` path); cap read from `/api/config`, `errors == []` throughout
- `tests/e2e/test_x11_capture.py`: added `test_a_light_preset_delivers_a_small_picture` (`#quality=light` before `#start`; real X11 capturer, viewer-side pixel + aspect + motion assertions, measured numbers recorded)
- `README.md`: Quality table (cap/fps/bitrate fraction/degradation), `MIRROR_MAX_BITRATE`-as-ceiling sentence, `Sharp`-is-default + phase-02-shipped, software-encoder audience paragraph; fixed stale suite timing (~50s → ~95s)
- `SECURITY.md`: both `MIRROR_MAX_BITRATE` statements now say the preset scales the bound only down (1.0×/0.6×/0.35×), never above the ceiling
**Gates** — `ruff check` + `format --check`: clean · `pyright`: 0 errors · `uv run pytest -m "not e2e"`: 181 passed · `uv run pytest -m e2e`: 103 passed, 5 skipped (realdisplay-gated) · `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay`: 4 passed, 1 skipped (audio — host offers no source, documented gate) · full `uv run pytest`: **284 passed, 0 failed** (~94s) · `test_sender_fidelity.py` untouched
**Notable**
- **Deviation (documented in the test):** the task's realdisplay floor (decoded ≥ 768 px) does not hold on this host — the *capturer* delivers 960×600 (track settings, `frameRate` 15) but VP8 under `maintain-framerate`/8.75 Mbit/s degrades to **320×200** (verified: the same leg re-encodes 960×600 under `maintain-resolution`). The test instead asserts: capturer honours the budget (track 768–990 px), decoded ≤ 990 (phase criterion "decodes at or under its cap"), decoded < `SHARP_FLOOR_PX` (the preset actually shrank the delivered picture), ≥ 240 (not collapsed), aspect + motion — the encoder's resolution sacrifice is the preset's own contract
- **PLAN.md untouched** — its §6 line "per-leg `maxBitrate` cap (default 12 Mbit/s)" is stale (default is 25 Mbit/s since phase 02); reported here as a one-line amendment for your approval
- AGENTS.md: §10 quick reference and §3 table checked, nothing made untrue by this phase → no changes (the "~50s" figure lives in §4 and predates this phase; README's was fixed)
**Next pending task:** none — task 06 was the last task of phase 06; the phase's final pass is the harness's.
@@ -0,0 +1,36 @@
........................................................................ [ 24%]
...............................sssss.................................... [ 49%]
........................................................................ [ 74%]
........................................................................ [ 99%]
. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.8-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 185 12 94% 108, 113, 116-117, 182-183, 212-216, 245
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 59 0 100%
src/mirror/registry.py 191 3 98% 166, 180, 355
------------------------------------------------------
TOTAL 653 33 95%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:545: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
284 passed, 5 skipped, 1 warning in 93.26s (0:01:33)
All checks passed!
32 files already formatted
0 errors, 0 warnings, 0 informations