/* global __filename, $, $iq, Strophe */ import async from 'async'; import { getLogger } from 'jitsi-meet-logger'; import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler'; import JingleSession from './JingleSession'; import SDP from './SDP'; import SDPDiffer from './SDPDiffer'; import SDPUtil from './SDPUtil'; import SignalingLayerImpl from './SignalingLayerImpl'; import Statistics from '../statistics/statistics'; import XMPPEvents from '../../service/xmpp/XMPPEvents'; import * as JingleSessionState from './JingleSessionState'; const logger = getLogger(__filename); /** * Constant tells how long we're going to wait for IQ response, before timeout * error is triggered. * @type {number} */ const IQ_TIMEOUT = 10000; /** * */ export default class JingleSessionPC extends JingleSession { /* eslint-disable max-params */ /** * Creates new JingleSessionPC * @param {string} sid the Jingle Session ID - random string which * identifies the session * @param {string} me our JID * @param {string} peerjid remote peer JID * @param {Strophe.Connection} connection Strophe XMPP connection instance * used to send packets. * @param mediaConstraints the media constraints object passed to * createOffer/Answer, as defined by the WebRTC standard * @param iceConfig the ICE servers config object as defined by the WebRTC * standard. * @param {boolean} isP2P indicates whether this instance is * meant to be used in a direct, peer to peer connection or false * if it's a JVB connection. * @param {boolean} isInitiator indicates whether or not we are the side * which sends the 'session-intiate'. * @param {object} options a set of config options * @param {boolean} options.webrtcIceUdpDisable true to block UDP * candidates. * @param {boolean} options.webrtcIceTcpDisable true to block TCP * candidates. * @param {boolean} options.failICE it's an option used in the tests. Set to * true to block any real candidates and make the ICE fail. * * @constructor * * @implements {SignalingLayer} */ constructor( sid, me, peerjid, connection, mediaConstraints, iceConfig, isP2P, isInitiator, options) { super(sid, me, peerjid, connection, mediaConstraints, iceConfig); /** * Stores result of {@link window.performance.now()} at the time when * ICE enters 'checking' state. * @type {number|null} null if no value has been stored yet * @private */ this._iceCheckingStartedTimestamp = null; /** * Stores result of {@link window.performance.now()} at the time when * first ICE candidate is spawned by the peerconnection to mark when * ICE gathering started. That's, because ICE gathering state changed * events are not supported by most of the browsers, so we try something * that will work everywhere. It may not be as accurate, but given that * 'host' candidate usually comes first, the delay should be minimal. * @type {number|null} null if no value has been stored yet * @private */ this._gatheringStartedTimestamp = null; /** * Marks that ICE gathering duration has been reported already. That * prevents reporting it again, after eventual 'transport-replace' (JVB * conference migration/ICE restart). * @type {boolean} * @private */ this._gatheringReported = false; this.lasticecandidate = false; this.closed = false; /** * Indicates whether this instance is an initiator or an answerer of * the Jingle session. * @type {boolean} */ this.isInitiator = isInitiator; /** * Indicates whether or not this JingleSessionPC is used in * a peer to peer type of session. * @type {boolean} true if it's a peer to peer * session or false if it's a JVB session */ this.isP2P = isP2P; /** * Stores a state for * {@link TraceablePeerConnection.mediaTransferActive} until * {@link JingleSessionPC.peerconnection} is initialised and capable of * handling the value. * @type {boolean} * @private */ this.mediaTransferActive = true; /** * The signaling layer implementation. * @type {SignalingLayerImpl} */ this.signalingLayer = new SignalingLayerImpl(); this.webrtcIceUdpDisable = Boolean(options.webrtcIceUdpDisable); this.webrtcIceTcpDisable = Boolean(options.webrtcIceTcpDisable); /** * Flag used to enforce ICE failure through the URL parameter for * the automatic testing purpose. * @type {boolean} */ this.failICE = Boolean(options.failICE); this.modificationQueue = async.queue(this._processQueueTasks.bind(this), 1); /** * This is the MUC JID which will be used to add "owner" extension to * each of the local SSRCs signaled over Jingle. * Usually those are added automatically by Jicofo, but it is not * involved in a P2P session. * @type {string} */ this.ssrcOwnerJid = null; /** * Flag used to guarantee that the connection established event is * triggered just once. * @type {boolean} */ this.wasConnected = false; } /** * Checks whether or not this session instance has been ended and eventually * logs a message which mentions that given actionName was * cancelled. * @param {string} actionName * @return {boolean} true if this {@link JingleSessionPC} has * entered {@link JingleSessionState.ENDED} or false otherwise. * @private */ _assertNotEnded(actionName) { if (this.state === JingleSessionState.ENDED) { logger.log( `The session has ended - cancelling action: ${actionName}`); return false; } return true; } /** * Finds all "source" elements under RTC "description" in given Jingle IQ * and adds 'ssrc-info' with the owner attribute set to * {@link ssrcOwnerJid}. * @param jingleIq the IQ to be modified * @private */ _markAsSSRCOwner(jingleIq) { $(jingleIq).find('description source') .append( '`); } /** * Sets the JID which will be as an owner value for the local SSRCs * signaled over Jingle. Should be our MUC JID. * @param {string} ownerJid */ setSSRCOwnerJid(ownerJid) { this.ssrcOwnerJid = ownerJid; } /* eslint-enable max-params */ /** * */ doInitialize() { this.lasticecandidate = false; // True if reconnect is in progress this.isreconnect = false; // Set to true if the connection was ever stable this.wasstable = false; // Create new peer connection instance this.peerconnection = this.rtc.createPeerConnection( this.signalingLayer, this.iceConfig, this.isP2P, { disableSimulcast: this.room.options.disableSimulcast, disableRtx: this.room.options.disableRtx, preferH264: this.room.options.preferH264 }); this.peerconnection.setMediaTransferActive(this.mediaTransferActive); this.peerconnection.onicecandidate = ev => { if (!ev) { // There was an incomplete check for ev before which left // the last line of the function unprotected from a potential // throw of an exception. Consequently, it may be argued that // the check is unnecessary. Anyway, I'm leaving it and making // the check complete. return; } // XXX this is broken, candidate is not parsed. const candidate = ev.candidate; const now = window.performance.now(); if (candidate) { if (this._gatheringStartedTimestamp === null) { this._gatheringStartedTimestamp = now; } // Discard candidates of disabled protocols. let protocol = candidate.protocol; if (typeof protocol === 'string') { protocol = protocol.toLowerCase(); if (protocol === 'tcp' || protocol === 'ssltcp') { if (this.webrtcIceTcpDisable) { return; } } else if (protocol === 'udp') { if (this.webrtcIceUdpDisable) { return; } } } } else if (!this._gatheringReported) { // End of gathering let eventName = this.isP2P ? 'p2p.ice.' : 'ice.'; eventName += this.isInitiator ? 'initiator' : 'responder'; eventName += '.gatheringDuration'; Statistics.analytics.sendEvent( eventName, { value: now - this._gatheringStartedTimestamp }); this._gatheringReported = true; } this.sendIceCandidate(candidate); }; // Note there is a change in the spec about closed: // This value moved into the RTCPeerConnectionState enum in // the May 13, 2016 draft of the specification, as it reflects the state // of the RTCPeerConnection, not the signaling connection. You now // detect a closed connection by checking for connectionState to be // "closed" instead. // I suppose at some point this will be moved to onconnectionstatechange this.peerconnection.onsignalingstatechange = () => { if (!this.peerconnection) { return; } if (this.peerconnection.signalingState === 'stable') { this.wasstable = true; } else if ( (this.peerconnection.signalingState === 'closed' || this.peerconnection.connectionState === 'closed') && !this.closed) { this.room.eventEmitter.emit(XMPPEvents.SUSPEND_DETECTED, this); } }; /** * The oniceconnectionstatechange event handler contains the code to * execute when the iceconnectionstatechange event, of type Event, * is received by this RTCPeerConnection. Such an event is sent when * the value of RTCPeerConnection.iceConnectionState changes. */ this.peerconnection.oniceconnectionstatechange = () => { if (!this.peerconnection || !this._assertNotEnded('oniceconnectionstatechange')) { return; } const now = window.performance.now(); if (!this.isP2P) { this.room.connectionTimes[ `ice.state.${this.peerconnection.iceConnectionState}`] = now; } logger.log( `(TIME) ICE ${this.peerconnection.iceConnectionState}` + ` P2P? ${this.isP2P}:\t`, now); Statistics.analytics.sendEvent( `${this.isP2P ? 'p2p.ice.' : 'ice.'}` + `${this.peerconnection.iceConnectionState}`, { value: now }); this.room.eventEmitter.emit( XMPPEvents.ICE_CONNECTION_STATE_CHANGED, this, this.peerconnection.iceConnectionState); switch (this.peerconnection.iceConnectionState) { case 'checking': this._iceCheckingStartedTimestamp = now; break; case 'connected': // Informs interested parties that the connection has been // restored. if (this.peerconnection.signalingState === 'stable') { if (this.isreconnect) { this.room.eventEmitter.emit( XMPPEvents.CONNECTION_RESTORED, this); } } if (!this.wasConnected && this.wasstable) { let eventName = this.isP2P ? 'p2p.ice.' : 'ice.'; eventName += this.isInitiator ? 'initiator.' : 'responder.'; Statistics.analytics.sendEvent( `${eventName}checksDuration`, { value: now - this._iceCheckingStartedTimestamp }); // Switch between ICE gathering and ICE checking whichever // started first (scenarios are different for initiator // vs responder) const iceStarted = Math.min( this._iceCheckingStartedTimestamp, this._gatheringStartedTimestamp); Statistics.analytics.sendEvent( `${eventName}establishmentDuration`, { value: now - iceStarted }); this.wasConnected = true; this.room.eventEmitter.emit( XMPPEvents.CONNECTION_ESTABLISHED, this); } this.isreconnect = false; break; case 'disconnected': if (this.closed) { break; } this.isreconnect = true; // Informs interested parties that the connection has been // interrupted. if (this.wasstable) { this.room.eventEmitter.emit( XMPPEvents.CONNECTION_INTERRUPTED, this); } break; case 'failed': this.room.eventEmitter.emit( XMPPEvents.CONNECTION_ICE_FAILED, this); this.room.eventEmitter.emit( XMPPEvents.CONFERENCE_SETUP_FAILED, this, new Error('ICE fail')); break; } }; this.peerconnection.onnegotiationneeded = () => { this.room.eventEmitter.emit(XMPPEvents.PEERCONNECTION_READY, this); }; // The signaling layer will bind it's listeners at this point this.signalingLayer.setChatRoom(this.room); } /** * Sends given candidate in Jingle 'transport-info' message. * @param {RTCIceCandidate} candidate the WebRTC ICE candidate instance * @private */ sendIceCandidate(candidate) { const localSDP = new SDP(this.peerconnection.localDescription.sdp); if (candidate && !this.lasticecandidate) { const ice = SDPUtil.iceparams( localSDP.media[candidate.sdpMLineIndex], localSDP.session); const jcand = SDPUtil.candidateToJingle(candidate.candidate); if (!(ice && jcand)) { const errorMesssage = 'failed to get ice && jcand'; GlobalOnErrorHandler.callErrorHandler(new Error(errorMesssage)); logger.error(errorMesssage); return; } ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1'; if (this.usedrip) { if (this.dripContainer.length === 0) { // start 20ms callout setTimeout(() => { if (this.dripContainer.length === 0) { return; } this.sendIceCandidates(this.dripContainer); this.dripContainer = []; }, 20); } this.dripContainer.push(candidate); } else { this.sendIceCandidates([ candidate ]); } } else { logger.log('sendIceCandidate: last candidate.'); // FIXME: remember to re-think in ICE-restart this.lasticecandidate = true; } } /** * Sends given candidates in Jingle 'transport-info' message. * @param {Array} candidates an array of the WebRTC ICE * candidate instances * @private */ sendIceCandidates(candidates) { if (!this._assertNotEnded('sendIceCandidates')) { return; } logger.log('sendIceCandidates', candidates); const cand = $iq({ to: this.peerjid, type: 'set' }) .c('jingle', { xmlns: 'urn:xmpp:jingle:1', action: 'transport-info', initiator: this.initiator, sid: this.sid }); const localSDP = new SDP(this.peerconnection.localDescription.sdp); for (let mid = 0; mid < localSDP.media.length; mid++) { const cands = candidates.filter(el => el.sdpMLineIndex === mid); const mline = SDPUtil.parseMLine(localSDP.media[mid].split('\r\n')[0]); if (cands.length > 0) { const ice = SDPUtil.iceparams(localSDP.media[mid], localSDP.session); ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1'; cand.c('content', { creator: this.initiator === this.localJid ? 'initiator' : 'responder', name: cands[0].sdpMid ? cands[0].sdpMid : mline.media }).c('transport', ice); for (let i = 0; i < cands.length; i++) { const candidate = SDPUtil.candidateToJingle(cands[i].candidate); // Mangle ICE candidate if 'failICE' test option is enabled if (this.failICE) { candidate.ip = '1.1.1.1'; } cand.c('candidate', candidate).up(); } // add fingerprint const fingerprintLine = SDPUtil.findLine( localSDP.media[mid], 'a=fingerprint:', localSDP.session); if (fingerprintLine) { const tmp = SDPUtil.parseFingerprint(fingerprintLine); tmp.required = true; cand.c( 'fingerprint', { xmlns: 'urn:xmpp:jingle:apps:dtls:0' }) .t(tmp.fingerprint); delete tmp.fingerprint; cand.attrs(tmp); cand.up(); } cand.up(); // transport cand.up(); // content } } // might merge last-candidate notification into this, but it is called // a lot later. See webrtc issue #2340 // logger.log('was this the last candidate', this.lasticecandidate); this.connection.sendIQ( cand, null, this.newJingleErrorHandler(cand, error => { GlobalOnErrorHandler.callErrorHandler( new Error(`Jingle error: ${JSON.stringify(error)}`)); }), IQ_TIMEOUT); } /** * {@inheritDoc} */ addIceCandidates(elem) { if (this.peerconnection.signalingState === 'closed') { logger.warn('Ignored add ICE candidate when in closed state'); return; } const iceCandidates = []; elem.find('>content>transport>candidate') .each((idx, candidate) => { let line = SDPUtil.candidateFromJingle(candidate); line = line.replace('\r\n', '').replace('a=', ''); // FIXME this code does not care to handle // non-bundle transport const rtcCandidate = new RTCIceCandidate({ sdpMLineIndex: 0, // FF comes up with more complex names like audio-23423, // Given that it works on both Chrome and FF without // providing it, let's leave it like this for the time // being... // sdpMid: 'audio', candidate: line }); iceCandidates.push(rtcCandidate); }); if (!iceCandidates.length) { logger.error( 'No ICE candidates to add ?', elem[0] && elem[0].outerHTML); return; } // We want to have this task queued, so that we know it is executed, // after the initial sRD/sLD offer/answer cycle was done (based on // the assumption that candidates are spawned after the offer/answer // and XMPP preserves order). const workFunction = finishedCallback => { for (const iceCandidate of iceCandidates) { this.peerconnection.addIceCandidate( iceCandidate, () => { logger.debug('addIceCandidate ok!'); }, error => { logger.error('addIceCandidate failed!', error); }); } finishedCallback(); }; logger.debug( `Queued add (${iceCandidates.length}) ICE candidates task...`); this.modificationQueue.push(workFunction); } /** * * @param contents */ readSsrcInfo(contents) { $(contents).each((i1, content) => { const ssrcs = $(content).find( 'description>' + 'source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); ssrcs.each((i2, ssrcElement) => { const ssrc = Number(ssrcElement.getAttribute('ssrc')); $(ssrcElement) .find('>ssrc-info[xmlns="http://jitsi.org/jitmeet"]') .each((i3, ssrcInfoElement) => { const owner = ssrcInfoElement.getAttribute('owner'); if (owner && owner.length) { if (isNaN(ssrc) || ssrc < 0) { logger.warn( `Invalid SSRC ${ssrc} value received` + ` for ${owner}`); } else { this.signalingLayer.setSSRCOwner( ssrc, Strophe.getResourceFromJid(owner)); } } } ); }); }); } /** * Makes the underlying TraceablePeerConnection generate new SSRC for * the recvonly video stream. * @deprecated */ generateRecvonlySsrc() { if (this.peerconnection) { this.peerconnection.generateRecvonlySsrc(); } else { logger.error( 'Unable to generate recvonly SSRC - no peerconnection'); } } /* eslint-disable max-params */ /** * Accepts incoming Jingle 'session-initiate' and should send * 'session-accept' in result. * @param jingleOffer jQuery selector pointing to the jingle element of * the offer IQ * @param success callback called when we accept incoming session * successfully and receive RESULT packet to 'session-accept' sent. * @param failure function(error) called if for any reason we fail to accept * the incoming offer. 'error' argument can be used to log some details * about the error. * @param {Array} [localTracks] the optional list of * the local tracks that will be added, before the offer/answer cycle * executes. We allow the localTracks to optionally be passed in so that * the addition of the local tracks and the processing of the initial offer * can all be done atomically. We want to make sure that any other * operations which originate in the XMPP Jingle messages related with * this session to be executed with an assumption that the initial * offer/answer cycle has been executed already. */ acceptOffer(jingleOffer, success, failure, localTracks) { this.setOfferAnswerCycle( jingleOffer, () => { // FIXME we may not care about RESULT packet for session-accept // then we should either call 'success' here immediately or // modify sendSessionAccept method to do that this.sendSessionAccept(success, failure); }, failure, localTracks); } /* eslint-enable max-params */ /** * Creates an offer and sends Jingle 'session-initiate' to the remote peer. * @param {Array} localTracks the local tracks that will be * added, before the offer/answer cycle executes (for the local track * addition to be an atomic operation together with the offer/answer). */ invite(localTracks) { if (!this.isInitiator) { throw new Error('Trying to invite from the responder session'); } const workFunction = finishedCallback => { for (const localTrack of localTracks) { this.peerconnection.addTrack(localTrack); } this.peerconnection.createOffer( sdp => { this.sendSessionInitiate( sdp, finishedCallback, finishedCallback ); }, error => { logger.error( 'Failed to create an offer', error, this.mediaConstraints); finishedCallback(error); }, this.mediaConstraints); }; this.modificationQueue.push( workFunction, error => { if (error) { logger.error('invite error', error); } else { logger.debug('invite executed - OK'); } }); } /** * Sends 'session-initiate' to the remote peer. * @param {object} sdp the local session description object as defined by * the WebRTC standard. * @param {function} success executed when the operation succeeds. * @param {function(error)} failure executed when the operation fails with * an error passed as an argument. * @private */ sendSessionInitiate(sdp, success, failure) { logger.log('createdOffer', sdp); const sendJingle = () => { let init = $iq({ to: this.peerjid, type: 'set' }).c('jingle', { xmlns: 'urn:xmpp:jingle:1', action: 'session-initiate', initiator: this.initiator, sid: this.sid }); const localSDP = new SDP(this.peerconnection.localDescription.sdp); localSDP.toJingle( init, this.initiator === this.me ? 'initiator' : 'responder'); init = init.tree(); this._markAsSSRCOwner(init); logger.info('Session-initiate: ', init); this.connection.sendIQ(init, () => { logger.info('Got RESULT for "session-initiate"'); }, error => { logger.error('"session-initiate" error', error); }, IQ_TIMEOUT); // NOTE the callback is executed immediately as we don't want to // wait for the XMPP response which would delay the startup process. success(); }; this.peerconnection.setLocalDescription( sdp, sendJingle, error => { logger.error('session-init setLocalDescription failed', error); failure(error); } ); } /** * Sets the answer received from the remote peer. * @param jingleAnswer */ setAnswer(jingleAnswer) { if (!this.isInitiator) { throw new Error('Trying to set an answer on the responder session'); } this.setOfferAnswerCycle( jingleAnswer, () => { logger.info('setAnswer - succeeded'); }, error => { logger.error('setAnswer failed: ', error); }); } /* eslint-disable max-params */ /** * This is a setRemoteDescription/setLocalDescription cycle which starts at * converting Strophe Jingle IQ into remote offer SDP. Once converted * setRemoteDescription, createAnswer and setLocalDescription calls follow. * @param jingleOfferAnswerIq jQuery selector pointing to the jingle element * of the offer (or answer) IQ * @param success callback called when sRD/sLD cycle finishes successfully. * @param failure callback called with an error object as an argument if we * fail at any point during setRD, createAnswer, setLD. * @param {Array} [localTracks] the optional list of * the local tracks that will be added, before the offer/answer cycle * executes (for the local track addition to be an atomic operation together * with the offer/answer). */ setOfferAnswerCycle(jingleOfferAnswerIq, success, failure, localTracks) { const workFunction = finishedCallback => { if (localTracks) { for (const track of localTracks) { this.peerconnection.addTrack(track); } } const newRemoteSdp = this._processNewJingleOfferIq(jingleOfferAnswerIq); const oldLocalSdp = this.peerconnection.localDescription.sdp; this._renegotiate(newRemoteSdp.raw) .then(() => { if (this.state === JingleSessionState.PENDING) { this.state = JingleSessionState.ACTIVE; } // Old local SDP will be available when we're setting answer // for the first time, but not when offer and it's fine // since we're generating an answer now it will contain all // our SSRCs if (oldLocalSdp) { const newLocalSdp = new SDP(this.peerconnection.localDescription.sdp); this.notifyMySSRCUpdate( new SDP(oldLocalSdp), newLocalSdp); } finishedCallback(); }, error => { logger.error( `Error renegotiating after setting new remote ${ (this.isInitiator ? 'answer: ' : 'offer: ') }${error}`, newRemoteSdp); JingleSessionPC.onJingleFatalError(this, error); finishedCallback(error); }); }; this.modificationQueue.push( workFunction, error => { error ? failure(error) : success(); }); } /* eslint-enable max-params */ /** * Although it states "replace transport" it does accept full Jingle offer * which should contain new ICE transport details. * @param jingleOfferElem an element Jingle IQ that contains new offer and * transport info. * @param success callback called when we succeed to accept new offer. * @param failure function(error) called when we fail to accept new offer. */ replaceTransport(jingleOfferElem, success, failure) { // We need to first set an offer without the 'data' section to have the // SCTP stack cleaned up. After that the original offer is set to have // the SCTP connection established with the new bridge. this.room.eventEmitter.emit(XMPPEvents.ICE_RESTARTING, this); const originalOffer = jingleOfferElem.clone(); jingleOfferElem.find('>content[name=\'data\']').remove(); // First set an offer without the 'data' section this.setOfferAnswerCycle( jingleOfferElem, () => { // Now set the original offer(with the 'data' section) this.setOfferAnswerCycle( originalOffer, () => { const localSDP = new SDP(this.peerconnection.localDescription.sdp); this.sendTransportAccept(localSDP, success, failure); }, failure); }, failure ); } /** * Sends Jingle 'session-accept' message. * @param {function()} success callback called when we receive 'RESULT' * packet for the 'session-accept' * @param {function(error)} failure called when we receive an error response * or when the request has timed out. * @private */ sendSessionAccept(success, failure) { // NOTE: since we're just reading from it, we don't need to be within // the modification queue to access the local description const localSDP = new SDP(this.peerconnection.localDescription.sdp); let accept = $iq({ to: this.peerjid, type: 'set' }) .c('jingle', { xmlns: 'urn:xmpp:jingle:1', action: 'session-accept', initiator: this.initiator, responder: this.responder, sid: this.sid }); if (this.webrtcIceTcpDisable) { localSDP.removeTcpCandidates = true; } if (this.webrtcIceUdpDisable) { localSDP.removeUdpCandidates = true; } if (this.failICE) { localSDP.failICE = true; } localSDP.toJingle( accept, this.initiator === this.localJid ? 'initiator' : 'responder', null); // Calling tree() to print something useful accept = accept.tree(); this._markAsSSRCOwner(accept); logger.info('Sending session-accept', accept); this.connection.sendIQ(accept, success, this.newJingleErrorHandler(accept, error => { failure(error); // 'session-accept' is a critical timeout and we'll // have to restart this.room.eventEmitter.emit( XMPPEvents.SESSION_ACCEPT_TIMEOUT, this); }), IQ_TIMEOUT); // XXX Videobridge needs WebRTC's answer (ICE ufrag and pwd, DTLS // fingerprint and setup) ASAP in order to start the connection // establishment. // // FIXME Flushing the connection at this point triggers an issue with // BOSH request handling in Prosody on slow connections. // // The problem is that this request will be quite large and it may take // time before it reaches Prosody. In the meantime Strophe may decide // to send the next one. And it was observed that a small request with // 'transport-info' usually follows this one. It does reach Prosody // before the previous one was completely received. 'rid' on the server // is increased and Prosody ignores the request with 'session-accept'. // It will never reach Jicofo and everything in the request table is // lost. Removing the flush does not guarantee it will never happen, but // makes it much less likely('transport-info' is bundled with // 'session-accept' and any immediate requests). // // this.connection.flush(); } /** * Sends Jingle 'transport-accept' message which is a response to * 'transport-replace'. * @param localSDP the 'SDP' object with local session description * @param success callback called when we receive 'RESULT' packet for * 'transport-replace' * @param failure function(error) called when we receive an error response * or when the request has timed out. * @private */ sendTransportAccept(localSDP, success, failure) { let transportAccept = $iq({ to: this.peerjid, type: 'set' }) .c('jingle', { xmlns: 'urn:xmpp:jingle:1', action: 'transport-accept', initiator: this.initiator, sid: this.sid }); localSDP.media.forEach((medialines, idx) => { const mline = SDPUtil.parseMLine(medialines.split('\r\n')[0]); transportAccept.c('content', { creator: this.initiator === this.localJid ? 'initiator' : 'responder', name: mline.media } ); localSDP.transportToJingle(idx, transportAccept); transportAccept.up(); }); // Calling tree() to print something useful to the logger transportAccept = transportAccept.tree(); logger.info('Sending transport-accept: ', transportAccept); this.connection.sendIQ(transportAccept, success, this.newJingleErrorHandler(transportAccept, failure), IQ_TIMEOUT); } /** * Sends Jingle 'transport-reject' message which is a response to * 'transport-replace'. * @param success callback called when we receive 'RESULT' packet for * 'transport-replace' * @param failure function(error) called when we receive an error response * or when the request has timed out. * * FIXME method should be marked as private, but there's some spaghetti that * needs to be fixed prior doing that */ sendTransportReject(success, failure) { // Send 'transport-reject', so that the focus will // know that we've failed let transportReject = $iq({ to: this.peerjid, type: 'set' }) .c('jingle', { xmlns: 'urn:xmpp:jingle:1', action: 'transport-reject', initiator: this.initiator, sid: this.sid }); transportReject = transportReject.tree(); logger.info('Sending \'transport-reject', transportReject); this.connection.sendIQ(transportReject, success, this.newJingleErrorHandler(transportReject, failure), IQ_TIMEOUT); } /** * @inheritDoc */ terminate(success, failure, options) { if (this.state === JingleSessionState.ENDED) { return; } if (!options || Boolean(options.sendSessionTerminate)) { let sessionTerminate = $iq({ to: this.peerjid, type: 'set' }) .c('jingle', { xmlns: 'urn:xmpp:jingle:1', action: 'session-terminate', initiator: this.initiator, sid: this.sid }) .c('reason') .c((options && options.reason) || 'success'); if (options && options.reasonDescription) { sessionTerminate.up() .c('text') .t(options.reasonDescription); } // Calling tree() to print something useful sessionTerminate = sessionTerminate.tree(); logger.info('Sending session-terminate', sessionTerminate); this.connection.sendIQ( sessionTerminate, success, this.newJingleErrorHandler(sessionTerminate, failure), IQ_TIMEOUT); } else { logger.info(`Skipped sending session-terminate for ${this}`); } // this should result in 'onTerminated' being called by strope.jingle.js this.connection.jingle.terminate(this.sid); } /** * * @param reasonCondition * @param reasonText */ onTerminated(reasonCondition, reasonText) { this.state = JingleSessionState.ENDED; // Do something with reason and reasonCondition when we start to care // this.reasonCondition = reasonCondition; // this.reasonText = reasonText; logger.info(`Session terminated ${this}`, reasonCondition, reasonText); this.close(); } /** * Parse the information from the xml sourceAddElem and translate it * into sdp lines * @param {jquery xml element} sourceAddElem the source-add * element from jingle * @param {SDP object} currentRemoteSdp the current remote * sdp (as of this new source-add) * @returns {list} a list of SDP line strings that should * be added to the remote SDP */ _parseSsrcInfoFromSourceAdd(sourceAddElem, currentRemoteSdp) { const addSsrcInfo = []; $(sourceAddElem).each((i1, content) => { const name = $(content).attr('name'); let lines = ''; $(content) .find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]') .each(function() { // eslint-disable-next-line no-invalid-this const semantics = this.getAttribute('semantics'); const ssrcs = $(this) // eslint-disable-line no-invalid-this .find('>source') .map(function() { // eslint-disable-next-line no-invalid-this return this.getAttribute('ssrc'); }) .get(); if (ssrcs.length) { lines += `a=ssrc-group:${semantics} ${ssrcs.join(' ') }\r\n`; } }); // handles both >source and >description>source const tmp = $(content).find( 'source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); /* eslint-disable no-invalid-this */ tmp.each(function() { const ssrc = $(this).attr('ssrc'); if (currentRemoteSdp.containsSSRC(ssrc)) { logger.warn( `Source-add request for existing SSRC: ${ssrc}`); return; } // eslint-disable-next-line newline-per-chained-call $(this).find('>parameter').each(function() { lines += `a=ssrc:${ssrc} ${$(this).attr('name')}`; if ($(this).attr('value') && $(this).attr('value').length) { lines += `:${$(this).attr('value')}`; } lines += '\r\n'; }); }); /* eslint-enable no-invalid-this */ currentRemoteSdp.media.forEach((media, i2) => { if (!SDPUtil.findLine(media, `a=mid:${name}`)) { return; } if (!addSsrcInfo[i2]) { addSsrcInfo[i2] = ''; } addSsrcInfo[i2] += lines; }); }); return addSsrcInfo; } /** * Handles a Jingle source-add message for this Jingle session. * @param elem An array of Jingle "content" elements. */ addRemoteStream(elem) { this._addOrRemoveRemoteStream(true /* add */, elem); } /** * Handles a Jingle source-remove message for this Jingle session. * @param elem An array of Jingle "content" elements. */ removeRemoteStream(elem) { this._addOrRemoveRemoteStream(false /* remove */, elem); } /** * Handles either Jingle 'source-add' or 'source-remove' message for this * Jingle session. * @param {boolean} isAdd true for 'source-add' or false * otherwise. * @param {Array} elem an array of Jingle "content" elements. * @private */ _addOrRemoveRemoteStream(isAdd, elem) { const logPrefix = isAdd ? 'addRemoteStream' : 'removeRemoteStream'; if (isAdd) { this.readSsrcInfo(elem); } const workFunction = finishedCallback => { if (!this.peerconnection.localDescription || !this.peerconnection.localDescription.sdp) { const errMsg = `${logPrefix} - localDescription not ready yet`; logger.error(errMsg); finishedCallback(errMsg); return; } logger.log(`Processing ${logPrefix}`); logger.log( 'ICE connection state: ', this.peerconnection.iceConnectionState); const oldLocalSdp = new SDP(this.peerconnection.localDescription.sdp); const sdp = new SDP(this.peerconnection.remoteDescription.sdp); const addOrRemoveSsrcInfo = isAdd ? this._parseSsrcInfoFromSourceAdd(elem, sdp) : this._parseSsrcInfoFromSourceRemove(elem, sdp); const newRemoteSdp = isAdd ? this._processRemoteAddSource(addOrRemoveSsrcInfo) : this._processRemoteRemoveSource(addOrRemoveSsrcInfo); this._renegotiate(newRemoteSdp.raw) .then(() => { const newLocalSdp = new SDP(this.peerconnection.localDescription.sdp); logger.log( `${logPrefix} - OK, SDPs: `, oldLocalSdp, newLocalSdp); this.notifyMySSRCUpdate(oldLocalSdp, newLocalSdp); finishedCallback(); }, error => { logger.error(`${logPrefix} failed:`, error); finishedCallback(error); }); }; // Queue and execute this.modificationQueue.push(workFunction); } /** * The 'task' function will be given a callback it MUST call with either: * 1) No arguments if it was successful or * 2) An error argument if there was an error * If the task wants to process the success or failure of the task, it * should pass a handler to the .push function, e.g.: * queue.push(task, (err) => { * if (err) { * // error handling * } else { * // success handling * } * }); */ _processQueueTasks(task, finishedCallback) { task(finishedCallback); } /** * Takes in a jingle offer iq, returns the new sdp offer * @param {jquery xml element} offerIq the incoming offer * @returns {SDP object} the jingle offer translated to SDP */ _processNewJingleOfferIq(offerIq) { const remoteSdp = new SDP(''); if (this.webrtcIceTcpDisable) { remoteSdp.removeTcpCandidates = true; } if (this.webrtcIceUdpDisable) { remoteSdp.removeUdpCandidates = true; } if (this.failICE) { remoteSdp.failICE = true; } remoteSdp.fromJingle(offerIq); this.readSsrcInfo($(offerIq).find('>content')); return remoteSdp; } /** * Remove the given ssrc lines from the current remote sdp * @param {list} removeSsrcInfo a list of SDP line strings that * should be removed from the remote SDP * @returns type {SDP Object} the new remote SDP (after removing the lines * in removeSsrcInfo */ _processRemoteRemoveSource(removeSsrcInfo) { const remoteSdp = new SDP(this.peerconnection.remoteDescription.sdp); removeSsrcInfo.forEach((lines, idx) => { // eslint-disable-next-line no-param-reassign lines = lines.split('\r\n'); lines.pop(); // remove empty last element; lines.forEach(line => { remoteSdp.media[idx] = remoteSdp.media[idx].replace(`${line}\r\n`, ''); }); }); remoteSdp.raw = remoteSdp.session + remoteSdp.media.join(''); return remoteSdp; } /** * Add the given ssrc lines to the current remote sdp * @param {list} addSsrcInfo a list of SDP line strings that * should be added to the remote SDP * @returns type {SDP Object} the new remote SDP (after removing the lines * in removeSsrcInfo */ _processRemoteAddSource(addSsrcInfo) { const remoteSdp = new SDP(this.peerconnection.remoteDescription.sdp); addSsrcInfo.forEach((lines, idx) => { remoteSdp.media[idx] += lines; }); remoteSdp.raw = remoteSdp.session + remoteSdp.media.join(''); return remoteSdp; } /** * Do a new o/a flow using the existing remote description * @param {string} [optionalRemoteSdp] optional, raw remote sdp * to use. If not provided, the remote sdp from the * peerconnection will be used * @returns {Promise} promise which resolves when the * o/a flow is complete with no arguments or * rejects with an error {string} */ _renegotiate(optionalRemoteSdp) { const remoteSdp = optionalRemoteSdp || this.peerconnection.remoteDescription.sdp; if (!remoteSdp) { return Promise.reject( 'Can not renegotiate without remote description,' + `- current state: ${this.state}`); } const remoteDescription = new RTCSessionDescription({ type: this.isInitiator ? 'answer' : 'offer', sdp: remoteSdp }); return new Promise((resolve, reject) => { if (this.peerconnection.signalingState === 'closed') { reject('Attempted to renegotiate in state closed'); return; } if (this.isInitiator) { this._initiatorRenegotiate(remoteDescription, resolve, reject); } else { this._responderRenegotiate(remoteDescription, resolve, reject); } }); } /** * Renegotiate cycle implementation for the responder case. * @param {object} remoteDescription the SDP object as defined by the WebRTC * which will be used as remote description in the cycle. * @param {function} resolve the success callback * @param {function} reject the failure callback * @private */ _responderRenegotiate(remoteDescription, resolve, reject) { // FIXME use WebRTC promise API to simplify things logger.debug('Renegotiate: setting remote description'); this.peerconnection.setRemoteDescription( remoteDescription, () => { logger.debug('Renegotiate: creating answer'); this.peerconnection.createAnswer( answer => { logger.debug('Renegotiate: setting local description'); this.peerconnection.setLocalDescription( answer, () => { resolve(); }, error => { reject( `setLocalDescription failed: ${error}`); } ); }, error => reject(`createAnswer failed: ${error}`), this.mediaConstraints ); }, error => reject(`setRemoteDescription failed: ${error}`) ); } /** * Renegotiate cycle implementation for the initiator's case. * @param {object} remoteDescription the SDP object as defined by the WebRTC * which will be used as remote description in the cycle. * @param {function} resolve the success callback * @param {function} reject the failure callback * @private */ _initiatorRenegotiate(remoteDescription, resolve, reject) { // FIXME use WebRTC promise API to simplify things if (this.peerconnection.signalingState === 'have-local-offer') { // Skip createOffer and setLocalDescription or FF will fail logger.debug( 'Renegotiate: setting remote description'); this.peerconnection.setRemoteDescription( remoteDescription, () => { resolve(); }, error => reject(`setRemoteDescription failed: ${error}`) ); } else { logger.debug('Renegotiate: creating offer'); this.peerconnection.createOffer( offer => { logger.debug('Renegotiate: setting local description'); this.peerconnection.setLocalDescription(offer, () => { logger.debug( 'Renegotiate: setting remote description'); this.peerconnection.setRemoteDescription( remoteDescription, () => { resolve(); }, error => reject( `setRemoteDescription failed: ${error}`) ); }, error => { reject('setLocalDescription failed: ', error); }); }, error => reject(`createOffer failed: ${error}`), this.mediaConstraints); } } /** * Replaces oldTrack with newTrack and performs a single * offer/answer cycle after both operations are done. Either * oldTrack or newTrack can be null; replacing a valid * oldTrack with a null newTrack effectively just removes * oldTrack * @param {JitsiLocalTrack|null} oldTrack the current track in use to be * replaced * @param {JitsiLocalTrack|null} newTrack the new track to use * @returns {Promise} which resolves once the replacement is complete * with no arguments or rejects with an error {string} */ replaceTrack(oldTrack, newTrack) { const workFunction = finishedCallback => { const oldLocalSdp = this.peerconnection.localDescription.sdp; // NOTE the code below assumes that no more than 1 video track // can be added to the peer connection. // Transition from no video to video (possibly screen sharing) if (!oldTrack && newTrack && newTrack.isVideoTrack()) { // Clearing current primary SSRC will make // the SdpConsistency generate a new one which will result // with: // 1. source-remove for the recvonly // 2. source-add for the new video stream this.peerconnection.clearRecvonlySsrc(); // Transition from video to no video } else if (oldTrack && oldTrack.isVideoTrack() && !newTrack) { // Clearing current primary SSRC and generating the recvonly // will result in: // 1. source-remove for the old video stream // 2. source-add for the recvonly stream this.peerconnection.clearRecvonlySsrc(); this.peerconnection.generateRecvonlySsrc(); } if (oldTrack) { this.peerconnection.removeTrack(oldTrack); } if (newTrack) { this.peerconnection.addTrack(newTrack); } if ((oldTrack || newTrack) && this.state === JingleSessionState.ACTIVE) { this._renegotiate() .then(() => { const newLocalSDP = new SDP( this.peerconnection.localDescription.sdp); this.notifyMySSRCUpdate( new SDP(oldLocalSdp), newLocalSDP); finishedCallback(); }, finishedCallback /* will be called with en error */); } else { finishedCallback(); } }; this.modificationQueue.push( workFunction, error => { if (error) { logger.error('Replace track error:', error); } else { logger.info('Replace track done!'); } }); } /** * Parse the information from the xml sourceRemoveElem and translate it * into sdp lines * @param {jquery xml element} sourceRemoveElem the source-remove * element from jingle * @param {SDP object} currentRemoteSdp the current remote * sdp (as of this new source-remove) * @returns {list} a list of SDP line strings that should * be removed from the remote SDP */ _parseSsrcInfoFromSourceRemove(sourceRemoveElem, currentRemoteSdp) { const removeSsrcInfo = []; $(sourceRemoveElem).each((i1, content) => { const name = $(content).attr('name'); let lines = ''; $(content) .find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]') .each(function() { /* eslint-disable no-invalid-this */ const semantics = this.getAttribute('semantics'); const ssrcs = $(this) .find('>source') .map(function() { return this.getAttribute('ssrc'); }) .get(); if (ssrcs.length) { lines += `a=ssrc-group:${semantics} ${ssrcs.join(' ') }\r\n`; } /* eslint-enable no-invalid-this */ }); const ssrcs = []; // handles both >source and >description>source versions const tmp = $(content).find( 'source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); tmp.each(function() { // eslint-disable-next-line no-invalid-this const ssrc = $(this).attr('ssrc'); ssrcs.push(ssrc); }); currentRemoteSdp.media.forEach((media, i2) => { if (!SDPUtil.findLine(media, `a=mid:${name}`)) { return; } if (!removeSsrcInfo[i2]) { removeSsrcInfo[i2] = ''; } ssrcs.forEach(ssrc => { const ssrcLines = SDPUtil.findLines(media, `a=ssrc:${ssrc}`); if (ssrcLines.length) { removeSsrcInfo[i2] += `${ssrcLines.join('\r\n')}\r\n`; } }); removeSsrcInfo[i2] += lines; }); }); return removeSsrcInfo; } /** * Will print an error if there is any difference, between the SSRCs given * in the oldSDP and the ones currently described in * the peerconnection's local description. * @param {string} operationName the operation's name which will be printed * in the error message. * @param {SDP} oldSDP the old local SDP which will be compared with * the current one. * @return {boolean} true if there was any change or false * otherwise. * @private */ _verifyNoSSRCChanged(operationName, oldSDP) { const currentLocalSDP = new SDP(this.peerconnection.localDescription.sdp); let sdpDiff = new SDPDiffer(oldSDP, currentLocalSDP); const addedMedia = sdpDiff.getNewMedia(); if (Object.keys(addedMedia).length) { logger.error( `Some SSRC were added on ${operationName}`, addedMedia); return false; } sdpDiff = new SDPDiffer(currentLocalSDP, oldSDP); const removedMedia = sdpDiff.getNewMedia(); if (Object.keys(removedMedia).length) { logger.error( `Some SSRCs were removed on ${operationName}`, removedMedia); return false; } return true; } /** * Adds local track back to this session, as part of the unmute operation. * @param {JitsiLocalTrack} track * @return {Promise} a promise that will resolve once the local track is * added back to this session and renegotiation succeeds. Will be rejected * with a string that provides some error details in case something * goes wrong. */ addTrackAsUnmute(track) { return this._addRemoveTrackAsMuteUnmute( false /* add as unmute */, track); } /** * Remove local track as part of the mute operation. * @param {JitsiLocalTrack} track the local track to be removed * @return {Promise} a promise which will be resolved once the local track * is removed from this session and the renegotiation is performed. * The promise will be rejected with a string that the describes * the error if anything goes wrong. */ removeTrackAsMute(track) { return this._addRemoveTrackAsMuteUnmute( true /* remove as mute */, track); } /** * See {@link addTrackAsUnmute} and {@link removeTrackAsMute}. * @param {boolean} isMute true for "remove as mute" or * false for "add as unmute". * @param {JitsiLocalTrack} track the track that will be added/removed * @private */ _addRemoveTrackAsMuteUnmute(isMute, track) { if (!track) { return Promise.reject('invalid "track" argument value'); } const operationName = isMute ? 'removeTrackMute' : 'addTrackUnmute'; const workFunction = finishedCallback => { const tpc = this.peerconnection; if (!tpc) { finishedCallback( `Error: tried ${operationName} track with no active peer` + 'connection'); return; } const oldLocalSDP = tpc.localDescription.sdp; const tpcOperation = isMute ? tpc.removeTrackMute.bind(tpc, track) : tpc.addTrackUnmute.bind(tpc, track); if (!tpcOperation()) { finishedCallback(`${operationName} failed!`); } else if (!oldLocalSDP || !tpc.remoteDescription.sdp) { finishedCallback(); } else { this._renegotiate() .then(() => { // The results are ignored, as this check failure is not // enough to fail the whole operation. It will log // an error inside. this._verifyNoSSRCChanged( operationName, new SDP(oldLocalSDP)); finishedCallback(); }, finishedCallback /* will be called with an error */); } }; return new Promise((resolve, reject) => { this.modificationQueue.push( workFunction, error => { if (error) { reject(error); } else { resolve(); } }); }); } /** * Resumes or suspends media transfer over the underlying peer connection. * @param {boolean} active true to enable media transfer or * false to suspend any media transmission. * @return {Promise} a Promise which will resolve once * the operation is done. It will be rejected with an error description as * a string in case anything goes wrong. */ setMediaTransferActive(active) { const workFunction = finishedCallback => { // Because the value is modified on the queue it's impossible to // check it's final value reliably prior to submitting the task. // The rule here is that the last submitted state counts. // Check the value here to avoid unnecessary renegotiation cycle. if (this.mediaTransferActive === active) { finishedCallback(); return; } this.mediaTransferActive = active; if (this.peerconnection) { this.peerconnection.setMediaTransferActive( this.mediaTransferActive); // Will do the sRD/sLD cycle to update SDPs and adjust the media // direction this._renegotiate() .then( finishedCallback, finishedCallback /* will be called with an error */); } else { finishedCallback(); } }; const logStr = active ? 'active' : 'inactive'; logger.info(`Queued make media transfer ${logStr} task...`); return new Promise((resolve, reject) => { this.modificationQueue.push( workFunction, error => { if (error) { reject(error); } else { resolve(); } }); }); } /** * Figures out added/removed ssrcs and send update IQs. * @param oldSDP SDP object for old description. * @param newSDP SDP object for new description. */ notifyMySSRCUpdate(oldSDP, newSDP) { if (this.state !== JingleSessionState.ACTIVE) { logger.warn(`Skipping SSRC update in '${this.state} ' state.`); return; } // send source-remove IQ. let sdpDiffer = new SDPDiffer(newSDP, oldSDP); const remove = $iq({ to: this.peerjid, type: 'set' }) .c('jingle', { xmlns: 'urn:xmpp:jingle:1', action: 'source-remove', initiator: this.initiator, sid: this.sid } ); const removedAnySSRCs = sdpDiffer.toJingle(remove); if (removedAnySSRCs) { logger.info('Sending source-remove', remove.tree()); this.connection.sendIQ( remove, null, this.newJingleErrorHandler(remove, error => { GlobalOnErrorHandler.callErrorHandler( new Error(`Jingle error: ${JSON.stringify(error)}`)); }), IQ_TIMEOUT); } else { logger.log('removal not necessary'); } // send source-add IQ. sdpDiffer = new SDPDiffer(oldSDP, newSDP); const add = $iq({ to: this.peerjid, type: 'set' }) .c('jingle', { xmlns: 'urn:xmpp:jingle:1', action: 'source-add', initiator: this.initiator, sid: this.sid } ); const containsNewSSRCs = sdpDiffer.toJingle(add); if (containsNewSSRCs) { logger.info('Sending source-add', add.tree()); this.connection.sendIQ( add, null, this.newJingleErrorHandler(add, error => { GlobalOnErrorHandler.callErrorHandler( new Error(`Jingle error: ${JSON.stringify(error)}`)); }), IQ_TIMEOUT); } else { logger.log('addition not necessary'); } } /** * Method returns function(errorResponse) which is a callback to be passed * to Strophe connection.sendIQ method. An 'error' structure is created that * is passed as 1st argument to given failureCb. The format of this * structure is as follows: * { * code: {XMPP error response code} * reason: {the name of XMPP error reason element or 'timeout' if the * request has timed out within IQ_TIMEOUT milliseconds} * source: {request.tree() that provides original request} * session: {JingleSessionPC instance on which the error occurred} * } * @param request Strophe IQ instance which is the request to be dumped into * the error structure * @param failureCb function(error) called when error response was returned * or when a timeout has occurred. * @returns {function(this:JingleSessionPC)} */ newJingleErrorHandler(request, failureCb) { return function(errResponse) { const error = {}; // Get XMPP error code and condition(reason) const errorElSel = $(errResponse).find('error'); if (errorElSel.length) { error.code = errorElSel.attr('code'); const errorReasonSel = $(errResponse).find('error :first'); if (errorReasonSel.length) { error.reason = errorReasonSel[0].tagName; } } if (!errResponse) { error.reason = 'timeout'; } error.source = request; if (request && typeof request.tree === 'function') { error.source = request.tree(); } if (error.source && error.source.outerHTML) { error.source = error.source.outerHTML; } // Commented to fix JSON.stringify(error) exception for circular // dependancies when we print that error. // FIXME: Maybe we can include part of the session object // error.session = this; if (failureCb) { failureCb(error); } else { logger.error('Jingle error', error); } }; } /** * * @param session * @param error */ static onJingleFatalError(session, error) { if (this.room) { this.room.eventEmitter.emit( XMPPEvents.CONFERENCE_SETUP_FAILED, session, error); this.room.eventEmitter.emit( XMPPEvents.JINGLE_FATAL_ERROR, session, error); } } /** * Returns the ice connection state for the peer connection. * @returns the ice connection state for the peer connection. */ getIceConnectionState() { return this.peerconnection.iceConnectionState; } /** * Closes the peerconnection. */ close() { this.closed = true; // The signaling layer will remove it's listeners this.signalingLayer.setChatRoom(null); // do not try to close if already closed. this.peerconnection && ((this.peerconnection.signalingState && this.peerconnection.signalingState !== 'closed') || (this.peerconnection.connectionState && this.peerconnection.connectionState !== 'closed')) && this.peerconnection.close(); } /** * Converts to string with minor summary. * @return {string} */ toString() { return `JingleSessionPC[p2p=${this.isP2P},` + `initiator=${this.isInitiator},sid=${this.sid}]`; } }