diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index 3fd0794733df3dc357f3997f9375d5501340911d..e3bf03f8866591699996783e6f00f7f39bfd4519 100644 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -2,23 +2,24 @@ import VoiceUsers from '/imports/api/voice-users'; import { Tracker } from 'meteor/tracker'; import BaseAudioBridge from './base'; -const STUN_TURN_FETCH_URL = Meteor.settings.public.media.stunTurnServersFetchAddress; -const MEDIA_TAG = Meteor.settings.public.media.mediaTag; -const CALL_TRANSFER_TIMEOUT = Meteor.settings.public.media.callTransferTimeout; - -const handleStunTurnResponse = ({ result, stunServers, turnServers }) => - new Promise((resolve) => { - if (result) { - resolve({ error: 404, stun: [], turn: [] }); - } - resolve({ - stun: stunServers.map(server => server.url), - turn: turnServers.map(server => server.url), - }); - }); +const MEDIA = Meteor.settings.public.media; +const STUN_TURN_FETCH_URL = MEDIA.stunTurnServersFetchAddress; +const MEDIA_TAG = MEDIA.mediaTag; +const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout; const fetchStunTurnServers = sessionToken => new Promise(async (resolve, reject) => { + const handleStunTurnResponse = ({ result, stunServers, turnServers }) => + new Promise((resolve) => { + if (result) { + resolve({ error: 404, stun: [], turn: [] }); + } + resolve({ + stun: stunServers.map(server => server.url), + turn: turnServers.map(server => server.url), + }); + }); + const url = `${STUN_TURN_FETCH_URL}?sessionToken=${sessionToken}`; const response = await fetch(url) @@ -29,34 +30,28 @@ const fetchStunTurnServers = sessionToken => return resolve(response); }); -const inviteUserAgent = (voiceBridge, server, userAgent, inputStream) => { - const options = { - media: { - stream: inputStream, - constraints: { - audio: true, - video: false, - }, - render: { - remote: document.querySelector(MEDIA_TAG), - }, - }, - RTCConstraints: { - mandatory: { - OfferToReceiveAudio: true, - OfferToReceiveVideo: false, - }, - }, - }; - - return userAgent.invite(`sip:${voiceBridge}@${server}`, options); -}; - export default class SIPBridge extends BaseAudioBridge { constructor(userData) { super(userData); - this.isConnected = false; + const { + userId, + username, + sessionToken, + } = userData; + + this.user = { + userId, + sessionToken, + name: username, + }; + + this.media = { + inputDevice: {}, + }; + + this.protocol = window.document.location.protocol; + this.hostname = window.document.location.hostname; this.errorCodes = { 'Request Timeout': this.baseErrorCodes.REQUEST_TIMEOUT, 'Invalid Target': this.baseErrorCodes.INVALID_TARGET, @@ -82,16 +77,27 @@ export default class SIPBridge extends BaseAudioBridge { }); } - transferCall(onTransferStart, onTransferSuccess) { + transferCall(onTransferSuccess) { return new Promise((resolve, reject) => { - onTransferStart(); - this.currentSession.dtmf(1); let trackerControl = null; + + const timeout = setTimeout(() => { + clearTimeout(timeout); + trackerControl.stop(); + this.callback({ + status: this.baseCallStates.failed, + error: this.baseErrorCodes.REQUEST_TIMEOUT, + bridgeError: 'Timeout on call transfer' }); + reject('Timeout on call transfer'); + }, CALL_TRANSFER_TIMEOUT); + + // This is is the call transfer code ask @chadpilkey + this.currentSession.dtmf(1); + Tracker.autorun((c) => { trackerControl = c; const selector = { meetingId: this.userData.meetingId, intId: this.userData.userId }; const query = VoiceUsers.find(selector); - window.Kappa = query; query.observeChanges({ changed: (id, fields) => { @@ -104,16 +110,6 @@ export default class SIPBridge extends BaseAudioBridge { }, }); }); - - const timeout = setTimeout(() => { - clearTimeout(timeout); - trackerControl.stop(); - this.callback({ - status: this.baseCallStates.failed, - error: this.baseErrorCodes.REQUEST_TIMEOUT, - bridgeError: 'Could not transfer the call' }); - reject('Call transfer timeout'); - }, CALL_TRANSFER_TIMEOUT); }); } @@ -126,42 +122,50 @@ export default class SIPBridge extends BaseAudioBridge { }); } - doCall({ isListenOnly, callExtension, inputStream }, callback) { + doCall(options) { + const { + isListenOnly, + } = options; + const { userId, - username, + name, sessionToken, - } = this.userData; - - const server = window.document.location.hostname; + } = this.user; - const callerIdPrefix = userId; - const callerIdSufix = isListenOnly ? `LINSTENONLY-${username}` : username; const callerIdName = [ - callerIdPrefix, + userId, 'bbbID', - callerIdSufix, + isListenOnly ? `LISTENONLY-${name}` : name, ].join('-'); + this.user.callerIdName = callerIdName; + this.callOptions = options; + return fetchStunTurnServers(sessionToken) - .then(stunTurnServers => - this.createUserAgent(server, callerIdName, stunTurnServers)) - .then(userAgent => - inviteUserAgent(callExtension, server, userAgent, inputStream)) - .then(currentSession => - this.setupEventHandlers(currentSession, callback)); + .then(this.createUserAgent.bind(this)) + .then(this.inviteUserAgent.bind(this)) + .then(this.setupEventHandlers.bind(this)); } - createUserAgent(server, username, { stun, turn }) { + createUserAgent({ stun, turn }) { return new Promise((resolve, reject) => { - const protocol = document.location.protocol; - this.userAgent = new window.SIP.UA({ - uri: `sip:${encodeURIComponent(username)}@${server}`, - wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${server}/ws`, - // log: { - // builtinEnabled: false, - // }, - displayName: username, + const { + hostname, + protocol, + } = this; + + const { + callerIdName, + } = this.user; + + let userAgent = new window.SIP.UA({ + uri: `sip:${encodeURIComponent(callerIdName)}@${hostname}`, + wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws`, + log: { + builtinEnabled: false, + }, + displayName: callerIdName, register: false, traceSip: true, autostart: false, @@ -170,55 +174,149 @@ export default class SIPBridge extends BaseAudioBridge { turnServers: turn, }); - this.userAgent.removeAllListeners('connected'); - this.userAgent.removeAllListeners('disconnected'); + userAgent.removeAllListeners('connected'); + userAgent.removeAllListeners('disconnected'); + + const handleUserAgentConnection = () => { + resolve(userAgent); + }; + + const handleUserAgentDisconnection = () => { + userAgent.stop(); + userAgent = null; + this.callback({ + status: this.baseCallStates.failed, + error: this.baseErrorCodes.GENERIC_ERROR, + bridgeError: 'User Agent' }); + reject('CONNECTION_ERROR'); + }; - this.userAgent.on('connected', () => this.handleUserAgentConnection(resolve)); - this.userAgent.on('disconnected', () => this.handleUserAgentDisconnection(reject)); + userAgent.on('connected', handleUserAgentConnection); + userAgent.on('disconnected', handleUserAgentDisconnection); - this.userAgent.start(); + userAgent.start(); }); } - handleUserAgentConnection(resolve) { - this.isConnected = true; - resolve(this.userAgent); - } + inviteUserAgent(userAgent) { + const { + hostname, + } = this; + + const { + inputStream, + callExtension, + } = this.callOptions; + + const options = { + media: { + stream: inputStream, + constraints: { + audio: true, + video: false, + }, + render: { + remote: document.querySelector(MEDIA_TAG), + }, + }, + RTCConstraints: { + mandatory: { + OfferToReceiveAudio: true, + OfferToReceiveVideo: false, + }, + }, + }; - handleUserAgentDisconnection(reject) { - this.userAgent.stop(); - this.userAgent = null; - this.callback({ - status: this.baseCallStates.failed, - error: this.baseErrorCodes.GENERIC_ERROR, - bridgeError: 'User Agent' }); - reject('CONNECTION_ERROR'); + return userAgent.invite(`sip:${callExtension}@${hostname}`, options); } - setupEventHandlers(currentSession, callback) { + setupEventHandlers(currentSession) { return new Promise((resolve) => { - currentSession.on('terminated', (message, cause) => this.handleSessionTerminated(message, cause, callback)); + const handleConnectionCompleted = () => { + this.callback({ status: this.baseCallStates.started }); + resolve(); + }; + + const handleSessionTerminated = (message, cause) => { + if (!message && !cause) { + return this.callback({ + status: this.baseCallStates.ended, + }); + } + + const mappedCause = cause in this.errorCodes ? + this.errorCodes[cause] : + this.baseErrorCodes.GENERIC_ERROR; + return this.callback({ + status: this.baseCallStates.failed, + error: mappedCause, + bridgeError: cause, + }); + }; - currentSession.mediaHandler.on('iceConnectionCompleted', () => this.handleConnectionCompleted(resolve)); - currentSession.mediaHandler.on('iceConnectsionConnected', () => this.handleConnectionCompleted(resolve)); + currentSession.on('terminated', handleSessionTerminated); + currentSession.mediaHandler.on('iceConnectionCompleted', handleConnectionCompleted); + currentSession.mediaHandler.on('iceConnectsionConnected', handleConnectionCompleted); this.currentSession = currentSession; }); } - handleConnectionCompleted(resolve) { - this.callback({ status: this.baseCallStates.started }); - resolve(); - } + async changeInputDevice(value) { + const { + media, + } = this; + + const getMediaStream = constraints => + navigator.mediaDevices.getUserMedia(constraints); + + if (!value) { + const mediaStream = await getMediaStream({ audio: true }); + const deviceLabel = mediaStream.getAudioTracks()[0].label; + const mediaDevices = await navigator.mediaDevices.enumerateDevices(); + const device = mediaDevices.find(d => d.label === deviceLabel); + return this.changeInputDevice(device.deviceId); + } + + if (media.inputDevice.audioContext) { + media.inputDevice.audioContext.close().then(() => { + media.inputDevice.audioContext = null; + media.inputDevice.scriptProcessor = null; + media.inputDevice.source = null; + return this.changeInputDevice(value); + }); + } - handleSessionTerminated(message, cause, callback) { - if (!message && !cause) { - return callback({ status: this.baseCallStates.ended }); + media.inputDevice.id = value; + if ('AudioContext' in window) { + media.inputDevice.audioContext = new window.AudioContext(); + } else { + media.inputDevice.audioContext = new window.webkitAudioContext(); } + media.inputDevice.scriptProcessor = media.inputDevice.audioContext + .createScriptProcessor(2048, 1, 1); + media.inputDevice.source = null; + + const constraints = { + audio: { + deviceId: value, + }, + }; - const mappedCause = cause in this.errorCodes ? - this.errorCodes[cause] : - this.baseErrorCodes.GENERIC_ERROR; - return callback({ status: this.baseCallStates.failed, error: mappedCause, bridgeError: cause }); + const mediaStream = await getMediaStream(constraints); + media.inputDevice.stream = mediaStream; + media.inputDevice.source = media.inputDevice.audioContext.createMediaStreamSource(mediaStream); + media.inputDevice.source.connect(media.inputDevice.scriptProcessor); + media.inputDevice.scriptProcessor.connect(media.inputDevice.audioContext.destination); + + return this.media.inputDevice; + } + + changeOutputDevice(value) { + const audioContext = document.querySelector(MEDIA_TAG); + + if (audioContext.setSinkId) { + audioContext.setSinkId(deviceId); + } } } diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index c86f0506eabc4ca7dbc9b737c55fc7e1c12cfdc6..b82aaf2edbe132cbc1a5c3e35ece03457fe14b0c 100644 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -28,13 +28,14 @@ const propTypes = { actionsbar: PropTypes.node, media: PropTypes.node, location: PropTypes.object.isRequired, - children: PropTypes.node.isRequired, + children: PropTypes.node, }; const defaultProps = { navbar: <NavBarContainer />, actionsbar: <ActionsBarContainer />, media: <MediaContainer />, + children: null, }; const intlMessages = defineMessages({ diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx index e22a170d95b019b0cae15535978253089a9ff559..27d11d6302d0e06e17f3c5af452fcdee54f83380 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx @@ -11,8 +11,8 @@ export default withModalMounter(createContainer(({ mountModal }) => closeModal: () => { if (!Service.isConnecting()) mountModal(null); }, - joinMicrophone: () => { - return new Promise((resolve, reject) => { + joinMicrophone: () => + new Promise((resolve, reject) => { Service.transferCall().then(() => { mountModal(null); resolve(); @@ -20,8 +20,7 @@ export default withModalMounter(createContainer(({ mountModal }) => Service.exitAudio(); reject(); }); - }); - }, + }), joinListenOnly: () => Service.joinListenOnly().then(() => mountModal(null)), leaveEchoTest: () => { if (!Service.isEchoTest()) { diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index 99e2e551ca0f4ca20303470fcfd4e416a0364c96..2086e464af7df9c188c7f72c2d74252879c37d57 100644 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -3,8 +3,9 @@ import { makeCall } from '/imports/ui/services/api'; import VertoBridge from '/imports/api/audio/client/bridge/verto'; import SIPBridge from '/imports/api/audio/client/bridge/sip'; -const USE_SIP = Meteor.settings.public.media.useSIPAudio; -const OUTPUT_TAG = Meteor.settings.public.media.mediaTag; +const MEDIA = Meteor.settings.public.media; +const USE_SIP = MEDIA.useSIPAudio; +const ECHO_TEST_NUMBER = MEDIA.echoTestNumber; const CALL_STATES = { STARTED: 'started', @@ -18,17 +19,6 @@ class AudioManager { tracker: new Tracker.Dependency(), }; - navigator.mediaDevices - .getUserMedia({ audio: true }) - .then((stream) => { - const deviceLabel = stream.getAudioTracks()[0].label; - navigator.mediaDevices.enumerateDevices().then((devices) => { - const device = devices.find(d => d.label === deviceLabel); - this.changeInputDevice(device.deviceId); - }); - }).catch((err) => { this.error = err; }); - - this.defineProperties({ isMuted: false, isConnected: false, @@ -40,6 +30,13 @@ class AudioManager { }); } + init(userData) { + this.bridge = USE_SIP ? new SIPBridge(userData) : new VertoBridge(userData); + this.userData = userData; + + this.changeInputDevice(); + } + defineProperties(obj) { Object.keys(obj).forEach((key) => { const privateKey = `_${key}`; @@ -61,11 +58,6 @@ class AudioManager { }); } - init(userData) { - this.bridge = USE_SIP ? new SIPBridge(userData) : new VertoBridge(userData); - this.userData = userData; - } - joinAudio(options = {}, callbacks = {}) { const { isListenOnly, @@ -73,6 +65,7 @@ class AudioManager { } = options; this.isConnecting = true; + this.isMuted = false; this.error = null; this.isListenOnly = isListenOnly || false; this.isEchoTest = isEchoTest || false; @@ -80,22 +73,20 @@ class AudioManager { const callOptions = { isListenOnly: this.isListenOnly, - extension: isEchoTest ? '9196' : null, + extension: isEchoTest ? ECHO_TEST_NUMBER : null, inputStream: this.isListenOnly ? this.createListenOnlyStream() : this.inputStream, }; - // if (this.isListenOnly) makeCall('listenOnlyToggle', true); - return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)); } exitAudio() { - console.log('LOL'); return this.bridge.exitAudio(); } transferCall() { - return this.bridge.transferCall(this.onTransferStart.bind(this), this.onAudioJoin.bind(this)); + this.onTransferStart(); + return this.bridge.transferCall(this.onAudioJoin.bind(this)); } toggleMuteMicrophone() { @@ -109,9 +100,6 @@ class AudioManager { this.isConnected = true; } - if (this.isListenOnly) makeCall('listenOnlyToggle', true); - console.log('joined', this.isListenOnly); - this.isConnecting = false; } @@ -124,8 +112,6 @@ class AudioManager { this.isConnected = false; this.isConnecting = false; - if (this.isListenOnly) makeCall('listenOnlyToggle', false); - if (this.isEchoTest) { this.isEchoTest = false; } @@ -135,13 +121,6 @@ class AudioManager { this.isMuted = !this.isMuted; } - //--------------------------- - // update(key, value) { - // const query = { _id: this.stateId }; - // const modifier = { $set: { [key]: value }}; - // collection.update(query, modifier); - // } - callStateCallback(response) { return new Promise((resolve) => { const { @@ -161,77 +140,36 @@ class AudioManager { } else if (status === ENDED) { this.onAudioExit(); } else if (status === FAILED) { - console.log('error happened'); this.error = error; this.onAudioExit(); } }); } - set userData(value) { - this._userData = value; - } - - get userData() { - return this._userData; - } - createListenOnlyStream() { if (this.listenOnlyAudioContext) { this.listenOnlyAudioContext.close(); } - if ('webkitAudioContext' in window) { - this.listenOnlyAudioContext = new window.webkitAudioContext(); - } else { - this.listenOnlyAudioContext = new window.AudioContext(); - } + this.listenOnlyAudioContext = window.AudioContext ? + new window.AudioContext() : + new window.webkitAudioContext(); return this.listenOnlyAudioContext.createMediaStreamDestination().stream; } - changeInputDevice(value) { - if (this._inputDevice.audioContext) { - this._inputDevice.audioContext.close().then(() => { - this._inputDevice.audioContext = null; - this._inputDevice.scriptProcessor = null; - this._inputDevice.source = null; - - this.changeInputDevice(value); - }); - return; - } - - this._inputDevice.id = value; - if ('webkitAudioContext' in window) { - this._inputDevice.audioContext = new window.webkitAudioContext(); - } else { - this._inputDevice.audioContext = new AudioContext(); - } - this._inputDevice.scriptProcessor = this._inputDevice.audioContext - .createScriptProcessor(2048, 1, 1); - this._inputDevice.source = null; - - const constraints = { - audio: { - deviceId: value, - }, - }; + async changeInputDevice(deviceId) { + const device = await this.bridge.changeInputDevice(deviceId); + this.inputDevice = device; + } - navigator.mediaDevices - .getUserMedia(constraints) - .then((stream) => { - this._inputDevice.stream = stream; - this._inputDevice.source = this._inputDevice.audioContext.createMediaStreamSource(stream); - this._inputDevice.source.connect(this._inputDevice.scriptProcessor); - this._inputDevice.scriptProcessor.connect(this._inputDevice.audioContext.destination); - this._inputDevice.tracker.changed(); - }); + async changeOutputDevice(deviceId) { + this.outputDeviceId = await this.bridge.changeOutputDevice(deviceId); } - changeOutputDevice(deviceId) { - this.outputDeviceId = deviceId; - document.querySelector(OUTPUT_TAG).setSinkId(deviceId); + set inputDevice(value) { + Object.assign(this._inputDevice, value); + this._inputDevice.tracker.changed(); } get inputStream() { @@ -242,6 +180,14 @@ class AudioManager { this._inputDevice.tracker.depend(); return this._inputDevice.id; } + + set userData(value) { + this._userData = value; + } + + get userData() { + return this._userData; + } } const audioManager = new AudioManager(); diff --git a/bigbluebutton-html5/private/config/public/media.yaml b/bigbluebutton-html5/private/config/public/media.yaml index 124e4aeed3c26a15b717faaa56f215c00d472c25..500be9ecf044482819e4a368f7efe9d569f6873f 100644 --- a/bigbluebutton-html5/private/config/public/media.yaml +++ b/bigbluebutton-html5/private/config/public/media.yaml @@ -11,3 +11,4 @@ media: stunTurnServersFetchAddress: '/bigbluebutton/api/stuns' mediaTag: '#remote-media' callTransferTimeout: 5000 + echoTestNumber: '9196'