From 6d4cca9306a38ffab938576f58a110bdc78f1b8b Mon Sep 17 00:00:00 2001 From: Gustavo Trott <gustavO@trott.com.br> Date: Fri, 21 Dec 2018 15:14:05 -0200 Subject: [PATCH] Fix audio in ios webview --- .../client/compatibility/sip.js | 11 +- .../imports/api/audio/client/bridge/sip.js | 12 +-- .../ui/services/audio-manager/index.js | 40 ++++--- .../ios-webview-audio-polyfills.js | 100 ++++++++++++++++++ 4 files changed, 141 insertions(+), 22 deletions(-) mode change 100755 => 100644 bigbluebutton-html5/client/compatibility/sip.js create mode 100644 bigbluebutton-html5/imports/ui/services/audio-manager/ios-webview-audio-polyfills.js diff --git a/bigbluebutton-html5/client/compatibility/sip.js b/bigbluebutton-html5/client/compatibility/sip.js old mode 100755 new mode 100644 index 1c86225267..5c02062d96 --- a/bigbluebutton-html5/client/compatibility/sip.js +++ b/bigbluebutton-html5/client/compatibility/sip.js @@ -11564,6 +11564,13 @@ MediaHandler.prototype = Object.create(SIP.MediaHandler.prototype, { self.ready = false; methodName = self.hasOffer('remote') ? 'createAnswer' : 'createOffer'; + if(constraints.offerToReceiveAudio) { + //Needed for Safari on webview + try { + pc.addTransceiver('audio'); + } catch (e) {} + } + return SIP.Utils.promisify(pc, methodName, true)(constraints) .catch(function methodError(e) { self.emit('peerConnection-' + methodName + 'Failed', e); @@ -11611,7 +11618,9 @@ MediaHandler.prototype = Object.create(SIP.MediaHandler.prototype, { try { streams = [].concat(streams); streams.forEach(function (stream) { - this.peerConnection.addStream(stream); + try { + this.peerConnection.addStream(stream); + } catch (e) {} }, this); } catch(e) { this.logger.error('error adding stream'); diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index 2961cfd2cb..47113af383 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -259,10 +259,8 @@ export default class SIPBridge extends BaseAudioBridge { }, }, RTCConstraints: { - mandatory: { - OfferToReceiveAudio: true, - OfferToReceiveVideo: false, - }, + offerToReceiveAudio: true, + offerToReceiveVideo: false, }, }; @@ -295,9 +293,9 @@ export default class SIPBridge extends BaseAudioBridge { }); } - const mappedCause = cause in this.errorCodes ? - this.errorCodes[cause] : - this.baseErrorCodes.GENERIC_ERROR; + const mappedCause = cause in this.errorCodes + ? this.errorCodes[cause] + : this.baseErrorCodes.GENERIC_ERROR; return this.callback({ status: this.baseCallStates.failed, diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index 58e2eed1c8..f3b9a51299 100755 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -8,6 +8,7 @@ import SIPBridge from '/imports/api/audio/client/bridge/sip'; import logger from '/imports/startup/client/logger'; import { notify } from '/imports/ui/services/notification'; import browser from 'browser-detect'; +import iosWebviewAudioPolyfills from './ios-webview-audio-polyfills'; const MEDIA = Meteor.settings.public.media; const MEDIA_TAG = MEDIA.mediaTag; @@ -53,6 +54,7 @@ class AudioManager { this.userData = userData; this.initialized = true; } + setAudioMessages(messages) { this.messages = messages; } @@ -87,10 +89,14 @@ class AudioManager { this.isWaitingPermissions = false; this.devicesInitialized = false; - return Promise.all([ - this.setDefaultInputDevice(), - this.setDefaultOutputDevice(), - ]).then(() => { + // Avoid ask microphone permission for "Listen Only" + const devicesInitializePromises = []; + if (this.isListenOnly == false) devicesInitializePromises.push(this.setDefaultInputDevice()); + devicesInitializePromises.push(this.setDefaultOutputDevice()); + + return Promise.all( + devicesInitializePromises, + ).then(() => { this.devicesInitialized = true; this.isWaitingPermissions = false; }).catch((err) => { @@ -150,6 +156,13 @@ class AudioManager { await this.askDevicesPermissions(); } + // Call polyfills for webrtc client if navigator is "iOS Webview" + const userAgent = window.navigator.userAgent.toLocaleLowerCase(); + if ((userAgent.indexOf('iphone') > -1 || userAgent.indexOf('ipad') > -1) + && userAgent.indexOf('safari') == -1) { + iosWebviewAudioPolyfills(); + } + // We need this until we upgrade to SIP 9x. See #4690 const iceGatheringErr = 'ICE_TIMEOUT'; const iceGatheringTimeout = new Promise((resolve, reject) => { @@ -312,9 +325,9 @@ class AudioManager { this.listenOnlyAudioContext.close(); } - this.listenOnlyAudioContext = window.AudioContext ? - new window.AudioContext() : - new window.webkitAudioContext(); + this.listenOnlyAudioContext = window.AudioContext + ? new window.AudioContext() + : new window.webkitAudioContext(); const dest = this.listenOnlyAudioContext.createMediaStreamDestination(); @@ -331,8 +344,8 @@ class AudioManager { } isUsingAudio() { - return this.isConnected || this.isConnecting || - this.isHangingUp || this.isEchoTest; + return this.isConnected || this.isConnecting + || this.isHangingUp || this.isEchoTest; } setDefaultInputDevice() { @@ -349,11 +362,10 @@ class AudioManager { return Promise.resolve(inputDevice); }; - const handleChangeInputDeviceError = () => - Promise.reject({ - type: 'MEDIA_ERROR', - message: this.messages.error.MEDIA_ERROR, - }); + const handleChangeInputDeviceError = () => Promise.reject({ + type: 'MEDIA_ERROR', + message: this.messages.error.MEDIA_ERROR, + }); if (!deviceId) { return this.bridge.setDefaultInputDevice() diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/ios-webview-audio-polyfills.js b/bigbluebutton-html5/imports/ui/services/audio-manager/ios-webview-audio-polyfills.js new file mode 100644 index 0000000000..f4f8cc0eac --- /dev/null +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/ios-webview-audio-polyfills.js @@ -0,0 +1,100 @@ +const iosWebviewAudioPolyfills = function () { + function shimRemoteStreamsAPI(window) { + if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) { + window.RTCPeerConnection.prototype.getRemoteStreams = function () { + return this._remoteStreams ? this._remoteStreams : []; + }; + } + if (!('onaddstream' in window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { + get: function get() { + return this._onaddstream; + }, + set: function set(f) { + const _this3 = this; + + if (this._onaddstream) { + this.removeEventListener('addstream', this._onaddstream); + this.removeEventListener('track', this._onaddstreampoly); + } + this.addEventListener('addstream', this._onaddstream = f); + this.addEventListener('track', this._onaddstreampoly = function (e) { + e.streams.forEach((stream) => { + if (!_this3._remoteStreams) { + _this3._remoteStreams = []; + } + if (_this3._remoteStreams.includes(stream)) { + return; + } + _this3._remoteStreams.push(stream); + const event = new Event('addstream'); + event.stream = stream; + _this3.dispatchEvent(event); + }); + }); + }, + }); + const origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; + window.RTCPeerConnection.prototype.setRemoteDescription = function () { + const pc = this; + if (!this._onaddstreampoly) { + this.addEventListener('track', this._onaddstreampoly = function (e) { + e.streams.forEach((stream) => { + if (!pc._remoteStreams) { + pc._remoteStreams = []; + } + if (pc._remoteStreams.indexOf(stream) >= 0) { + return; + } + pc._remoteStreams.push(stream); + const event = new Event('addstream'); + event.stream = stream; + pc.dispatchEvent(event); + }); + }); + } + return origSetRemoteDescription.apply(pc, arguments); + }; + } + } + + function shimCallbacksAPI(window) { + const prototype = window.RTCPeerConnection.prototype; + const createOffer = prototype.createOffer; + const setLocalDescription = prototype.setLocalDescription; + const setRemoteDescription = prototype.setRemoteDescription; + + prototype.createOffer = function (successCallback, failureCallback) { + const options = arguments.length >= 2 ? arguments[2] : arguments[0]; + const promise = createOffer.apply(this, [options]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + + prototype.setLocalDescription = function withCallback(description, successCallback, failureCallback) { + const promise = setLocalDescription.apply(this, [description]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + + prototype.setRemoteDescription = function withCallback(description, successCallback, failureCallback) { + const promise = setRemoteDescription.apply(this, [description]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + } + + shimCallbacksAPI(window); + shimRemoteStreamsAPI(window); +}; + +export default iosWebviewAudioPolyfills; -- GitLab