Documentation Index
Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
Use this file to discover all available pages before exploring further.
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