RnD sync#883
Open
testradav wants to merge 2270 commits into
Open
Conversation
…kpressure fix(codecs): release-side WebCodecs decode-queue backpressure (#1024)
- Action bar: set BOTH left+right longhands in every dock arm (and a non-empty 'left:auto; right:auto' on mobile) so dioxus-web 0.7.3's omit-and-restore inline-style behavior can't resurrect a stale opposite anchor on a dock switch or desktop->mobile transition (controls bar was stretching full-width / staying offset). Pure bottom-dock pin/unpin was unaffected; this fixes the dock-switch + mobile paths. - Resize grip: rest color var(--border) -> var(--border-emphasis) (~1.2:1 was invisible until hover; now perceptible at rest). Hover/focus/active var(--accent) unchanged. - a11y: add aria_label "Resize panel" to both resize separators. - e2e: add a revert-sensitive test for the pinned Bottom->Right dock-switch stale-anchor resurrection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ard (#1295) Joining with the camera ON showed a persistent dark square (no video feed) until a manual camera OFF→ON. Root cause: the acquire-phase and per-frame supersede guards added in #1295 read `switching`. On initial join a post-permission `devicechange` raises `switching` (via select() while enabled) on the very loop that should bind the capture stream to <video id=webcam>; that loop then bailed BEFORE set_src_object, and with device churn re-raising `switching` no loop ever bound — a persistent dark square (a manual toggle's stop()+start() clears `switching` and rebinds). Fix: extract `loop_is_superseded(enabled, loop_epoch, my_epoch)` — epoch + enabled ONLY, no `switching` — and use it in both supersede guards. The epoch is the sole supersede authority: a real switch tears the old loop down and bumps the epoch (caught by loop_epoch != my_epoch), while the newest loop IS the switch's response and must own it, not self-abort on the request flag — so it binds. `start()` now lowers `switching` at the epoch commit (the request is satisfied by the new generation); the now-dead task-exit `switching` resets are removed. The wrong-device protection (#1295's purpose) is unchanged — a stale loop still bails on epoch mismatch before binding, so it can never bind the wrong device. Test: native unit test `loop_is_not_superseded_when_only_switching_is_raised` pins the predicate (enabled + matching epoch must NOT be superseded); mutation-verified — re-adding a `switching` term forces a signature change that breaks the test. (A JS-only e2e cannot deterministically separate fixed-vs-buggy here because the restart darkens both sides; the kept e2e convergence test's comments are re-pointed at this Rust pin.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oin-bind # Conflicts: # videocall-client/src/encode/camera_encoder.rs
…#1283)
The #1170 demand gauge (relay_layer_preference_sessions) was unit-tested at the
pure-classifier level, but the actor-driven sweep loop (sweep_layer_preference_gauge:
room iteration, the per-session has_any() skip, and the explicit-zero SET) had no
test that seeds state and asserts the resulting gauge values.
Add an integration test (NATS-backed, like the other chat_server tests) that seeds
room_members + session_layer_prefs for two rooms — one active with a known
distribution across all 4 buckets x {video,screen}, one with a member but no
recorded prefs (has_any() false) — drives sweep_layer_preference_gauge directly
(it is &self, no actor context), and asserts the per-cell gauge.
Mutation-proofing: a Prometheus gauge reads 0 whether SET to 0 or never touched, so
the test PRE-POISONS every asserted cell to 99 before the sweep; each assertion then
proves the sweep actually SET the cell. Verified the test fails if the has_any() skip
is inverted (active counts collapse to 0) or the explicit-zero SET is dropped (the
poisoned 99 survives where 0 is expected).
Closes #1283.
fix(ui): drawer follow-ups — action-bar reflow, both-open, robust resize + visible grip (#1296)
…-map-bound fix(relay): bound KeyframeRequestLimiter map, gate new buckets on global cap (#1303)
fix(client): bind camera on join — drop switching from supersede guard (#1295)
recompute_layer_hints_for_source scanned all three media kinds (VIDEO=1,
AUDIO=2, SCREEN=3) via LAYER_HINT_MEDIA_KINDS, but AUDIO has no simulcast
ladder and the publisher's LAYER_HINT dispatch arm ignores AUDIO/UNSPECIFIED
entries (video_call_client.rs, the _ => {} fail-open arm). So the AUDIO union
scan and any emitted AUDIO entry are pure wasted work on the relay.
Drop AUDIO from LAYER_HINT_MEDIA_KINDS ([1,2,3] -> [1,3]) so the relay
computes/emits hints only for VIDEO + SCREEN. Verified safe on both ends:
the const is used only in the recompute scan loop, and the client already
ignores AUDIO hint entries.
Test pins the scan list to {VIDEO, SCREEN} (re-adding AUDIO fails it) and ties
the scanned kinds to laddered wire kinds. Full chat_server layer-hint/recompute
suite (incl. #1203 coalescing) green against NATS.
Note: #1118 N1 (cancel orphaned suppress-lazy re-check timers) is NOT in this
commit — see the issue; the per-source timer vs per-kind window interaction
needs careful per-deadline handling, deferred to avoid a subtle regression.
Addresses #1118 (N3).
Code review note: the layer_hint_media_kind doc still said LAYER_HINT_MEDIA_KINDS "only contains 1/2/3" — stale after N3 dropped AUDIO. Correct it to "1 and 3" and note the AUDIO(2) match arm is retained for completeness. Doc-only.
…8 N1) The suppress-lazy re-check armed by `recompute_layer_hints_for_source` discarded its `notify_later` `SpawnHandle`, so rapid drop→restore→drop churn at the ~200 ms preference cadence accumulated orphaned timers (bounded ~10 per `(source, kind)`, self-draining after the 2000 ms window, and safe — a fired re-check hits the `!is_member` / `SkipClearPending` no-op — but wasteful). Arm one timer per `(room, source, kind)` and track its handle in a new `layer_hint_recheck_handles` map, cancelling it the instant the downgrade is resolved: on `SkipClearPending` (demand restored), on `Emit` (matured or superseded by an eager restore), in `forget_layer_hint_state_for_source` (publisher left), and wholesale in `Actor::stopping`. Mirrors the #1203 / pending-departure `cancel_future` pattern. Mutation-proof test drives drop→restore churn through the real actor and asserts both (1) the tracked timer count arms-to-1 / cancels-to-0 each cycle and (2) no orphaned actix timer fires after the full window — the latter catches a `remove` that forgets `cancel_future` (dropping a `SpawnHandle` does NOT cancel an actix timer).
- Correct the test's mutation-A prediction: neutering the SkipClearPending cancel trips proof (1) at cycle 0's `after_drop` (reads 3, not 1) — the un-cancelled timers, including the solo-join (union=0) ones the restore normally reaps, accumulate. (Prior text said `after_restore`.) - Document why the never-activated-session expiry branch deliberately does NOT reap either LAYER_HINT map: both are keyed by source publisher, and an RTT-election loser never published, so it holds no entry to reap and the per-(source,kind) recheck invariant is not stranded. No logic change.
…intln-to-log perf(codecs): convert jitter_buffer.rs println! tracing to log:: (#1023)
test(relay): cover the layer-preference gauge sweep loop end-to-end (#1283)
…3-followups perf(relay): skip AUDIO layer-hint union scan (#1118 N3)
Per request: drop the drawer pin/overlay-toggle entirely; drawers are now overlay-only but still resizable and still both-openable. Removed (drawer pin only): - pin toggle buttons on both drawers + their pinned/on_toggle_pin props (peer_list.rs, diagnostics.rs) and the .pin-button / .pinned CSS. - left_pinned/right_pinned signals + the vc_drawer_*_pinned localStorage load/save. - the inset/reflow math (left_inset/right_inset, 60%-cap, *_render_w), grid-container inline left/right insets (grid + screen-share now full-bleed left:0;right:0), and the action-bar (controls nav) inline carve-out — the tile grid + controls return to the original full-width/overlay layout. - the now-dead `mobile` local that only fed the pin paths. Kept: - drag-resize: .drawer-resize-handle + visible ::before grip, aria-label "Resize panel", rAF-throttled pointer handlers, robust drag-end on pointerup/pointercancel/lostpointercapture; width applied to the overlay drawer; vc_drawer_left_width/vc_drawer_right_width persistence (load_f64 clamp + save_f64 on drag-end), DRAWER_MIN_WIDTH/max_for_side clamp. - both drawers open simultaneously (independent open state, mobile z-index stacking). NOT touched: the separate peer-TILE pin (pinned_peer_id / on_toggle_pin on canvas tiles / grid-item-pinned) is a different feature and is left intact (canvas_generator.rs zero diff). e2e: renamed drawer-pin-resize.spec.ts -> drawer-resize.spec.ts; dropped the pin tests (toggle, reflow, action-bar carve-out, both-pinned, dock-switch stale-anchor, mobile-ignores-pin); kept resize-clamp, width-persist-across- reload, no-hover-latch, lost-capture end, visible grip, mobile-handle-hidden, both-open; added a "left drawer overlays, no grid reflow" guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(ui): remove drawer pin capability, keep drag-resize (#1296)
…r-hint fix(relay): drop client-sent LAYER_HINT at ingest, never reflect to room (#1119)
Sync HCL PR-staging into the public RnD branch. Brings the latest HCL work (pre-join auto-show devices, transport-pin override, lobby device passthrough + camera-bind fix, drawer resize, relay/decode hardening, etc.). OSS `.github/workflows` restored from upstream/main (HCL-specific workflows stripped). HCL-only blocked paths verified absent. upstream/RnD (security-union#863) incorporated. Build-verified: meeting-api + wasm videocall-client + wasm videocall-ui compile; fmt + clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed-recheck-timers fix(relay): cancel orphaned layer-hint suppress re-check timers (#1118 N1)
A relay layer-union-cap shed is demand-driven (every receiver asked for fewer layers), NOT a flap, so it is deliberately excluded from the anti-flap penalty box (#1141). But it was also SILENT — field logs could not tell a union-driven shed from a backpressure/congestion one, so an incident like the observed 11-shed/11-restore churn could not be attributed to a shed path. Per the #1294 decision (observability-first; do NOT damp the union path — it is correct demand-driven behavior, and the churn, if real, originates upstream in the relay union aggregation [#1288/#1202], not the sender AQ): emit an `AQ_LAYER_SHED: cause=union_cap` log line plus a monotonic `union_cap_shed_total` counter on the union-cap shed branch. Observability ONLY — no control decision changes; the penalty-box exclusion is untouched. Mutation-proof test asserts (1) a union-cap shed increments the counter and (2) a backpressure shed does NOT — the two halves independently guard against counting on the wrong branch and counting on every shed.
Per the #1201 decision: keep audio rungs always-published (no runtime shed), do NOT act on relay AUDIO hints. - Fix the stale "AUDIO has no simulcast ladder" comments in the LAYER_HINT dispatch arm — audio HAS had a 3-rung ladder since #1086; the accurate reason AUDIO hints are ignored is that the publisher does not act on them and the relay computes no AUDIO union (#1118 N3). - Document on `audio_layer_is_published` that the user SEND ceiling is the ONLY runtime audio gate, so the full 24/32/50 kbps ladder (~106 kbps) is published unconditionally as an accepted cost. A publisher-side shed was rejected because today's only signal is the receiver's VIDEO-downlink proxy; shedding audio on a video-congestion proxy would risk degrading the highest-priority/lowest-cost stream while audio is fine. Revisit only if a real audio-downlink-health signal lands (#1290). Comment/doc only — no behavior change.
The relay-side 'computes no AUDIO union' is only true once #1118 N3 (PR #1330) merges; on current PR-staging the relay still computes it. Reword so the claims are accurate regardless of N3 merge order: the client-side truth (publisher never acts on AUDIO hints; audio always published) holds now, and the relay change is cited as the coordinated #1330 follow-up rather than asserted as already-live.
…ility log Review fix: active is capped to `union_requested_layer_cap.min(user_layer_ceiling_cap)`, so a top-side cap shed can be driven by the relay union OR the user "layers published" ceiling. The first cut hardcoded `cause=union_cap` and incremented `union_cap_shed_total` for either — so a user lowering their perf-panel thumb (union fail-open) emitted the self-contradictory `cause=union_cap ... cap=uncapped` and corrupted the very signal #1294 adds. Attribute the shed to whichever cap is the lower count (the one that determined `cap` via `.min()`): `cause=union_cap` (+ counter) when the union binds, `cause=user_cap` (no counter) when the user ceiling binds. New test `test_user_ceiling_shed_is_not_counted_as_union_cap_1294` guards it; verified to fail if every cap shed is attributed to the union counter. Still observability only — no control change.
Metric-only (no governor) signal for how far behind live a receiver's decoded video is — making the #1252 audio-ahead-of-video lag observable. The 5-6s lag read 94-97 on the quality score during the stall, and #1024's release-side gate may have RELOCATED the backlog from stage-2 into stage-1 rather than removing it; neither was visible server-side. Estimator (worker-computed, read-only, spans both receive stages): playout_latency_ms = stage1_span_ms (newest - next-to-release buffered arrival-time span, the dominant term) + decode_queue_depth() * source_frame_interval_ms (stage-2, bounded by #1024's HWM). source_frame_interval_ms tracks SOURCE cadence via an EWMA over released-frame inter-arrival deltas, not the decoder drain rate. - proto: VideoStats.playout_latency_ms (5) + playout_stage1_span_ms (6) - jitter_buffer: read-only buffered_span_ms / source_frame_interval_ms / playout_latency_ms + release-cadence EWMA; 3 unit tests - worker/messages/decoder: carry both metrics across the worker->main bus - health_reporter: fold into the health packet only when fps_received > 0 (a paused/hidden tile's stale frame is not latency) - metrics(server): two per-peer GaugeVec mirroring video_seq_loss_per_sec, set unconditionally (recover to 0) + per-peer label cleanup stage1_span is emitted separately so a dashboard can tell whether #1024 removed the backlog (total drops with stage1) or relocated it (total stays high while stage1 drops).
Adds a per-peer observability metric `videocall_video_playout_paint_lag_ms`:
the decoded-but-unpainted backlog sitting in the worker->main postMessage
queue + main-thread paint task queue — a region `decode_queue_size()`
(stage 2) structurally cannot see. Complements the #1337 playout-latency
metric and is the signal that disambiguates the open #1252 question: is the
5-6s lag in stage-1 (the jitter-buffer no-keyframe hole, which #1337 already
surfaces) or downstream of the jitter buffer in the paint path (which #1337
is blind to)? GPU-15%-while-lagging fits the latter.
Estimator (worker-computed, read-only):
playout_paint_lag_ms = (frames_emitted - frames_painted)
* source_frame_interval_ms
frames_emitted is incremented in the WebCodecs `on_output` closure; the main
thread ACKs its cumulative frames_painted (every drained VideoFrame, painted
or hidden-dropped) back via WorkerMessage::PaintProgress, throttled to <=2/s.
Computed in the worker because the worker's 1Hz stats message rides the SAME
postMessage FIFO as the frames — comparing emitted-vs-painted at stats-receipt
on the main thread would be delayed by the very backlog it measures and read ~0.
The emitted/painted counters are deliberately lifetime-monotonic on BOTH sides
and are NOT reset on an in-place jitter-buffer reset: the main-thread painted
counter survives the worker's internal decoder reset, so zeroing only the
worker side would desync it from the next monotonic ACK and floor the metric
to a false 0 for minutes. Frames dropped by destroy_decoder() were never
emitted; already-posted frames still drain and count — so emitted-painted stays
the true in-flight backlog across resets without any reset.
- proto: VideoStats.playout_paint_lag_ms (7)
- jitter_buffer: pure `paint_lag_ms` helper + 2 mutation-resistant unit tests
- worker/messages/decoder: FRAMES_EMITTED/PAINTED counters + PaintProgress ACK
- health_reporter: fold into the health packet only when fps_received > 0
- metrics(server): one per-peer GaugeVec on the same 6-label set as #1337
(no new Prometheus dimension), set unconditionally + per-peer cleanup
The keyframe-less freshness-eviction path (#1025) fired a proactive keyframe request at a fixed ~1/s for the entire duration of a stall. Field data from two consecutive infra meetings showed this 1:1 (freshness_skip == proactive_kf, e.g. 79/79) as the receiver-side amplifier of the PLI keyframe-storm freeze loop: every receiver pokes the active speaker once per second across a meeting-long sequence of freezes, and the relay's delivery-unaware KEYFRAME_REQUEST limiter (#1297 residual) then delays the very recovery keyframe those pokes ask for. Damp the loop: the first request still fires at the relay window (1000ms base), but each subsequent request within the backoff window doubles the interval (capped at 8s via PROACTIVE_KEYFRAME_REQUEST_MAX_BACKOFF_EXP=3). Crucially the backoff exponent is keyed to *request cadence*, not to frame release. The #1479 field shape is many short (1-5s) freezes interleaved with smooth playout, NOT one long stall — so resetting on every frame release would zero the exponent during each smooth gap and the backoff would never accumulate across the storm (a premise the pre-submit backend reviewer correctly flagged on the first cut). The exponent therefore persists across the storm's closely-spaced re-freezes and only resets after a genuinely quiet interval (PROACTIVE_KEYFRAME_REQUEST_BACKOFF_RESET_MS=12s, comfortably above the 8s cap) with no proactive request — a true sustained recovery. Going quiet is safe: the independent reactive gap-driven request path (peer_decode_manager::should_request_keyframe, 1s base + its own backoff + uncapped slow-retry) remains the binding lower bound on freeze recovery; this proactive path is only an accelerator on top of it. Introduces zero new keyframe-request sources, preserving the #1287/#1297/security-union#814 keyframe-storm-avoidance invariant. Adds proactive_keyframe_request_backs_off_and_resets_after_quiet_interval (mutation-checked: killing the backoff or the quiet-interval reset both fail it) and updates the existing throttle test to assert the grown interval.
…--tests The flush precondition assert used packet_buffer.len() > 0, which trips clippy::len_zero. The failing CI job runs a target combination the local checks missed: cargo clippy -p neteq --no-default-features --features web --tests -- -D warnings. Verified the fix against that exact command.
CI's cargo clippy job runs five commands (.github/workflows/ pr-check-rust-hcl.yaml), not one: --all plus per-crate --tests runs with crate-specific feature flags (neteq --no-default-features --features web --tests). A local 'cargo clippy' / 'cargo clippy --all' lints only library/binary targets, so deny-by-default lints inside #[test] code and behind feature flags slip through locally and fail in CI on an already-pushed PR (PR #1491 shipped a clippy::len_zero in a neteq --features web test for exactly this reason). Add a 'make clippy-ci' target that mirrors those five commands byte-for-byte, and point the CLAUDE.md linter rules at it. Keep the target in sync if the workflow changes.
…fix banner count in SS mode PR #1467 follow-up. The screen-share panel inlined a duplicate of the user-requested decode-promotion loop; route it through the pure promote_requested_into_decoded helper (already used by the normal grid) so the two can't drift. SS renders all tiles (vertical scroll, no +N badge), so it passes displayed_tile_count = ss_all.len() — every off-budget tile is renderable, so the helper's true-overflow bound (#1470) never excludes an SS peer, preserving the prior inline behaviour. The pinned_slot resolution keeps the ss_budget < ss_all.len() bound (mirroring the helper's own early-return) so the get_peer_user_id scan is skipped when the budget already covers every tile. Also fix DecodeBudgetBanner avatar_count during screen share: it reflected the normal-grid avatar_tiles.len() even when the SS panel was the active layout. Derive it from ss_avatar_tiles.len() when has_screen_share so the 'N videos paused' count matches what the user sees. Cosmetic/pre-existing (the old avatar_tile_count had the same property), no live bug. Adds promote_with_displayed_eq_len_admits_all_offbudget_requests, a host test pinning the displayed_tile_count == all_tiles.len() no-op-take equivalence the SS path relies on (mutation-checked: tightening the .take() bound fails it).
videocall-ui runs native libtest, so its #[test] code was invisible to
'cargo clippy --all' (which lints only lib/bin targets) — the same gap
that let PR #1491 ship a clippy::len_zero in a neteq --features web test.
A pre-existing clippy::assertions_on_constants was sitting unlinted in
decode_budget.rs as a result.
- Add 'clippy (videocall-ui test targets)' step to pr-check-rust-hcl.yaml
- Add the matching line to the Makefile clippy-ci target (kept in sync)
- Fix the surfaced lint: assert! over consts -> const { assert!(..) }
- CLAUDE.md: when adding a crate with test code, add a --tests clippy
step to BOTH the workflow and clippy-ci.
…li-backoff fix(#1479): exponential backoff on proactive-PLI keyframe requests
fix(#1402): wire WorkerMsg::Flush to actually flush the NetEq buffer (no-op flush left hiss after peer audio-off)
…target ci: add make clippy-ci + lint videocall-ui test targets (#1498)
…udget-dry fix(#1472): share decode-budget promotion helper with screen-share + fix SS banner count
…ess-log-instrumentation feat(#1486 + #1384): log relay downlink-relief epoch staleness + host-testable freshness_skip formatter
enable protocol badge
feat(#1483): server-gated WT/WS transport badge on peer tiles
id vs name shown for WS vs WT
# Conflicts: # .github/workflows/ci-host-janitor-hcl.yaml # .github/workflows/daily-deploy-ascend.yaml # .github/workflows/daily-deploy-hcl.yaml # .github/workflows/pr-check-backend-hcl.yaml # .github/workflows/pr-check-rust-hcl.yaml # docs/hcl-daily-operations.md
Squashed public sync of the latest PR-staging R&D batch (decode-budget tile fixes, simulcast/diagnostics observability, audio/NetEq + transport resilience, perf, prometheus-ingress TLS). The HCL-internal Smatter/JMAP in-meeting chat integration is EXCLUDED from the public fork (depends on an internal backend); its tree is fully removed AND its commit history is intentionally not carried onto this branch (squashed to avoid exposing internal docs/secrets that lived in that branch's history). Verified: cargo check green (meeting-api, wasm videocall-client, wasm videocall-ui). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Syncs HCL PR-staging fixes into the public RnD branch. HCL-internal Smatter/JMAP chat integration, HCL workflows, and blocked paths are excluded; internal commit history is collapsed to this single commit.
…aging-derived tree security-union#864 was an earlier HCL->public RnD sync. This branch (testradav/RnD) already carries the newer, equivalent content for every file: all 14 conflicting files are byte-identical to labs/PR-staging here (attendants.rs == PR-staging minus the intended Smatter-chat strip), while security-union#864's versions are the older snapshot. No file exists in security-union#864 that is absent here. Resolved with -s ours so the public tree stays exactly the vetted, HCL-stripped current state; security-union#864 is recorded as merged to clear the PR conflict.
…nion#866 conflicts (newer HCL code wins; upstream owns config/helm/OSS-workflow)
…conciliation; trees already identical, no content change
👋 Thanks for your contribution!A maintainer will review your changes shortly. ✅ Automated ChecksCI/CD tests will run automatically. Please ensure all checks pass before requesting review. 🚀 Preview DeploymentsMaintainers can test this PR in a live environment using these commands:
Preview URLs will be posted automatically once deployed. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
July 10
Realtime-first video playout and honest painted-fps tile metrics
Realtime-first frame presentation
requestAnimationFramepresents only the newest, so a burst becomes one jump-to-live paint. Steady state (≤1 frame pending per display refresh) is untouched — the mechanism self-gates with no thresholds — and the delta reference chain is preserved (every frame still decodes in order; coalescing is strictly presentational). Camera and screen share intentionally share the path; painted/backpressure accounting is unaffected. Ships mutation-sensitive unit tests on the production mailbox type.Per-tile media-metrics overlay: measured-at-the-paint fps + reliable audio kbps
Meeting-roster hardening
July 9
Keyboard-accessible action-bar customization, lighter simulcast ladders with a per-tile metrics overlay, and notification-preference splits
Accessibility — the action-bar customize mode is now fully keyboard-usable
Lighter simulcast ladders + real-time-first frame policy
New: per-tile media-metrics overlay
Notification preferences
UI fixes
Tests