Managing Audio Focus and Echo Cancellation Across Devices in WebRTC
Resolve audio routing conflicts, echo, and focus loss when switching between Bluetooth, wired headsets, and speakerphone during active WebRTC sessions.
OS-Level Audio Focus vs WebRTC Media Constraints
Effective Audio/Video Track Management begins with understanding how the host OS arbitrates audio focus. When a WebRTC session initializes, the browser delegates routing to the platformβs audio HAL. Android implicitly triggers AudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION), while iOS relies on AVAudioSession routing.
Always explicitly request echoCancellation: true and noiseSuppression: true in getUserMedia constraints. This prevents the browser from falling back to conflicting software processing pipelines.
Reproduction Steps
- Start a WebRTC call on a mobile device with Bluetooth headphones connected.
- Force disconnect Bluetooth via OS settings while the call is active.
- Observe audio routing fallback to speakerphone and check for sudden echo or focus loss.
- Verify if
MediaStreamTrack.onendedfires unexpectedly due to focus arbitration.
Key Log Patterns
[WebRTC] AudioDeviceModule::SetRecordingDevice failed: Device busyAEC: Hardware AEC disabled, falling back to WebRTC APMAudioFocus: Request failed, another app holds focus
Dynamic Device Switching and Sink Routing
Cross-platform routing requires intercepting the devicechange event. Apply HTMLMediaElement.setSinkId() or MediaStreamTrack.applyConstraints() without triggering full SDP renegotiation. Proper integration within broader Media Handling, Codecs & Bandwidth Estimation pipelines ensures track replacements maintain jitter buffer continuity.
Preserve the existing RTCRtpSender and swap the underlying MediaStreamTrack using replaceTrack(). This avoids renegotiation latency and stream interruptions.
Reproduction Steps
- Establish a peer-to-peer WebRTC connection with default audio input.
- Plug in a USB microphone or Bluetooth headset.
- Enumerate devices via
navigator.mediaDevices.enumerateDevices(). - Call
replaceTrack()with the new audio track. VerifygetStats()showsaudioLevelcontinuity.
Key Log Patterns
ICE: Connection state changed to connectedMediaStreamTrack: Constraint applied successfullyAudioProcessingModule: AEC tail length adjusted to 480ms
Debugging Echo Cancellation Failures and Focus Conflicts
Persistent echo or focus drops typically stem from mismatched sample rates, aggressive AGC, or background app suspension. Use chrome://webrtc-internals or about:webrtc to monitor googEchoCancellation metrics. If hardware AEC is unavailable, the browser injects a software delay line.
Monitor audioInputLevel and audioOutputLevel in real-time. This detects clipping or feedback loops caused by improper gain staging.
Reproduction Steps
- Run a call with
echoCancellation: trueandautoGainControl: false. - Place the device near the speaker to simulate acoustic feedback.
- Check
getStats()fortotalAudioEnergyandechoReturnLossEnhancement. - If echo persists, force software AEC by setting
googEchoCancellation: truein advanced constraints.
Key Log Patterns
AudioDeviceModule: Audio delay compensation applied: 120msAEC: Divergence detected, resetting filterMediaStreamTrack: muted state changed to true (focus lost)
Implementation Patterns
Initialize Audio Stream with Explicit AEC Constraints
const constraints = {
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: false,
latency: 0,
deviceId: { ideal: 'default' }
}
};
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
const audioTrack = stream.getAudioTracks()[0];
document.addEventListener('visibilitychange', () => {
audioTrack.enabled = !document.hidden; // Prevent background echo
});
} catch (err) {
console.error('Audio focus/constraint error:', err.name, err.message);
}
Dynamic Track Replacement Without SDP Renegotiation
async function switchAudioDevice(newDeviceId, peerConnection) {
const newStream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: { exact: newDeviceId }, echoCancellation: true }
});
const newTrack = newStream.getAudioTracks()[0];
const sender = peerConnection.getSenders().find(s => s.track.kind === 'audio');
if (sender) {
await sender.replaceTrack(newTrack);
console.log('Audio track swapped successfully');
}
}
Common Mistakes
- Enabling hardware and software AEC simultaneously, causing comb filtering and metallic echo.
- Ignoring
visibilitychangeandfocusevents, triggering background routing conflicts. - Triggering full SDP renegotiation on device switches instead of using
replaceTrack(). - Setting
autoGainControl: truewith high-gain mics, causing AEC filter divergence. - Failing to handle
setSinkId()browser compatibility (Firefox/Safari require anHTMLAudioElementwrapper).
FAQ
Why does echo return when switching from Bluetooth to wired headphones during a call? The OS audio HAL reinitializes the routing path, temporarily disabling hardware AEC. The browser falls back to software AEC with incorrect latency assumptions until the delay line recalibrates.
How can I force hardware echo cancellation in WebRTC?
Set echoCancellation: true in getUserMedia constraints. The browser automatically negotiates with the platform driver. Verify activation via googEchoCancellation in getStats() or chrome://webrtc-internals.
Does WebRTC support audio focus management on iOS Safari?
iOS Safari strictly enforces AVAudioSession routing. WebRTC requests communication mode automatically, but you must handle pagehide/visibilitychange to mute tracks on background, as iOS suspends Web Audio and MediaStream processing.