diff --git a/src/react-components/room/ChatSidebar.js b/src/react-components/room/ChatSidebar.js index 23960e0bbf..9cf3c0f332 100644 --- a/src/react-components/room/ChatSidebar.js +++ b/src/react-components/room/ChatSidebar.js @@ -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." } }); diff --git a/src/systems/audio-system.js b/src/systems/audio-system.js index 4502340361..60964395fc 100644 --- a/src/systems/audio-system.js +++ b/src/systems/audio-system.js @@ -1,3 +1,5 @@ +import { LogMessageType } from "../react-components/room/ChatSidebar"; + let delayedReconnectTimeout = null; function performDelayedReconnect(gainNode) { if (delayedReconnectTimeout) { @@ -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(); @@ -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) { @@ -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) { @@ -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 + }); + } + }; + } }