We have a flow in our app where we allow someone to be speaking (microphone) to a bot and the bot is talking back via audio.
It works fine when there is only one device, but if the user wants to use airpods we are having problems. On iOS (Safari, and Chrome) when using wired/Bluetooth headphones that include a microphone here is what we experience:
I created a very simple page to reproduce the issue. I did find a bug that says it's been fixed but its clearly no .cgi?id=196539
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Microphone Recorder</title>
</head>
<body>
<h1>Microphone Recorder</h1>
<button id="startBtn">Start Recording</button>
<button id="stopBtn" disabled>Stop Recording</button>
<audio id="audioPlayback" controls src="/[email protected]/media/Justice_Genesis_16bit_trim_mono_y6iHYTjEyKU.wav" playsinline></audio>
<script>
let mediaRecorder;
let audioChunks = [];
document.getElementById('startBtn').addEventListener('click', async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
const audioUrl = URL.createObjectURL(audioBlob);
document.getElementById('audioPlayback').src = audioUrl;
};
mediaRecorder.start();
document.getElementById('startBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
} catch (error) {
console.error('Error accessing microphone:', error);
}
});
document.getElementById('stopBtn').addEventListener('click', () => {
mediaRecorder.stop();
document.getElementById('startBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
});
</script>
</body>
</html>
Again the steps to reproduce:
Notice: sound changes from headphones to speakers. This works fine when using Chrome on my laptop.
We have a flow in our app where we allow someone to be speaking (microphone) to a bot and the bot is talking back via audio.
It works fine when there is only one device, but if the user wants to use airpods we are having problems. On iOS (Safari, and Chrome) when using wired/Bluetooth headphones that include a microphone here is what we experience:
I created a very simple page to reproduce the issue. I did find a bug that says it's been fixed but its clearly no https://bugs.webkit.org/show_bug.cgi?id=196539
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Microphone Recorder</title>
</head>
<body>
<h1>Microphone Recorder</h1>
<button id="startBtn">Start Recording</button>
<button id="stopBtn" disabled>Stop Recording</button>
<audio id="audioPlayback" controls src="https://cdn.jsdelivr.net/npm/[email protected]/media/Justice_Genesis_16bit_trim_mono_y6iHYTjEyKU.wav" playsinline></audio>
<script>
let mediaRecorder;
let audioChunks = [];
document.getElementById('startBtn').addEventListener('click', async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
const audioUrl = URL.createObjectURL(audioBlob);
document.getElementById('audioPlayback').src = audioUrl;
};
mediaRecorder.start();
document.getElementById('startBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
} catch (error) {
console.error('Error accessing microphone:', error);
}
});
document.getElementById('stopBtn').addEventListener('click', () => {
mediaRecorder.stop();
document.getElementById('startBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
});
</script>
</body>
</html>
Again the steps to reproduce:
Notice: sound changes from headphones to speakers. This works fine when using Chrome on my laptop.
I have found a workaround! It uses the newish AudioSession API to kick iOS into rerouting properly.
In addition to the behavior in the original question, I'd like to also add that if you use a wired headset+mic, sometimes the iphone mic will still be used even though output is routed through the headphones! Very confusing.
I found a workaround for all scenarios though:
navigator.audioSession.type = 'auto'
(just to reset to defaults)getUserMedia({audio: true})
, this will return the iphone mic and likely reroute audio output to the handset speakersnavigator.audioSession.type = 'play-and-record'
This seems to "kick" iOS into rerouting audio. Now the external device's mic and speakers (headphones) will be used!
NOTE: if you attempt to set play-and-record
before calling getUserMedia
, it seems like you have a 50/50 chance of still getting the iPhone mic and not the headphones mic.
Sometimes, an additional problem will manifest after you have closed the mic stream/tracks: the audioSession will remain in play-and-record
, resulting in degraded audio output quality. The solution is to always (after releasing the mic):
navigator.audioSession.type = 'playback'
navigator.audioSession.type = 'auto'
(immediately, no need for a delay)This will again "kick" iOS into rerouting / resetting audio. You'll be back in hi fidelity!
I hope this helps!
I have posted similar instructions on the newish open bug report that seems to accurately describe the issue: https://bugs.webkit.org/show_bug.cgi?id=282939