Device Management
Pick the mic, camera, and speaker — before a call and on the fly
Pick the mic, camera, and speaker — before a call and on the fly
To let users pick which microphone, camera, and speaker a call uses, subscribe to the device lists, render a picker, and apply the user’s choice — either as a preference for the next call or as a live swap during one. The same APIs cover hot-plug events, so a new headset shows up in the list as soon as it’s connected.
The sections below cover requesting permission, reading the device lists, applying the user’s pick before a call and again mid-call, routing remote audio to the chosen speaker, and reacting when a device disappears.
Before you start. Devices live behind the browser’s permission gate. getUserMedia only runs on secure origins (HTTPS or localhost), and device labels are empty strings until the user has granted access at least once. Plan the UX so picking a device comes after the prompt — not the other way around.
Each kind comes with its own pair of observables: one for the list of available devices, and one for the current selection. Subscribe to both whenever you render a picker so the <select> reflects what’s actually in use — including changes that happen without an explicit user pick, such as a recovery after the active device was unplugged.
audioInputDevices$ lists every mic; selectedAudioInputDevice$ tracks the current pick.
videoInputDevices$ lists every camera; selectedVideoInputDevice$ tracks the current pick.
audioOutputDevices$ lists every speaker; selectedAudioOutputDevice$ tracks the current pick.
When you only need the current snapshot — for example, populating a dropdown once on click — read the non-$ accessor instead: client.audioInputDevices, client.videoInputDevices, client.audioOutputDevices. Use the observable when the UI should keep up with hot-plugs and SDK-driven switches.
Each list populates from navigator.mediaDevices.enumerateDevices(). Until the user grants permission, that call returns devices with empty label strings, which makes a picker UI useless. Call requestMediaPermissions() once at startup to drive the prompt and re-enumerate with labels filled in:
The returned PermissionResult reports which scopes were granted and which device the browser handed back for each kind. Those devices become the initial selection unless you’ve already set a preference. The browser only shows the prompt the first time per origin; later calls resolve immediately with the existing grant.
If the user denies the prompt, enumerateDevices() still returns the device list, but with empty labels. Show a “permission needed” banner instead of an empty dropdown, and link to the browser’s lock-icon controls so they can grant it later.
Each list is an observable of MediaDeviceInfo[]. Subscribe once at startup — every subscription receives the current list immediately and then again whenever the OS reports a change. Pair the list with the matching selected…$ observable so a <select> reflects the right value when the SDK switches devices on its own:
The same pattern fits videoInputDevices$ and audioOutputDevices$ — only the property names change. Caching the latest of each list in module-scope state keeps the rendering pure and avoids pulling in observable combinators.
There are two scopes for “use this device.” Match the scope to the moment the user picks:
Set a preference on the client. The selection sticks for every future dial() and answer(), so the next outbound call is captured from the right mic and camera without any extra wiring. Wire the picker’s change event to look up the chosen MediaDeviceInfo from the live list and pass it to selectAudioInputDevice() or selectVideoInputDevice():
The next dial() captures from the chosen mic and camera with no extra arguments. To change the default back later, look up a new device the same way, or pass null to clear the preference.
In practice, a single picker handler covers both scopes — fall through to the client-level preference when there’s no active call, replace the live track when there is:
Speakers follow a slightly different path — see the next section.
Selecting a speaker takes an extra step the other devices don’t — most browsers route audio output through the <video> or <audio> element the remote stream is attached to, and switching the sink is an element-level call. applySelectedAudioOutputDevice() wraps HTMLMediaElement.setSinkId() so you don’t have to:
The method returns true when the sink was changed and false when no speaker is selected or the browser doesn’t support setSinkId. Call it once after binding the remote stream to the element, and again any time the user picks a new speaker.
Firefox doesn’t ship setSinkId yet. applySelectedAudioOutputDevice() returns false on Firefox and audio plays through the system default speaker. Check the return value and surface a “speaker selection unavailable” notice rather than silently swallowing the choice — see the MDN compatibility table.
Hot-plug events are already covered by the list observables — adding a USB headset re-emits audioInputDevices$ with the new entry. What you usually want on top of that is a notification when the SDK automatically switches a device because the previous one was unplugged. Subscribe to deviceRecovered$:
The DeviceRecoveryEvent carries everything you need to compose that notification:
Reach for it when the user should know “we lost your AirPods and put you on the built-in mic” rather than discovering it mid-sentence.
Device monitoring is on by default. Use disableDeviceMonitoring() and enableDeviceMonitoring() to pause and resume it — handy on mobile when the page goes into the background and you don’t want hot-plug events firing while the user can’t see the UI.
The fastest way to see the device APIs end-to-end is a single page that wires the three observable lists to three <select> elements, dials a test destination on demand, and switches the mic, camera, or speaker on the live call from the same picker. The handler routes preference changes through client.* before the call and live-track swaps through activeCall.self.* while the call is up — so the same dropdowns exercise both branches of the Apply the user’s choice section.
Create a SAT for your project — the Authentication guide covers the production version of this flow; the Create Subscriber Token reference sends the request for you.
Save the page below as devices-demo.html and open it over HTTPS (or localhost). Paste the SAT and click Connect — the log reports the WebSocket coming up. Click Request permission to drive the browser’s prompt; the three dropdowns populate with labelled devices.
Pick a different mic, camera, or speaker before dialing. The log records each pick as preference: … — that’s the client.select* branch in action. Plug or unplug a USB headset to see deviceRecovered$ fire.
Enter a destination in the Destination field (a /public/<resource>, /private/<user>, PSTN number, or SIP URI the token can reach — see Outbound Calls for the destination shapes) and click Dial. The local and remote tiles populate once the call connects.
With the call connected, change the Microphone or Camera dropdown again. The log now records the change as live: … — the picker is routing through activeCall.self.selectAudioInputDevice() instead of the client-level preference, and the remote side hears or sees the new device immediately without renegotiation. Picking a new Speaker triggers applySelectedAudioOutputDevice() on the remote <video> element so the sink swaps too.
Click Hang up and try the dropdowns again — the log goes back to preference: … because activeCall is null. That’s the combined handler from the Apply the user’s choice section, end to end.