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) => {