diff --git a/bigbluebutton-html5/client/compatibility/sip.js b/bigbluebutton-html5/client/compatibility/sip.js old mode 100755 new mode 100644 index 1c8622526700a5e9e1c8ca446a8aeccfaa370f54..5c02062d9674a1b2d62f372553769452ba4d2fd7 --- 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 2961cfd2cbcd912db87c658e6bb4acf8c85fc3b9..47113af383f23616afc11bd7f2410a9f73cbe0aa 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 58e2eed1c883d1c1bb09906a359eac9f5d34147d..f3b9a51299d9bd44b64967bfcc8aec9db7965950 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 0000000000000000000000000000000000000000..f4f8cc0eac8ab93e34037163ec3062b86f91b96c --- /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;