diff --git a/bigbluebutton-html5/client/compatibility/kurento-extension.js b/bigbluebutton-html5/client/compatibility/kurento-extension.js
index 2229844ae4d938cd16b3cd7ff3014555798cca90..060a677a6ef39bc2fc62a44152cc503c6b345aab 100644
--- a/bigbluebutton-html5/client/compatibility/kurento-extension.js
+++ b/bigbluebutton-html5/client/compatibility/kurento-extension.js
@@ -476,12 +476,8 @@ Kurento.prototype.setAudio = function (tag) {
 
 Kurento.prototype.listenOnly = function () {
   var self = this;
-  const remoteVideo = document.getElementById(this.renderTag);
-  remoteVideo.muted = true;
   if (!this.webRtcPeer) {
     var options = {
-      audioStream: this.inputStream,
-      remoteVideo,
       onicecandidate : this.onListenOnlyIceCandidate.bind(this),
       mediaConstraints: {
         audio: true,
diff --git a/bigbluebutton-html5/client/compatibility/kurento-utils.js b/bigbluebutton-html5/client/compatibility/kurento-utils.js
index e3b8a003d39e81798390c59f75bbf22d5a46d8a0..5057232faa7ff13fe60123f90241ebd574e25986 100644
--- a/bigbluebutton-html5/client/compatibility/kurento-utils.js
+++ b/bigbluebutton-html5/client/compatibility/kurento-utils.js
@@ -38,11 +38,13 @@ function noop(error) {
         logger.error(error);
 }
 function trackStop(track) {
-    track.stop && track.stop();
+    track && track.stop && track.stop();
 }
 function streamStop(stream) {
-    stream.getTracks().forEach(trackStop);
+    let track = stream.track;
+    trackStop(track);
 }
+
 var dumpSDP = function (description) {
     if (typeof description === 'undefined' || description === null) {
         return '';
@@ -67,7 +69,9 @@ function bufferizeCandidates(pc, onerror) {
             break;
         case 'stable':
             if (pc.remoteDescription) {
-                pc.addIceCandidate(candidate, callback, callback);
+                pc.addIceCandidate(candidate).then(callback).catch(err => {
+                    callback(err);
+                });
                 break;
             }
         default:
@@ -228,7 +232,7 @@ function WebRtcPeer(mode, options, callback) {
                 candidategatheringdone = true;
         }
     });
-    pc.ontrack = options.onaddstream;
+    pc.onaddtrack = options.onaddstream;
     pc.onnegotiationneeded = options.onnegotiationneeded;
     this.on('newListener', function (event, listener) {
         if (event === 'icecandidate' || event === 'candidategatheringdone') {
@@ -300,7 +304,8 @@ function WebRtcPeer(mode, options, callback) {
             }).then(() => { remoteVideo.muted = false; played = true; attempt = 0;});
           }
         }
-        var stream = pc.getRemoteStreams()[0];
+
+        let stream = self.getRemoteStream();
 
         remoteVideo.oncanplaythrough = function() {
           playVideo();
@@ -338,10 +343,10 @@ function WebRtcPeer(mode, options, callback) {
         if (pc.signalingState === 'closed') {
             return callback('PeerConnection is closed');
         }
-        pc.setRemoteDescription(answer, function () {
+        pc.setRemoteDescription(answer).then(function () {
             setRemoteVideo();
             callback();
-        }, callback);
+        }).catch(callback);
     };
     this.processOffer = function (sdpOffer, callback) {
         callback = callback.bind(this);
@@ -398,10 +403,10 @@ function WebRtcPeer(mode, options, callback) {
             self.showLocalVideo();
         }
         if (videoStream) {
-            pc.addStream(videoStream);
+            videoStream.getTracks().forEach(track => pc.addTrack(track, videoStream));
         }
         if (audioStream) {
-            pc.addStream(audioStream);
+            audioStream.getTracks().forEach(track => pc.addTrack(track, audioStream));
         }
         var browser = parser.getBrowser();
         if (mode === 'sendonly' && (browser.name === 'Chrome' || browser.name === 'Chromium') && browser.major === 39) {
@@ -459,23 +464,28 @@ function createEnableDescriptor(type) {
         get: function () {
             if (!this.peerConnection)
                 return;
-            var streams = this.peerConnection.getLocalStreams();
-            if (!streams.length)
+
+            var senders = this.peerConnection.getSenders();
+            if (!senders.length)
                 return;
-            for (var i = 0, stream; stream = streams[i]; i++) {
-                var tracks = stream[method]();
-                for (var j = 0, track; track = tracks[j]; j++)
-                    if (!track.enabled)
-                        return false;
-            }
+
+            senders.forEach(sender => {
+                let track = sender.track;
+                if (!track.enabled && track.kind === type) {
+                    return false;
+                }
+            });
             return true;
         },
         set: function (value) {
             function trackSetEnable(track) {
                 track.enabled = value;
             }
-            this.peerConnection.getLocalStreams().forEach(function (stream) {
-                stream[method]().forEach(trackSetEnable);
+            this.peerConnection.getSenders().forEach(function (stream) {
+                let track = stream.track;
+                if (track.kind === type) {
+                    trackSetEnable(track);
+                }
             });
         }
     };
@@ -493,15 +503,37 @@ Object.defineProperties(WebRtcPeer.prototype, {
     'audioEnabled': createEnableDescriptor('Audio'),
     'videoEnabled': createEnableDescriptor('Video')
 });
-WebRtcPeer.prototype.getLocalStream = function (index) {
-    if (this.peerConnection) {
-        return this.peerConnection.getLocalStreams()[index || 0];
-    }
+WebRtcPeer.prototype.getLocalStream = function () {
+  if (this.localStream) {
+    return this.localStream;
+  }
+
+  if (this.peerConnection) {
+    this.localStream = new MediaStream();
+    this.peerConnection.getSenders().forEach((ls) => {
+      let track = ls.track;
+      if (track && !track.muted) {
+        this.localStream.addTrack(track);
+      }
+    });
+    return this.localStream;
+  }
 };
-WebRtcPeer.prototype.getRemoteStream = function (index) {
-    if (this.peerConnection) {
-        return this.peerConnection.getRemoteStreams()[index || 0];
-    }
+WebRtcPeer.prototype.getRemoteStream = function () {
+  if (this.remoteStream) {
+    return this.remoteStream;
+  }
+
+  if (this.peerConnection) {
+    this.remoteStream = new MediaStream();
+    this.peerConnection.getReceivers().forEach((rs) => {
+      let track = rs.track;
+      if (track && !track.muted) {
+        this.remoteStream.addTrack(track);
+      }
+    });
+    return this.remoteStream;
+  }
 };
 WebRtcPeer.prototype.dispose = function () {
     // logger.debug('Disposing WebRtcPeer');
@@ -516,7 +548,13 @@ WebRtcPeer.prototype.dispose = function () {
         if (pc) {
             if (pc.signalingState === 'closed')
                 return;
-            pc.getLocalStreams().forEach(streamStop);
+            pc.getSenders().forEach(streamStop);
+            if (this.remoteStream) {
+                this.remoteStream = null;
+            }
+            if (this.localStream) {
+                this.localStream = null;
+            }
             pc.close();
         }
     } catch (err) {
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js
index 868cd319a5afbc0261f5bb23d1647cbb56eeff71..2608e1dd42b072e305a966c5337a7458da0990a1 100644
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js
@@ -62,10 +62,6 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
     this.voiceBridge = voiceBridge;
   }
 
-  exitAudio(listenOnly) {
-    window.kurentoExitAudio();
-  }
-
   joinAudio({ isListenOnly, inputStream }, callback) {
     return new Promise(async (resolve, reject) => {
       this.callback = callback;
@@ -86,7 +82,18 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
           inputStream,
         };
 
-        const onSuccess = ack => resolve(this.callback({ status: this.baseCallStates.started }));
+        const onSuccess = ack => {
+          const { webRtcPeer } = window.kurentoManager.kurentoAudio;
+          if (webRtcPeer) {
+            const audioTag = document.getElementById(MEDIA_TAG);
+            const stream = webRtcPeer.getRemoteStream();
+            audioTag.pause();
+            audioTag.srcObject = stream;
+            audioTag.muted = false;
+            audioTag.play();
+          }
+          resolve(this.callback({ status: this.baseCallStates.started }));
+        };
 
         const onFail = error => {
           const { reason } = error;
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
index 63cd05048d733abbcfae9951feddd8af8ad7e626..53da41f6e3e1d6f07541a34931f204c97a0df078 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
@@ -407,11 +407,12 @@ class VideoProvider extends Component {
         options.configuration.iceServers = iceServers;
       }
 
-      let WebRtcPeerObj = window.kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly;
-
+      let WebRtcPeerObj;
       if (shareWebcam) {
         WebRtcPeerObj = window.kurentoUtils.WebRtcPeer.WebRtcPeerSendonly;
         this.shareWebcam();
+      } else {
+        WebRtcPeerObj = window.kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly;
       }
 
       this.webRtcPeers[id] = new WebRtcPeerObj(options, (error) => {
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
index 1d882823322bf83f60dfe02d48ed241cf944718b..9ca8ebb0e5c2041a48c1534df3931b4ee626e50d 100755
--- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -7,6 +7,7 @@ import VoiceUsers from '/imports/api/voice-users';
 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';
 
 const MEDIA = Meteor.settings.public.media;
 const MEDIA_TAG = MEDIA.mediaTag;
@@ -130,9 +131,10 @@ class AudioManager {
       .then(() => this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)));
   }
 
-  joinListenOnly(retries = 0) {
+  async joinListenOnly(retries = 0) {
     this.isListenOnly = true;
     this.isEchoTest = false;
+    const { name } = browser();
     // The kurento bridge isn't a full audio bridge yet, so we have to differ it
     const bridge  = this.useKurento? this.listenOnlyBridge : this.bridge;
 
@@ -142,18 +144,24 @@ class AudioManager {
       inputStream: this.createListenOnlyStream(),
     };
 
+    // Webkit ICE restrictions demand a capture device permission to release
+    // host candidates
+    if (name == 'safari') {
+      await this.askDevicesPermissions();
+    }
+
     // We need this until we upgrade to SIP 9x. See #4690
     const iceGatheringErr = 'ICE_TIMEOUT';
     const iceGatheringTimeout = new Promise((resolve, reject) => {
       setTimeout(reject, 12000, iceGatheringErr);
     });
 
-    const handleIceGatheringError = (err) => {
+    const handleListenOnlyError = async (err) => {
       if (iceGatheringTimeout) {
         clearTimeout(iceGatheringTimeout);
       }
 
-      logger.error('Listen only error:', err);
+      logger.error('Listen only error:', err, 'on try', retries);
       const error = {
         type: 'MEDIA_ERROR',
         message: this.messages.error.MEDIA_ERROR,
@@ -167,19 +175,23 @@ class AudioManager {
         iceGatheringTimeout,
       ]))
       .catch(async (err) => {
-        // If theres a iceGathering timeout we retry to join until MAX_LISTEN_ONLY_RETRIES
-        if (err === iceGatheringErr && retries < MAX_LISTEN_ONLY_RETRIES) {
+        if (retries < MAX_LISTEN_ONLY_RETRIES) {
           // Fallback to SIP.js listen only in case of failure
           if (this.useKurento) {
+            // Exit previous SFU session and clean audio tag state
+            window.kurentoExitAudio();
             this.useKurento = false;
+            let audio = document.querySelector(MEDIA_TAG);
+            audio.muted = false;
           }
+
           try {
             await this.joinListenOnly(++retries);
           } catch (err) {
-            return handleIceGatheringError(err);
+            return handleListenOnlyError(err);
           }
         } else {
-          handleIceGatheringError(err);
+          handleListenOnlyError(err);
         }
       });
   }
@@ -239,9 +251,6 @@ class AudioManager {
     if (!this.isEchoTest) {
       this.notify(this.messages.info.JOINED_AUDIO);
     }
-
-    // Restore the default listen only bridge
-    this.useKurento = Meteor.settings.public.kurento.enableListenOnly;
   }
 
   onTransferStart() {
@@ -264,9 +273,6 @@ class AudioManager {
     if (!this.error && !this.isEchoTest) {
       this.notify(this.messages.info.LEFT_AUDIO);
     }
-
-    // Restore the default listen only bridge
-    this.useKurento = Meteor.settings.public.kurento.enableListenOnly;
   }
 
   callStateCallback(response) {
diff --git a/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js b/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js
index 8997af06d0b77288cdf28bcbcc369eb8a8b90680..54d8eebd3d32633dfc053af49b31fc96ed51e8e4 100755
--- a/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js
+++ b/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js
@@ -21,6 +21,7 @@ module.exports = class AudioManager extends BaseManager {
     this._meetings = {};
     this._trackMeetingTermination();
     this.messageFactory(this._onMessage);
+    this._iceQueues = {};
   }
 
   _trackMeetingTermination () {
@@ -50,15 +51,12 @@ module.exports = class AudioManager extends BaseManager {
 
   async _onMessage(message) {
     Logger.debug(this._logPrefix, 'Received message [' + message.id + '] from connection', message.connectionId);
-    let session;
-
     let sessionId = message.voiceBridge;
     let voiceBridge = sessionId;
     let connectionId = message.connectionId;
 
-    if(this._sessions[sessionId]) {
-      session = this._sessions[sessionId];
-    }
+    let session = this._fetchSession(sessionId);
+    let iceQueue = this._fetchIceQueue(sessionId+connectionId);
 
     switch (message.id) {
       case 'start':
@@ -76,6 +74,9 @@ module.exports = class AudioManager extends BaseManager {
           const sdpAnswer = await session.start(sessionId, connectionId, sdpOffer, caleeName, userId, userName);
           Logger.info(this._logPrefix, "Started presenter ", sessionId, " for connection", connectionId);
 
+          // Empty the ICE queue
+          this._flushIceQueue(session, iceQueue);
+
           session.once(C.MEDIA_SERVER_OFFLINE, async (event) => {
             const errorMessage = this._handleError(this._logPrefix, connectionId, caleeName, C.RECV_ROLE, errors.MEDIA_SERVER_OFFLINE);
             errorMessage.id = 'webRTCAudioError';
@@ -117,12 +118,13 @@ module.exports = class AudioManager extends BaseManager {
           session.onIceCandidate(message.candidate, connectionId);
         } else {
           Logger.warn(this._logPrefix, "There was no audio session for onIceCandidate for", sessionId, ". There should be a queue here");
+          iceQueue.push(message.candidate);
         }
         break;
 
       case 'close':
         Logger.info(this._logPrefix, 'Connection ' + connectionId + ' closed');
-
+        this._deleteIceQueue(sessionId+connectionId);
         if (typeof session !== 'undefined') {
           Logger.info(this._logPrefix, "Stopping viewer " + sessionId);
           session.stopListener(message.connectionId);
diff --git a/labs/bbb-webrtc-sfu/lib/base/BaseManager.js b/labs/bbb-webrtc-sfu/lib/base/BaseManager.js
index 8a88e671fc1cc671d87eaa75a8399a0f8d405ecc..ee18a1e0e9043c586fb7a64d90c6b8ac39f73a7f 100644
--- a/labs/bbb-webrtc-sfu/lib/base/BaseManager.js
+++ b/labs/bbb-webrtc-sfu/lib/base/BaseManager.js
@@ -64,6 +64,12 @@ module.exports = class BaseManager {
     }
   }
 
+  _deleteIceQueue (sessionId) {
+    if (this._iceQueues[sessionId]) {
+      delete this._iceQueues[sessionId];
+    }
+  }
+
   _killConnectionSessions (connectionId) {
     const keys = Object.keys(this._sessions);
     keys.forEach((sessionId) => {