- The footer view model now carries the output devices, the current output and
the callback that changes it, alongside the ones it already carried for input.
- That callback is withheld where the platform cannot route audio to a chosen
device, and its absence is what renders the section disabled — an absent
action rather than a flag the view has to interpret.
- An empty output list stays a list rather than becoming an absence, so the
section is still drawn where a platform enumerates nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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>