Skip to main content

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:
ActionWhat it doesRemote party hears
call.muteAudio()Stops sending audioSilence
track.enabled = falseSame as mute (lower level)Silence
Switching to a different micChanges input deviceNew mic audio
Revoking mic permissionBrowser blocks accessNothing

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:
  1. Use echo cancellation (enabled by default in most browsers)
  2. Recommend headphones for long calls
  3. Use call.muteAudio() when not speaking

Device disappears mid-call

Cause: Bluetooth disconnected, USB device unplugged. Fix:
  1. Listen for devicechange events
  2. 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