Device Management
The Telnyx WebRTC JS SDK uses the browser’s MediaDevices API for audio device management. This guide covers selecting devices, switching mid-call, and handling permission changes.
Enumerate Devices
List available audio input and output devices:
const devices = await navigator.mediaDevices.enumerateDevices();
const microphones = devices.filter(d => d.kind === 'audioinput');
const speakers = devices.filter(d => d.kind === 'audiooutput');
microphones.forEach((mic, i) => {
console.log(`Mic ${i}: ${mic.label} (${mic.deviceId})`);
});
speakers.forEach((speaker, i) => {
console.log(`Speaker ${i}: ${speaker.label} (${speaker.deviceId})`);
});
Device labels are only available after the user grants microphone permission. Before permission, label is an empty string and deviceId is a placeholder.
Request Permissions
Before you can select a specific device, the user must grant microphone access:
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Permission granted — device labels now available
stream.getTracks().forEach(track => track.stop()); // Release immediately
} catch (err) {
if (err.name === 'NotAllowedError') {
console.error('User denied microphone permission');
} else if (err.name === 'NotFoundError') {
console.error('No microphone found');
}
}
Select a Specific Device
When placing a call
// Get the device ID first
const devices = await navigator.mediaDevices.getUserMedia({ audio: true });
const micDeviceId = devices.getAudioTracks()[0].getSettings().deviceId;
devices.getTracks().forEach(t => t.stop());
// Use it when placing the call
const call = client.newCall({
destinationNumber: '+12345678900',
audio: true,
localStream: await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: { exact: micDeviceId },
},
}),
});
Via ICallOptions constraints
const call = client.newCall({
destinationNumber: '+12345678900',
audio: true,
// The SDK will request this specific device
});
Switch Devices Mid-Call
Replace the audio track on an active PeerConnection:
async function switchMicrophone(newDeviceId) {
if (!call?.peerConnection) return;
// Get new stream with the selected device
const newStream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: { exact: newDeviceId },
},
});
const newTrack = newStream.getAudioTracks()[0];
const sender = call.peerConnection
.getSenders()
.find(s => s.track?.kind === 'audio');
if (sender) {
await sender.replaceTrack(newTrack);
console.log('Switched to microphone:', newTrack.label);
}
}
replaceTrack() doesn’t require renegotiation — the switch is seamless. The remote party won’t hear a gap.
Speaker Output
Set the audio output device (sink) on the audio element:
const audioElement = document.getElementById('remoteAudio');
// Check if the browser supports sink selection
if (typeof audioElement.sinkId !== 'undefined') {
const devices = await navigator.mediaDevices.enumerateDevices();
const speakers = devices.filter(d => d.kind === 'audiooutput');
// Switch to a specific speaker
await audioElement.setSinkId(speakers[1].deviceId);
}
setSinkId() is not supported in all browsers. Safari does not support it as of 2026. Check typeof audioElement.sinkId !== 'undefined' before using.
Device Change Detection
Listen for device changes (headphones plugged in, Bluetooth connected, etc.):
navigator.mediaDevices.addEventListener('devicechange', async () => {
console.log('Audio devices changed');
const devices = await navigator.mediaDevices.enumerateDevices();
const mics = devices.filter(d => d.kind === 'audioinput');
// Update device picker UI
updateMicrophoneList(mics);
});
Common scenarios:
- Headphones plugged in → switch output to headphones
- Bluetooth headset disconnected → fall back to built-in speaker
- USB microphone connected → update device list
Mute vs Device Off
Don’t confuse muting with device management:
| Action | What it does | Remote party hears |
|---|
call.muteAudio() | Stops sending audio | Silence |
track.enabled = false | Same as mute (lower level) | Silence |
| Switching to a different mic | Changes input device | New mic audio |
| Revoking mic permission | Browser blocks access | Nothing |
Common Issues
”Device not found” after permission grant
Cause: The device list was cached before permission was granted. Labels and real device IDs are only available after getUserMedia().
Fix: Re-enumerate devices after permission is granted:
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(t => t.stop());
// Now enumerate — labels and real IDs are available
const devices = await navigator.mediaDevices.enumerateDevices();
Echo or feedback
Cause: Speaker output is being picked up by the microphone (especially with built-in speakers + mic on laptops).
Fix:
- Use echo cancellation (enabled by default in most browsers)
- Recommend headphones for long calls
- Use
call.muteAudio() when not speaking
Device disappears mid-call
Cause: Bluetooth disconnected, USB device unplugged.
Fix:
- Listen for
devicechange events
- Fall back to the default device:
navigator.mediaDevices.addEventListener('devicechange', async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
const defaultMic = devices.find(d => d.kind === 'audioinput');
if (defaultMic) {
await switchMicrophone(defaultMic.deviceId);
}
});
See Also