Summary
Web Audio API developers often think navigator.mediaDevices.getUserMedia({ audio: { deviceId } }) gives a playback stream for a selected output device. In reality the API always returns an input (microphone) stream, because getUserMedia is defined for capture devices only. Trying to route audio through a MediaStreamDestinationNode built from that stream leads to silent output and misleading track labels.
Root Cause
getUserMediacaptures audio; it never opens a speaker or headset for playback.- The
deviceIdconstraint filters input devices, not output devices. - Browsers expose output device selection only via
HTMLMediaElement.setSinkId()(or the newerAudioOutputDeviceAPI), which works with rendering nodes, not withMediaStreaminputs. - The code creates a
MediaStreamDestinationNodefrom the captured stream, but the node’s purpose is to export audio out of theAudioContext, not to direct audio to a specific hardware output.
Why This Happens in Real Systems
- API design separation: capture (
getUserMedia) vs. render (AudioContext.destination,setSinkId). - Security model: browsers restrict arbitrary output routing to prevent covert channel attacks.
- Device enumeration returns both input and output kinds, but constraints in
getUserMediaare only meaningful foraudioinput. - Modern browsers deliberately ignore
deviceIdfor output when used withgetUserMedia, falling back to the default microphone.
Real-World Impact
- Silent tones when developers expect sound on a chosen interface.
- Confusing diagnostics: track label shows the microphone, leading to wasted debugging time.
- Ham radio or signaling applications may fail to transmit, potentially violating regulatory requirements.
- User experience degradation: UI appears to accept a device selection, but nothing audible happens.
Example or Code (if necessary and relevant)
// Correct way to play a tone on a user‑selected output device
async function playToneOnDevice(outputDeviceId) {
const audioCtx = new AudioContext();
const oscillator = audioCtx.createOscillator();
oscillator.type = "sine";
oscillator.frequency.value = 1750;
// Connect to the normal destination (the default output)
oscillator.connect(audioCtx.destination);
oscillator.start();
// Route the AudioContext’s destination to the chosen output
// Works only on elements that support setSinkId()
const dummyAudio = new Audio();
dummyAudio.srcObject = audioCtx.createMediaStreamDestination().stream;
await dummyAudio.setSinkId(outputDeviceId);
dummyAudio.play();
}
How Senior Engineers Fix It
- Use
setSinkIdon anHTMLMediaElement(orAudioWorkletNode+AudioContext.destination) to select the output device. - Separate concerns: capture with
getUserMedia, playback withAudioContext+setSinkId. - Validate device kind: ensure the selected
deviceIdcomes fromkind === "audiooutput"before callingsetSinkId. - Graceful fallback: if
setSinkIdis unavailable, notify the user that explicit output selection isn’t supported on their browser. - Audit permissions: request microphone only when actually needed; avoid unnecessary
getUserMediacalls.
Why Juniors Miss It
- Assume symmetry between input and output device APIs, treating
deviceIdas a universal selector. - Overlook documentation that
getUserMediais for capture only. - Mix up node types (
MediaStreamDestinationNodevs.MediaStreamAudioSourceNode) and think the former can drive hardware outputs. - Skip testing on multiple browsers, missing the fact that Chrome enforces this separation strictly.