Skip to content

Commit

Permalink
Safari mobile audiocontext suspended fix
Browse files Browse the repository at this point in the history
  • Loading branch information
keianhzo committed Feb 26, 2021
1 parent e83d75c commit 9910206
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 25 deletions.
8 changes: 8 additions & 0 deletions src/react-components/room/ChatSidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ const logMessages = defineMessages({
id: "chat-sidebar.log-message.invalid-audio-normalization-range",
defaultMessage:
"audioNormalization command needs a base volume number between 0 [no normalization] and 255. Default is 0. The recommended value is 4, if you would like to enable normalization."
},
[LogMessageType.audioSuspended]: {
id: "chat-sidebar.log-message.audio-suspended",
defaultMessage: "Audio has been suspended, click somewhere in the room to resume the audio."
},
[LogMessageType.audioResumed]: {
id: "chat-sidebar.log-message.audio-resumed",
defaultMessage: "Audio has been resumed."
}
});

Expand Down
90 changes: 65 additions & 25 deletions src/systems/audio-system.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { LogMessageType } from "../react-components/room/ChatSidebar";

let delayedReconnectTimeout = null;
function performDelayedReconnect(gainNode) {
if (delayedReconnectTimeout) {
Expand Down Expand Up @@ -102,12 +104,13 @@ async function enableChromeAEC(gainNode) {

export class AudioSystem {
constructor(sceneEl) {
sceneEl.audioListener = sceneEl.audioListener || new THREE.AudioListener();
if (sceneEl.camera) {
sceneEl.camera.add(sceneEl.audioListener);
this._sceneEl = sceneEl;
this._sceneEl.audioListener = this._sceneEl.audioListener || new THREE.AudioListener();
if (this._sceneEl.camera) {
this._sceneEl.camera.add(this._sceneEl.audioListener);
}
sceneEl.addEventListener("camera-set-active", evt => {
evt.detail.cameraEl.getObject3D("camera").add(sceneEl.audioListener);
this._sceneEl.addEventListener("camera-set-active", evt => {
evt.detail.cameraEl.getObject3D("camera").add(this._sceneEl.audioListener);
});

this.audioContext = THREE.AudioContext.getContext();
Expand All @@ -120,28 +123,15 @@ export class AudioSystem {
this.analyserLevels = new Uint8Array(this.outboundAnalyser.fftSize);
this.outboundGainNode.connect(this.outboundAnalyser);
this.outboundAnalyser.connect(this.mediaStreamDestinationNode);
this.audioContextNeedsToBeResumed = false;

/**
* Chrome and Safari will start Audio contexts in a "suspended" state.
* A user interaction (touch/mouse event) is needed in order to resume the AudioContext.
*/
const resume = () => {
this.audioContext.resume();

setTimeout(() => {
if (this.audioContext.state === "running") {
if (!AFRAME.utils.device.isMobile() && /chrome/i.test(navigator.userAgent)) {
enableChromeAEC(sceneEl.audioListener.gain);
}

document.body.removeEventListener("touchend", resume, false);
document.body.removeEventListener("mouseup", resume, false);
}
}, 0);
};
// Safari Mobile fix
if (AFRAME.utils.device.isMobile() && AFRAME.utils.device.isIOS()) {
this._safariMobileAudioInterruptionFix();
}

document.body.addEventListener("touchend", resume, false);
document.body.addEventListener("mouseup", resume, false);
document.body.addEventListener("touchend", this._resumeAudioContext, false);
document.body.addEventListener("mouseup", this._resumeAudioContext, false);
}

addStreamToOutboundAudio(id, mediaStream) {
Expand All @@ -154,6 +144,11 @@ export class AudioSystem {
sourceNode.connect(gainNode);
gainNode.connect(this.outboundGainNode);
this.audioNodes.set(id, { sourceNode, gainNode });

document.getElementById("avatar-rig").messageDispatch.receive({
type: "log",
messageType: LogMessageType.audioResumed
});
}

removeStreamFromOutboundAudio(id) {
Expand All @@ -164,4 +159,49 @@ export class AudioSystem {
this.audioNodes.delete(id);
}
}

/**
* Chrome and Safari will start Audio contexts in a "suspended" state.
* A user interaction (touch/mouse event) is needed in order to resume the AudioContext.
*/
_resumeAudioContext = () => {
this.audioContext.resume();

setTimeout(() => {
if (this.audioContext.state === "running") {
if (!AFRAME.utils.device.isMobile() && /chrome/i.test(navigator.userAgent)) {
enableChromeAEC(this._sceneEl.audioListener.gain);
}

document.body.removeEventListener("touchend", this._resumeAudioContext, false);
document.body.removeEventListener("mouseup", this._resumeAudioContext, false);
}
}, 0);
};

// Safari mobile fix
// https://stackoverflow.com/questions/10232908/is-there-a-way-to-detect-a-mobile-safari-audio-interruption-headphones-unplugg
_safariMobileAudioInterruptionFix() {
this.audioContext.onstatechange = () => {
console.log(`AudioContext state changed to ${this.audioContext.state}`);
if (this.audioContext.state === "suspended") {
// When you unplug the headphone or when the bluetooth headset disconnects on
// iOS Safari or Chrome, the state changes to suspended.
// Chrome Android doesn't go in suspended state for this case.
document.getElementById("avatar-rig").messageDispatch.receive({
type: "log",
messageType: LogMessageType.audioSuspended
});
document.body.addEventListener("touchend", this._resumeAudioContext, false);
document.body.addEventListener("mouseup", this._resumeAudioContext, false);
this.audioContextNeedsToBeResumed = true;
} else if (this.audioContext.state === "running" && this.audioContextNeedsToBeResumed) {
this.audioContextNeedsToBeResumed = false;
document.getElementById("avatar-rig").messageDispatch.receive({
type: "log",
messageType: LogMessageType.audioResumed
});
}
};
}
}

0 comments on commit 9910206

Please sign in to comment.