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