diff --git a/JitsiConference.js b/JitsiConference.js index f35209f022..7ff2135101 100644 --- a/JitsiConference.js +++ b/JitsiConference.js @@ -99,10 +99,6 @@ const JINGLE_SI_TIMEOUT = 5000; * "Math.random() < forceJVB121Ratio" will determine whether a 2 people * conference should be moved to the JVB instead of P2P. The decision is made on * the responder side, after ICE succeeds on the P2P connection. - * @param {*} [options.config.openBridgeChannel] Which kind of communication to - * open with the videobridge. Values can be "datachannel", "websocket", true - * (treat it as "datachannel"), undefined (treat it as "datachannel") and false - * (don't open any channel). * @constructor * * FIXME Make all methods which are called from lib-internal classes @@ -942,27 +938,23 @@ JitsiConference.prototype.getTranscriptionStatus = function() { /** * Adds JitsiLocalTrack object to the conference. - * @param track the JitsiLocalTrack object. + * @param {JitsiLocalTrack} track the JitsiLocalTrack object. * @returns {Promise} * @throws {Error} if the specified track is a video track and there is already * another video track in the conference. */ JitsiConference.prototype.addTrack = function(track) { - if (track.isVideoTrack()) { - // Ensure there's exactly 1 local video track in the conference. - const localVideoTrack = this.rtc.getLocalVideoTrack(); - - if (localVideoTrack) { - // Don't be excessively harsh and severe if the API client happens - // to attempt to add the same local video track twice. - if (track === localVideoTrack) { - return Promise.resolve(track); - } - - return Promise.reject(new Error( - 'cannot add second video track to the conference')); - + const mediaType = track.getType(); + const localTracks = this.rtc.getLocalTracks(mediaType); + + // Ensure there's exactly 1 local track of each media type in the conference. + if (localTracks.length > 0) { + // Don't be excessively harsh and severe if the API client happens to attempt to add the same local track twice. + if (track === localTracks[0]) { + return Promise.resolve(track); } + + return Promise.reject(new Error(`Cannot add second ${mediaType} track to the conference`)); } return this.replaceTrack(null, track); @@ -2003,23 +1995,12 @@ JitsiConference.prototype._setBridgeChannel = function(offerIq, pc) { wsUrl = webSocket[0].getAttribute('url'); } - let bridgeChannelType; - - switch (this.options.config.openBridgeChannel) { - case 'datachannel': - case true: - case undefined: - bridgeChannelType = 'datachannel'; - break; - case 'websocket': - bridgeChannelType = 'websocket'; - break; - } - - if (bridgeChannelType === 'datachannel') { - this.rtc.initializeBridgeChannel(pc, null); - } else if (bridgeChannelType === 'websocket' && wsUrl) { + if (wsUrl) { + // If the offer contains a websocket use it. this.rtc.initializeBridgeChannel(null, wsUrl); + } else { + // Otherwise, fall back to an attempt to use SCTP. + this.rtc.initializeBridgeChannel(pc, null); } }; @@ -2860,8 +2841,7 @@ JitsiConference.prototype._updateProperties = function(properties = {}) { 'bridge-count', // The conference creation time (set by jicofo). - 'created-ms', - 'octo-enabled' + 'created-ms' ]; analyticsKeys.forEach(key => { diff --git a/doc/API.md b/doc/API.md index bc66e8b47e..23cdc3515d 100644 --- a/doc/API.md +++ b/doc/API.md @@ -459,6 +459,8 @@ Throws NetworkError or InvalidStateError or Error if the operation fails. - `propertyKey` - string - custom property name - `propertyValue` - string - custom property value +38. `getParticipants()` - Retrieves an array of all participants in this conference. + JitsiTrack ====== The object represents single track - video or audio. They can be remote tracks ( from the other participants in the call) or local tracks (from the devices of the local participant). diff --git a/modules/RTC/RTC.js b/modules/RTC/RTC.js index 8e002038d2..4abcf354ab 100644 --- a/modules/RTC/RTC.js +++ b/modules/RTC/RTC.js @@ -526,6 +526,10 @@ export default class RTC extends Listenable { iceConfig.sdpSemantics = 'plan-b'; } + if (options.forceTurnRelay) { + iceConfig.iceTransportPolicy = 'relay'; + } + // Set the RTCBundlePolicy to max-bundle so that only one set of ice candidates is generated. // The default policy generates separate ice candidates for audio and video connections. // This change is necessary for Unified plan to work properly on Chrome and Safari. diff --git a/modules/RTC/RTCUtils.js b/modules/RTC/RTCUtils.js index 01d5f9f0ee..abf0bf2778 100644 --- a/modules/RTC/RTCUtils.js +++ b/modules/RTC/RTCUtils.js @@ -664,9 +664,9 @@ function onMediaDevicesListChanged(devicesReceived) { sendDeviceListToAnalytics(availableDevices); // Used by tracks to update the real device id before the consumer of lib-jitsi-meet receives the new device list. - eventEmitter.emit(RTCEvents.DEVICE_LIST_WILL_CHANGE, devicesReceived); + eventEmitter.emit(RTCEvents.DEVICE_LIST_WILL_CHANGE, availableDevices); - eventEmitter.emit(RTCEvents.DEVICE_LIST_CHANGED, devicesReceived); + eventEmitter.emit(RTCEvents.DEVICE_LIST_CHANGED, availableDevices); } /** @@ -846,7 +846,7 @@ class RTCUtils extends Listenable { if (this.isDeviceListAvailable()) { this.enumerateDevices(ds => { - availableDevices = ds.splice(0); + availableDevices = ds.slice(0); logger.debug('Available devices: ', availableDevices); sendDeviceListToAnalytics(availableDevices); diff --git a/modules/RTC/TPCUtils.js b/modules/RTC/TPCUtils.js index e50b50e076..fe881bff57 100644 --- a/modules/RTC/TPCUtils.js +++ b/modules/RTC/TPCUtils.js @@ -3,7 +3,6 @@ import transform from 'sdp-transform'; import * as MediaType from '../../service/RTC/MediaType'; import RTCEvents from '../../service/RTC/RTCEvents'; -import VideoType from '../../service/RTC/VideoType'; import browser from '../browser'; const logger = getLogger(__filename); @@ -447,20 +446,18 @@ export class TPCUtils { * @returns {void} */ updateEncodingsResolution(parameters) { - const localVideoTrack = this.pc.getLocalVideoTrack(); - - // Ignore desktop and non-simulcast tracks. - if (!(parameters - && parameters.encodings - && Array.isArray(parameters.encodings) - && this.pc.isSimulcastOn() - && localVideoTrack - && localVideoTrack.videoType !== VideoType.DESKTOP)) { + if (!(browser.isSafari() && parameters.encodings && Array.isArray(parameters.encodings))) { return; } - - parameters.encodings.forEach((encoding, idx) => { - encoding.scaleResolutionDownBy = this.localStreamEncodingsConfig[idx].scaleResolutionDownBy; - }); + const allEqualEncodings + = encodings => encodings.every(encoding => typeof encoding.scaleResolutionDownBy !== 'undefined' + && encoding.scaleResolutionDownBy === encodings[0].scaleResolutionDownBy); + + // Implement the workaround only when all the encodings report the same resolution. + if (allEqualEncodings(parameters.encodings)) { + parameters.encodings.forEach((encoding, idx) => { + encoding.scaleResolutionDownBy = this.localStreamEncodingsConfig[idx].scaleResolutionDownBy; + }); + } } } diff --git a/modules/xmpp/JingleSessionPC.js b/modules/xmpp/JingleSessionPC.js index bd0b4923cd..bba7a3fe03 100644 --- a/modules/xmpp/JingleSessionPC.js +++ b/modules/xmpp/JingleSessionPC.js @@ -331,6 +331,7 @@ export default class JingleSessionPC extends JingleSession { pcOptions.capScreenshareBitrate = false; pcOptions.enableInsertableStreams = options.enableInsertableStreams; pcOptions.videoQuality = options.videoQuality; + pcOptions.forceTurnRelay = options.forceTurnRelay; // codec preference options for jvb connection. if (pcOptions.videoQuality) { diff --git a/modules/xmpp/XmppConnection.js b/modules/xmpp/XmppConnection.js index a91a45ed76..ef25d93f2b 100644 --- a/modules/xmpp/XmppConnection.js +++ b/modules/xmpp/XmppConnection.js @@ -51,6 +51,7 @@ export default class XmppConnection extends Listenable { super(); this._options = { enableWebsocketResume: typeof enableWebsocketResume === 'undefined' ? true : enableWebsocketResume, + pingOptions: xmppPing, websocketKeepAlive: typeof websocketKeepAlive === 'undefined' ? 4 * 60 * 1000 : Number(websocketKeepAlive) }; @@ -174,6 +175,13 @@ export default class XmppConnection extends Listenable { return this._stropheConn.options; } + /** + * A getter for the domain to be used for ping. + */ + get pingDomain() { + return this._options.pingOptions?.domain || this.domain; + } + /** * A getter for the service URL. * @@ -255,7 +263,7 @@ export default class XmppConnection extends Listenable { this._maybeStartWSKeepAlive(); this._processDeferredIQs(); this._resumeTask.cancel(); - this.ping.startInterval(this.domain); + this.ping.startInterval(this._options.pingOptions?.domain || this.domain); } else if (status === Strophe.Status.DISCONNECTED) { this.ping.stopInterval(); diff --git a/modules/xmpp/moderator.js b/modules/xmpp/moderator.js index 9fbfd94064..5cbeaecc7b 100644 --- a/modules/xmpp/moderator.js +++ b/modules/xmpp/moderator.js @@ -55,12 +55,9 @@ export default function Moderator(roomName, xmpp, emitter, options) { this.externalAuthEnabled = false; this.options = options; - // Sip gateway can be enabled by configuring Jigasi host in config.js or - // it will be enabled automatically if focus detects the component through - // service discovery. - this.sipGatewayEnabled - = this.options.connection.hosts - && this.options.connection.hosts.call_control !== undefined; + // Whether SIP gateway (jigasi) support is enabled. This is set + // based on conference properties received in presence. + this.sipGatewayEnabled = false; this.eventEmitter = emitter; @@ -160,63 +157,13 @@ Moderator.prototype.createConferenceIq = function() { if (sessionId) { elem.attrs({ 'session-id': sessionId }); } - if (this.options.connection.enforcedBridge !== undefined) { - elem.c( - 'property', { - name: 'enforcedBridge', - value: this.options.connection.enforcedBridge - }).up(); - } - // Tell the focus we have Jigasi configured - if (this.options.connection.hosts !== undefined - && this.options.connection.hosts.call_control !== undefined) { - elem.c( - 'property', { - name: 'call_control', - value: this.options.connection.hosts.call_control - }).up(); - } elem.c( 'property', { name: 'disableRtx', value: Boolean(config.disableRtx) }).up(); - if (config.enableTcc !== undefined) { - elem.c( - 'property', { - name: 'enableTcc', - value: Boolean(config.enableTcc) - }).up(); - } - if (config.enableRemb !== undefined) { - elem.c( - 'property', { - name: 'enableRemb', - value: Boolean(config.enableRemb) - }).up(); - } - if (config.enableOpusRed === true) { - elem.c( - 'property', { - name: 'enableOpusRed', - value: true - }).up(); - } - if (config.minParticipants !== undefined) { - elem.c( - 'property', { - name: 'minParticipants', - value: config.minParticipants - }).up(); - } - - elem.c( - 'property', { - name: 'enableLipSync', - value: this.options.connection.enableLipSync === true - }).up(); if (config.audioPacketDelay !== undefined) { elem.c( 'property', { @@ -238,35 +185,6 @@ Moderator.prototype.createConferenceIq = function() { value: config.minBitrate }).up(); } - if (config.testing && config.testing.octo - && typeof config.testing.octo.probability === 'number') { - if (Math.random() < config.testing.octo.probability) { - elem.c( - 'property', { - name: 'octo', - value: true - }).up(); - } - } - - let openSctp; - - switch (this.options.conference.openBridgeChannel) { - case 'datachannel': - case true: - case undefined: - openSctp = true; - break; - case 'websocket': - openSctp = false; - break; - } - - elem.c( - 'property', { - name: 'openSctp', - value: openSctp - }).up(); if (config.opusMaxAverageBitrate) { elem.c( @@ -296,13 +214,6 @@ Moderator.prototype.createConferenceIq = function() { value: this.options.conference.stereo }).up(); } - if (this.options.conference.useRoomAsSharedDocumentName !== undefined) { - elem.c( - 'property', { - name: 'useRoomAsSharedDocumentName', - value: this.options.conference.useRoomAsSharedDocumentName - }).up(); - } elem.up(); return elem; @@ -348,8 +259,7 @@ Moderator.prototype.parseConfigOptions = function(resultIq) { this.eventEmitter.emit(AuthenticationEvents.IDENTITY_UPDATED, authenticationEnabled, authIdentity); - // Check if focus has auto-detected Jigasi component(this will be also - // included if we have passed our host from the config) + // Check if jicofo has jigasi support enabled. if ($(resultIq).find( '>conference>property' + '[name=\'sipGatewayEnabled\'][value=\'true\']').length) { diff --git a/modules/xmpp/strophe.jingle.js b/modules/xmpp/strophe.jingle.js index 0d0cd98d9a..fc2193d1c1 100644 --- a/modules/xmpp/strophe.jingle.js +++ b/modules/xmpp/strophe.jingle.js @@ -305,7 +305,7 @@ export default class JingleConnectionPlugin extends ConnectionPlugin { // https://code.google.com/p/webrtc/issues/detail?id=1650 this.connection.sendIQ( $iq({ type: 'get', - to: this.connection.domain }) + to: this.xmpp.options.hosts.domain }) .c('services', { xmlns: 'urn:xmpp:extdisco:1' }), res => { const iceservers = []; diff --git a/modules/xmpp/strophe.ping.js b/modules/xmpp/strophe.ping.js index a03423f604..02abba9bec 100644 --- a/modules/xmpp/strophe.ping.js +++ b/modules/xmpp/strophe.ping.js @@ -25,21 +25,6 @@ const PING_DEFAULT_TIMEOUT = 5000; */ const PING_DEFAULT_THRESHOLD = 2; -/** - * How often to send ping requests. - */ -let pingInterval; - -/** - * The time to wait for ping responses. - */ -let pingTimeout; - -/** - * How many ping failures will be tolerated before the connection is killed. - */ -let pingThreshold; - /** * XEP-0199 ping plugin. * @@ -120,35 +105,37 @@ export default class PingConnectionPlugin extends ConnectionPlugin { // when there were some server responses in the interval since the last time we checked (_lastServerCheck) // let's skip the ping - // server response is measured on raw input and ping response time is measured after all the xmpp - // processing is done, and when the last server response is a ping there can be slight misalignment of the - // times, we give it 100ms for that processing. - if (this._getTimeSinceLastServerResponse() + 100 < new Date() - this._lastServerCheck) { + const now = Date.now(); + + if (this._getTimeSinceLastServerResponse() < now - this._lastServerCheck) { // do this just to keep in sync the intervals so we can detect suspended device this._addPingExecutionTimestamp(); - this._lastServerCheck = new Date(); + this._lastServerCheck = now; this.failedPings = 0; return; } this.ping(remoteJid, () => { - this._lastServerCheck = new Date(); + // server response is measured on raw input and ping response time is measured after all the xmpp + // processing is done in js, so there can be some misalignment when we do the check above. + // That's why we store the last time we got the response + this._lastServerCheck = this._getTimeSinceLastServerResponse() + Date.now(); this.failedPings = 0; }, error => { this.failedPings += 1; const errmsg = `Ping ${error ? 'error' : 'timeout'}`; - if (this.failedPings >= pingThreshold) { + if (this.failedPings >= this.pingThreshold) { GlobalOnErrorHandler.callErrorHandler(new Error(errmsg)); logger.error(errmsg, error); this._onPingThresholdExceeded && this._onPingThresholdExceeded(); } else { logger.warn(errmsg, error); } - }, pingTimeout); + }, this.pingTimeout); }, this.pingInterval); logger.info(`XMPP pings will be sent every ${this.pingInterval} ms`); } @@ -211,7 +198,7 @@ export default class PingConnectionPlugin extends ConnectionPlugin { // remove the interval between the ping sent // this way in normal execution there is no suspend and the return // will be 0 or close to 0. - maxInterval -= pingInterval; + maxInterval -= this.pingInterval; // make sure we do not return less than 0 return Math.max(maxInterval, 0); diff --git a/modules/xmpp/xmpp.js b/modules/xmpp/xmpp.js index 1677b40a59..189c2f1363 100644 --- a/modules/xmpp/xmpp.js +++ b/modules/xmpp/xmpp.js @@ -106,6 +106,11 @@ export default class XMPP extends Listenable { initStropheNativePlugins(); + const xmppPing = options.xmppPing || {}; + + // let's ping the main domain (in case a guest one is used for the connection) + xmppPing.domain = options.hosts.domain; + this.connection = createConnection({ enableWebsocketResume: options.enableWebsocketResume, @@ -113,7 +118,7 @@ export default class XMPP extends Listenable { serviceUrl: options.serviceUrl || options.bosh, token, websocketKeepAlive: options.websocketKeepAlive, - xmppPing: options.xmppPing + xmppPing }); this._initStrophePlugins(); @@ -158,6 +163,13 @@ export default class XMPP extends Listenable { this.caps.addFeature('http://jitsi.org/opus-red'); } + if (typeof this.options.enableRemb === 'undefined' || this.options.enableRemb) { + this.caps.addFeature('http://jitsi.org/remb'); + } + if (typeof this.options.enableTcc === 'undefined' || this.options.enableTcc) { + this.caps.addFeature('http://jitsi.org/tcc'); + } + // this is dealt with by SDP O/A so we don't need to announce this // XEP-0293 // this.caps.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); @@ -220,15 +232,12 @@ export default class XMPP extends Listenable { // XmppConnection emits CONNECTED again on reconnect - a good opportunity to clear any "last error" flags this._resetState(); - // Schedule ping ? - const pingJid = this.connection.domain; - // FIXME no need to do it again on stream resume - this.caps.getFeaturesAndIdentities(pingJid) + this.caps.getFeaturesAndIdentities(this.options.hosts.domain) .then(({ features, identities }) => { if (!features.has(Strophe.NS.PING)) { - logger.error( - `Ping NOT supported by ${pingJid} - please enable ping in your XMPP server config`); + logger.error(`Ping NOT supported by ${ + this.options.hosts.domain} - please enable ping in your XMPP server config`); } // check for speakerstats @@ -531,8 +540,7 @@ export default class XMPP extends Listenable { */ ping(timeout) { return new Promise((resolve, reject) => { - this.connection.ping - .ping(this.connection.domain, resolve, reject, timeout); + this.connection.ping.ping(this.connection.pingDomain, resolve, reject, timeout); }); }