- Two labelled sections, speaker above microphone, with radio selection in both,
the pattern the settings list already uses.
- A device that cannot be chosen is shown disabled rather than hidden, so the
menu keeps one shape across browsers; where a platform lists no output at all,
a disabled Default stands in that section instead of leaving a bare heading.
- The list is bounded by the measured call area rather than the window, so it
stays usable where Element Call is a component in a corner of a host's page,
and follows that area when the host resizes it.
- It scrolls with its section headings pinned and the meter held at its foot,
and keeps both clear of whichever row the keyboard has just reached.
- The focus ring is shown only when the keyboard moved the focus, tracked on the
menu rather than the document, so two calls on one page cannot answer for each
other.
- While a requested device has not arrived, nothing in either section can be
picked, so a second request cannot overtake the first. That holds only while
the device is still offered and only within the menu it was asked in: one
removed before the switch lands never arrives, and waiting for it would leave
every device unselectable for the rest of the call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Draws the level as lit bars, and each failure state as its own message with a
next action rather than as a meter that has stopped.
- A bar and the space beside it keep one size wherever the meter is drawn; what
the width decides is how many bars there are. Bars spread to fill would be a
different shape in every place, and would close into one block where the space
ran short.
- Announces the level on a fixed scale, so it does not rest on hue or on how
many bars happened to fit.
- The hook is a bridge and nothing more: the capture belongs to the state layer.
- The capture is held by a component of its own, so a level that does change
redraws the meter rather than whatever is rendered beside it. A level arrives
many times a second; held by the menu it would reconcile every device row on
the way to the bars. A test counts those renders, because nothing else would
notice the hook moving back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Opens a short-lived capture of a named device and reports a smoothed level.
- Owns the capture, the audio context and the frame loop, and releases all three
idempotently, including when cleanup lands before getUserMedia has resolved.
- Reports denied permission and a missing device as states of their own, so a
caller never has to read silence as an error.
- Gives the microphone back when the audio graph fails to build. That can fail
after getUserMedia has resolved, and the capture would otherwise outlive its
own failure: the device open and its in-use light on, behind a meter saying
it is unavailable.
- Shares the capture stub through the test utilities, silence and a signal
alike, so the meter's tests and its stories drive the same microphone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
As of the removal of 'Legacy' mode, there can no longer be a difference between the transport you advertise in your membership and the transport you publish media on, so the local transport code can be simplified considerably. For instance, the function which gets the local transport can simply return a promise rather than being reactive.
I kept the local transport as an Observable in other modules so that they could easily be tested with existing marble tests.
LiveKit's livekit-ice logger records each local ICE candidate, ICE
candidate errors and ICE connection state changes, but only at debug,
which we cap to info unless extended LiveKit logs are switched on. That
means a 'could not establish pc connection' rageshake carries no way to
tell 'no relay candidates at all' from 'relay candidates that never
paired'. Keep that one logger at debug regardless of the toggle.
* Add AGENTS.md documenting the repo's conventions
Codifies what is currently tribal knowledge or only discoverable by reading CI
config: the view model / view contract, the rule that nothing reads the page,
Clean Code ordering, the three test layers, and reuse-before-you-build.
The root AGENTS.md is the always-loaded minimum and routes to docs/agents/, so
an agent loads only the detail its task needs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Allow either hand at the commit, and measure continuous redraws
Committing from the agent rules out running it where there is no git
identity, a sandbox included. Either is now fine: commit on the user's
word where the setup allows it, or write the message and hand it over.
Redrawing continuously is the other addition. A level meter reported a
fresh value every animation frame, so its whole menu reconciled sixty
times a second, silence included, and nobody noticed until review. A PR
that adds anything of the kind now says what it costs, or better, counts
the redraws — commits rather than render calls, since React runs a
component it then discards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The "has no matching matrix call member" warning fired on every re-render
(i.e. every active speaker update) and also for the local participant's own
track, which is never rendered here anyway. Skip local tracks and warn once
per unexpected identity.
On browsers without a "default" pseudo-device (Firefox, Safari), the
virtual default output entry was appended after the physical devices, so
with no saved preference EC selected the first physical device and pinned
every remote audio element to it with setSinkId. Pinned sinks are not
re-routed by the browser: on Firefox/Linux a Bluetooth headset switching
from A2DP to HFP when its microphone is opened (i.e. on unmute) destroys
the pinned sink and all remote audio goes silent, with no error and no
fallback (rageshake 17320).
List the virtual default first so it is the fallback both when nothing was
chosen and when the chosen output disappears, and stop labelling it with
the first device's name since the browser default is not necessarily that
device.
IMO this is where the delegation calls should have lived all along, since the leave event is part of the membership lifecycle, and we otherwise end up with an awkward hack to ignore transport updates.
Doing this now ensures that the client won't send any delegation requests if delegation is unsupported, and prepares the code for a future change in which we use the dedicated delegation endpoint from the CS API.