You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

JingleSessionPC.js 67KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873
  1. /* global __filename, $, $iq, Strophe */
  2. import async from 'async';
  3. import { getLogger } from 'jitsi-meet-logger';
  4. import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
  5. import JingleSession from './JingleSession';
  6. import SDP from './SDP';
  7. import SDPDiffer from './SDPDiffer';
  8. import SDPUtil from './SDPUtil';
  9. import SignalingLayerImpl from './SignalingLayerImpl';
  10. import Statistics from '../statistics/statistics';
  11. import XMPPEvents from '../../service/xmpp/XMPPEvents';
  12. import * as JingleSessionState from './JingleSessionState';
  13. const logger = getLogger(__filename);
  14. /**
  15. * Constant tells how long we're going to wait for IQ response, before timeout
  16. * error is triggered.
  17. * @type {number}
  18. */
  19. const IQ_TIMEOUT = 10000;
  20. /**
  21. *
  22. */
  23. export default class JingleSessionPC extends JingleSession {
  24. /* eslint-disable max-params */
  25. /**
  26. * Creates new <tt>JingleSessionPC</tt>
  27. * @param {string} sid the Jingle Session ID - random string which
  28. * identifies the session
  29. * @param {string} me our JID
  30. * @param {string} peerjid remote peer JID
  31. * @param {Strophe.Connection} connection Strophe XMPP connection instance
  32. * used to send packets.
  33. * @param mediaConstraints the media constraints object passed to
  34. * createOffer/Answer, as defined by the WebRTC standard
  35. * @param iceConfig the ICE servers config object as defined by the WebRTC
  36. * standard.
  37. * @param {boolean} isP2P indicates whether this instance is
  38. * meant to be used in a direct, peer to peer connection or <tt>false</tt>
  39. * if it's a JVB connection.
  40. * @param {boolean} isInitiator indicates whether or not we are the side
  41. * which sends the 'session-intiate'.
  42. * @param {object} options a set of config options
  43. * @param {boolean} options.webrtcIceUdpDisable <tt>true</tt> to block UDP
  44. * candidates.
  45. * @param {boolean} options.webrtcIceTcpDisable <tt>true</tt> to block TCP
  46. * candidates.
  47. * @param {boolean} options.failICE it's an option used in the tests. Set to
  48. * <tt>true</tt> to block any real candidates and make the ICE fail.
  49. *
  50. * @constructor
  51. *
  52. * @implements {SignalingLayer}
  53. */
  54. constructor(
  55. sid,
  56. me,
  57. peerjid,
  58. connection,
  59. mediaConstraints,
  60. iceConfig,
  61. isP2P,
  62. isInitiator,
  63. options) {
  64. super(sid, me, peerjid, connection, mediaConstraints, iceConfig);
  65. /**
  66. * Stores result of {@link window.performance.now()} at the time when
  67. * ICE enters 'checking' state.
  68. * @type {number|null} null if no value has been stored yet
  69. * @private
  70. */
  71. this._iceCheckingStartedTimestamp = null;
  72. /**
  73. * Stores result of {@link window.performance.now()} at the time when
  74. * first ICE candidate is spawned by the peerconnection to mark when
  75. * ICE gathering started. That's, because ICE gathering state changed
  76. * events are not supported by most of the browsers, so we try something
  77. * that will work everywhere. It may not be as accurate, but given that
  78. * 'host' candidate usually comes first, the delay should be minimal.
  79. * @type {number|null} null if no value has been stored yet
  80. * @private
  81. */
  82. this._gatheringStartedTimestamp = null;
  83. this.lasticecandidate = false;
  84. this.closed = false;
  85. /**
  86. * Indicates whether this instance is an initiator or an answerer of
  87. * the Jingle session.
  88. * @type {boolean}
  89. */
  90. this.isInitiator = isInitiator;
  91. /**
  92. * Indicates whether or not this <tt>JingleSessionPC</tt> is used in
  93. * a peer to peer type of session.
  94. * @type {boolean} <tt>true</tt> if it's a peer to peer
  95. * session or <tt>false</tt> if it's a JVB session
  96. */
  97. this.isP2P = isP2P;
  98. /**
  99. * Stores a state for
  100. * {@link TraceablePeerConnection.mediaTransferActive} until
  101. * {@link JingleSessionPC.peerconnection} is initialised and capable of
  102. * handling the value.
  103. * @type {boolean}
  104. * @private
  105. */
  106. this.mediaTransferActive = true;
  107. /**
  108. * The signaling layer implementation.
  109. * @type {SignalingLayerImpl}
  110. */
  111. this.signalingLayer = new SignalingLayerImpl();
  112. this.webrtcIceUdpDisable = Boolean(options.webrtcIceUdpDisable);
  113. this.webrtcIceTcpDisable = Boolean(options.webrtcIceTcpDisable);
  114. /**
  115. * Flag used to enforce ICE failure through the URL parameter for
  116. * the automatic testing purpose.
  117. * @type {boolean}
  118. */
  119. this.failICE = Boolean(options.failICE);
  120. this.modificationQueue
  121. = async.queue(this._processQueueTasks.bind(this), 1);
  122. /**
  123. * This is the MUC JID which will be used to add "owner" extension to
  124. * each of the local SSRCs signaled over Jingle.
  125. * Usually those are added automatically by Jicofo, but it is not
  126. * involved in a P2P session.
  127. * @type {string}
  128. */
  129. this.ssrcOwnerJid = null;
  130. /**
  131. * Flag used to guarantee that the connection established event is
  132. * triggered just once.
  133. * @type {boolean}
  134. */
  135. this.wasConnected = false;
  136. }
  137. /**
  138. * Checks whether or not this session instance has been ended and eventually
  139. * logs a message which mentions that given <tt>actionName</tt> was
  140. * cancelled.
  141. * @param {string} actionName
  142. * @return {boolean} <tt>true</tt> if this {@link JingleSessionPC} has
  143. * entered {@link JingleSessionState.ENDED} or <tt>false</tt> otherwise.
  144. * @private
  145. */
  146. _assertNotEnded(actionName) {
  147. if (this.state === JingleSessionState.ENDED) {
  148. logger.log(
  149. `The session has ended - cancelling action: ${actionName}`);
  150. return false;
  151. }
  152. return true;
  153. }
  154. /**
  155. * Finds all "source" elements under RTC "description" in given Jingle IQ
  156. * and adds 'ssrc-info' with the owner attribute set to
  157. * {@link ssrcOwnerJid}.
  158. * @param jingleIq the IQ to be modified
  159. * @private
  160. */
  161. _markAsSSRCOwner(jingleIq) {
  162. $(jingleIq).find('description source')
  163. .append(
  164. '<ssrc-info xmlns="http://jitsi.org/jitmeet" '
  165. + `owner="${this.ssrcOwnerJid}"></ssrc-info>`);
  166. }
  167. /**
  168. * Sets the JID which will be as an owner value for the local SSRCs
  169. * signaled over Jingle. Should be our MUC JID.
  170. * @param {string} ownerJid
  171. */
  172. setSSRCOwnerJid(ownerJid) {
  173. this.ssrcOwnerJid = ownerJid;
  174. }
  175. /* eslint-enable max-params */
  176. /**
  177. *
  178. */
  179. doInitialize() {
  180. this.lasticecandidate = false;
  181. // True if reconnect is in progress
  182. this.isreconnect = false;
  183. // Set to true if the connection was ever stable
  184. this.wasstable = false;
  185. // Create new peer connection instance
  186. this.peerconnection
  187. = this.rtc.createPeerConnection(
  188. this.signalingLayer,
  189. this.iceConfig,
  190. this.isP2P,
  191. {
  192. disableSimulcast: this.room.options.disableSimulcast,
  193. disableRtx: this.room.options.disableRtx,
  194. preferH264: this.room.options.preferH264
  195. });
  196. this.peerconnection.setMediaTransferActive(this.mediaTransferActive);
  197. this.peerconnection.onicecandidate = ev => {
  198. if (!ev) {
  199. // There was an incomplete check for ev before which left
  200. // the last line of the function unprotected from a potential
  201. // throw of an exception. Consequently, it may be argued that
  202. // the check is unnecessary. Anyway, I'm leaving it and making
  203. // the check complete.
  204. return;
  205. }
  206. // XXX this is broken, candidate is not parsed.
  207. const candidate = ev.candidate;
  208. const now = window.performance.now();
  209. if (candidate) {
  210. if (this._gatheringStartedTimestamp === null) {
  211. this._gatheringStartedTimestamp = now;
  212. }
  213. // Discard candidates of disabled protocols.
  214. let protocol = candidate.protocol;
  215. if (typeof protocol === 'string') {
  216. protocol = protocol.toLowerCase();
  217. if (protocol === 'tcp' || protocol === 'ssltcp') {
  218. if (this.webrtcIceTcpDisable) {
  219. return;
  220. }
  221. } else if (protocol === 'udp') {
  222. if (this.webrtcIceUdpDisable) {
  223. return;
  224. }
  225. }
  226. }
  227. } else {
  228. // End of gathering
  229. let eventName = this.isP2P ? 'p2p.ice.' : 'ice.';
  230. eventName += this.isInitiator ? 'initiator' : 'responder';
  231. eventName += '.gatheringDuration';
  232. Statistics.analytics.sendEvent(
  233. eventName,
  234. { value: now - this._gatheringStartedTimestamp });
  235. }
  236. this.sendIceCandidate(candidate);
  237. };
  238. // Note there is a change in the spec about closed:
  239. // This value moved into the RTCPeerConnectionState enum in
  240. // the May 13, 2016 draft of the specification, as it reflects the state
  241. // of the RTCPeerConnection, not the signaling connection. You now
  242. // detect a closed connection by checking for connectionState to be
  243. // "closed" instead.
  244. // I suppose at some point this will be moved to onconnectionstatechange
  245. this.peerconnection.onsignalingstatechange = () => {
  246. if (!this.peerconnection) {
  247. return;
  248. }
  249. if (this.peerconnection.signalingState === 'stable') {
  250. this.wasstable = true;
  251. } else if (
  252. (this.peerconnection.signalingState === 'closed'
  253. || this.peerconnection.connectionState === 'closed')
  254. && !this.closed) {
  255. this.room.eventEmitter.emit(XMPPEvents.SUSPEND_DETECTED, this);
  256. }
  257. };
  258. /**
  259. * The oniceconnectionstatechange event handler contains the code to
  260. * execute when the iceconnectionstatechange event, of type Event,
  261. * is received by this RTCPeerConnection. Such an event is sent when
  262. * the value of RTCPeerConnection.iceConnectionState changes.
  263. */
  264. this.peerconnection.oniceconnectionstatechange = () => {
  265. if (!this.peerconnection
  266. || !this._assertNotEnded('oniceconnectionstatechange')) {
  267. return;
  268. }
  269. const now = window.performance.now();
  270. if (!this.isP2P) {
  271. this.room.connectionTimes[
  272. `ice.state.${this.peerconnection.iceConnectionState}`]
  273. = now;
  274. }
  275. logger.log(
  276. `(TIME) ICE ${this.peerconnection.iceConnectionState}`
  277. + ` P2P? ${this.isP2P}:\t`,
  278. now);
  279. Statistics.analytics.sendEvent(
  280. `${this.isP2P ? 'p2p.ice.' : 'ice.'}`
  281. + `${this.peerconnection.iceConnectionState}`,
  282. { value: now });
  283. this.room.eventEmitter.emit(
  284. XMPPEvents.ICE_CONNECTION_STATE_CHANGED,
  285. this,
  286. this.peerconnection.iceConnectionState);
  287. switch (this.peerconnection.iceConnectionState) {
  288. case 'checking':
  289. this._iceCheckingStartedTimestamp = now;
  290. break;
  291. case 'connected':
  292. // Informs interested parties that the connection has been
  293. // restored.
  294. if (this.peerconnection.signalingState === 'stable') {
  295. if (this.isreconnect) {
  296. this.room.eventEmitter.emit(
  297. XMPPEvents.CONNECTION_RESTORED, this);
  298. }
  299. }
  300. if (!this.wasConnected && this.wasstable) {
  301. let eventName = this.isP2P ? 'p2p.ice.' : 'ice.';
  302. eventName += this.isInitiator ? 'initiator.' : 'responder.';
  303. eventName += 'checksDuration';
  304. Statistics.analytics.sendEvent(
  305. eventName,
  306. {
  307. value: now - this._iceCheckingStartedTimestamp
  308. });
  309. this.wasConnected = true;
  310. this.room.eventEmitter.emit(
  311. XMPPEvents.CONNECTION_ESTABLISHED, this);
  312. }
  313. this.isreconnect = false;
  314. break;
  315. case 'disconnected':
  316. if (this.closed) {
  317. break;
  318. }
  319. this.isreconnect = true;
  320. // Informs interested parties that the connection has been
  321. // interrupted.
  322. if (this.wasstable) {
  323. this.room.eventEmitter.emit(
  324. XMPPEvents.CONNECTION_INTERRUPTED, this);
  325. }
  326. break;
  327. case 'failed':
  328. this.room.eventEmitter.emit(
  329. XMPPEvents.CONNECTION_ICE_FAILED, this);
  330. this.room.eventEmitter.emit(
  331. XMPPEvents.CONFERENCE_SETUP_FAILED,
  332. this,
  333. new Error('ICE fail'));
  334. break;
  335. }
  336. };
  337. this.peerconnection.onnegotiationneeded = () => {
  338. this.room.eventEmitter.emit(XMPPEvents.PEERCONNECTION_READY, this);
  339. };
  340. // The signaling layer will bind it's listeners at this point
  341. this.signalingLayer.setChatRoom(this.room);
  342. }
  343. /**
  344. * Sends given candidate in Jingle 'transport-info' message.
  345. * @param {RTCIceCandidate} candidate the WebRTC ICE candidate instance
  346. * @private
  347. */
  348. sendIceCandidate(candidate) {
  349. const localSDP = new SDP(this.peerconnection.localDescription.sdp);
  350. if (candidate && !this.lasticecandidate) {
  351. const ice
  352. = SDPUtil.iceparams(
  353. localSDP.media[candidate.sdpMLineIndex], localSDP.session);
  354. const jcand = SDPUtil.candidateToJingle(candidate.candidate);
  355. if (!(ice && jcand)) {
  356. const errorMesssage = 'failed to get ice && jcand';
  357. GlobalOnErrorHandler.callErrorHandler(new Error(errorMesssage));
  358. logger.error(errorMesssage);
  359. return;
  360. }
  361. ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
  362. if (this.usedrip) {
  363. if (this.dripContainer.length === 0) {
  364. // start 20ms callout
  365. setTimeout(() => {
  366. if (this.dripContainer.length === 0) {
  367. return;
  368. }
  369. this.sendIceCandidates(this.dripContainer);
  370. this.dripContainer = [];
  371. }, 20);
  372. }
  373. this.dripContainer.push(candidate);
  374. } else {
  375. this.sendIceCandidates([ candidate ]);
  376. }
  377. } else {
  378. logger.log('sendIceCandidate: last candidate.');
  379. // FIXME: remember to re-think in ICE-restart
  380. this.lasticecandidate = true;
  381. }
  382. }
  383. /**
  384. * Sends given candidates in Jingle 'transport-info' message.
  385. * @param {Array<RTCIceCandidate>} candidates an array of the WebRTC ICE
  386. * candidate instances
  387. * @private
  388. */
  389. sendIceCandidates(candidates) {
  390. if (!this._assertNotEnded('sendIceCandidates')) {
  391. return;
  392. }
  393. logger.log('sendIceCandidates', candidates);
  394. const cand = $iq({ to: this.peerjid,
  395. type: 'set' })
  396. .c('jingle', { xmlns: 'urn:xmpp:jingle:1',
  397. action: 'transport-info',
  398. initiator: this.initiator,
  399. sid: this.sid });
  400. const localSDP = new SDP(this.peerconnection.localDescription.sdp);
  401. for (let mid = 0; mid < localSDP.media.length; mid++) {
  402. const cands = candidates.filter(el => el.sdpMLineIndex === mid);
  403. const mline
  404. = SDPUtil.parseMLine(localSDP.media[mid].split('\r\n')[0]);
  405. if (cands.length > 0) {
  406. const ice
  407. = SDPUtil.iceparams(localSDP.media[mid], localSDP.session);
  408. ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
  409. cand.c('content', {
  410. creator: this.initiator === this.localJid
  411. ? 'initiator' : 'responder',
  412. name: cands[0].sdpMid ? cands[0].sdpMid : mline.media
  413. }).c('transport', ice);
  414. for (let i = 0; i < cands.length; i++) {
  415. const candidate
  416. = SDPUtil.candidateToJingle(cands[i].candidate);
  417. // Mangle ICE candidate if 'failICE' test option is enabled
  418. if (this.failICE) {
  419. candidate.ip = '1.1.1.1';
  420. }
  421. cand.c('candidate', candidate).up();
  422. }
  423. // add fingerprint
  424. const fingerprintLine
  425. = SDPUtil.findLine(
  426. localSDP.media[mid],
  427. 'a=fingerprint:', localSDP.session);
  428. if (fingerprintLine) {
  429. const tmp = SDPUtil.parseFingerprint(fingerprintLine);
  430. tmp.required = true;
  431. cand.c(
  432. 'fingerprint',
  433. { xmlns: 'urn:xmpp:jingle:apps:dtls:0' })
  434. .t(tmp.fingerprint);
  435. delete tmp.fingerprint;
  436. cand.attrs(tmp);
  437. cand.up();
  438. }
  439. cand.up(); // transport
  440. cand.up(); // content
  441. }
  442. }
  443. // might merge last-candidate notification into this, but it is called
  444. // a lot later. See webrtc issue #2340
  445. // logger.log('was this the last candidate', this.lasticecandidate);
  446. this.connection.sendIQ(
  447. cand, null, this.newJingleErrorHandler(cand, error => {
  448. GlobalOnErrorHandler.callErrorHandler(
  449. new Error(`Jingle error: ${JSON.stringify(error)}`));
  450. }), IQ_TIMEOUT);
  451. }
  452. /**
  453. * {@inheritDoc}
  454. */
  455. addIceCandidates(elem) {
  456. if (this.peerconnection.signalingState === 'closed') {
  457. logger.warn('Ignored add ICE candidate when in closed state');
  458. return;
  459. }
  460. const iceCandidates = [];
  461. elem.find('>content>transport>candidate')
  462. .each((idx, candidate) => {
  463. let line = SDPUtil.candidateFromJingle(candidate);
  464. line = line.replace('\r\n', '').replace('a=', '');
  465. // FIXME this code does not care to handle
  466. // non-bundle transport
  467. const rtcCandidate = new RTCIceCandidate({
  468. sdpMLineIndex: 0,
  469. // FF comes up with more complex names like audio-23423,
  470. // Given that it works on both Chrome and FF without
  471. // providing it, let's leave it like this for the time
  472. // being...
  473. // sdpMid: 'audio',
  474. candidate: line
  475. });
  476. iceCandidates.push(rtcCandidate);
  477. });
  478. if (!iceCandidates.length) {
  479. logger.error(
  480. 'No ICE candidates to add ?', elem[0] && elem[0].outerHTML);
  481. return;
  482. }
  483. // We want to have this task queued, so that we know it is executed,
  484. // after the initial sRD/sLD offer/answer cycle was done (based on
  485. // the assumption that candidates are spawned after the offer/answer
  486. // and XMPP preserves order).
  487. const workFunction = finishedCallback => {
  488. for (const iceCandidate of iceCandidates) {
  489. this.peerconnection.addIceCandidate(
  490. iceCandidate,
  491. () => {
  492. logger.debug('addIceCandidate ok!');
  493. },
  494. error => {
  495. logger.error('addIceCandidate failed!', error);
  496. });
  497. }
  498. finishedCallback();
  499. };
  500. logger.debug(
  501. `Queued add (${iceCandidates.length}) ICE candidates task...`);
  502. this.modificationQueue.push(workFunction);
  503. }
  504. /**
  505. *
  506. * @param contents
  507. */
  508. readSsrcInfo(contents) {
  509. $(contents).each((i1, content) => {
  510. const ssrcs
  511. = $(content).find(
  512. 'description>'
  513. + 'source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
  514. ssrcs.each((i2, ssrcElement) => {
  515. const ssrc = ssrcElement.getAttribute('ssrc');
  516. $(ssrcElement)
  517. .find('>ssrc-info[xmlns="http://jitsi.org/jitmeet"]')
  518. .each((i3, ssrcInfoElement) => {
  519. const owner = ssrcInfoElement.getAttribute('owner');
  520. if (owner && owner.length) {
  521. this.signalingLayer.setSSRCOwner(
  522. ssrc, Strophe.getResourceFromJid(owner));
  523. }
  524. }
  525. );
  526. });
  527. });
  528. }
  529. /**
  530. * Makes the underlying TraceablePeerConnection generate new SSRC for
  531. * the recvonly video stream.
  532. * @deprecated
  533. */
  534. generateRecvonlySsrc() {
  535. if (this.peerconnection) {
  536. this.peerconnection.generateRecvonlySsrc();
  537. } else {
  538. logger.error(
  539. 'Unable to generate recvonly SSRC - no peerconnection');
  540. }
  541. }
  542. /* eslint-disable max-params */
  543. /**
  544. * Accepts incoming Jingle 'session-initiate' and should send
  545. * 'session-accept' in result.
  546. * @param jingleOffer jQuery selector pointing to the jingle element of
  547. * the offer IQ
  548. * @param success callback called when we accept incoming session
  549. * successfully and receive RESULT packet to 'session-accept' sent.
  550. * @param failure function(error) called if for any reason we fail to accept
  551. * the incoming offer. 'error' argument can be used to log some details
  552. * about the error.
  553. * @param {Array<JitsiLocalTrack>} [localTracks] the optional list of
  554. * the local tracks that will be added, before the offer/answer cycle
  555. * executes. We allow the localTracks to optionally be passed in so that
  556. * the addition of the local tracks and the processing of the initial offer
  557. * can all be done atomically. We want to make sure that any other
  558. * operations which originate in the XMPP Jingle messages related with
  559. * this session to be executed with an assumption that the initial
  560. * offer/answer cycle has been executed already.
  561. */
  562. acceptOffer(jingleOffer, success, failure, localTracks) {
  563. this.setOfferAnswerCycle(
  564. jingleOffer,
  565. () => {
  566. this.state = JingleSessionState.ACTIVE;
  567. // FIXME we may not care about RESULT packet for session-accept
  568. // then we should either call 'success' here immediately or
  569. // modify sendSessionAccept method to do that
  570. this.sendSessionAccept(success, failure);
  571. },
  572. failure,
  573. localTracks);
  574. }
  575. /* eslint-enable max-params */
  576. /**
  577. * Creates an offer and sends Jingle 'session-initiate' to the remote peer.
  578. * @param {Array<JitsiLocalTrack>} localTracks the local tracks that will be
  579. * added, before the offer/answer cycle executes (for the local track
  580. * addition to be an atomic operation together with the offer/answer).
  581. */
  582. invite(localTracks) {
  583. if (!this.isInitiator) {
  584. throw new Error('Trying to invite from the responder session');
  585. }
  586. for (const localTrack of localTracks) {
  587. this.peerconnection.addTrack(localTrack);
  588. }
  589. this.peerconnection.createOffer(
  590. this.sendSessionInitiate.bind(this),
  591. error => logger.error('Failed to create offer', error),
  592. this.mediaConstraints);
  593. }
  594. /**
  595. * Sends 'session-initiate' to the remote peer.
  596. * @param {object} sdp the local session description object as defined by
  597. * the WebRTC standard.
  598. * @private
  599. */
  600. sendSessionInitiate(sdp) {
  601. logger.log('createdOffer', sdp);
  602. const sendJingle = () => {
  603. let init = $iq({
  604. to: this.peerjid,
  605. type: 'set'
  606. }).c('jingle', {
  607. xmlns: 'urn:xmpp:jingle:1',
  608. action: 'session-initiate',
  609. initiator: this.initiator,
  610. sid: this.sid
  611. });
  612. const localSDP = new SDP(this.peerconnection.localDescription.sdp);
  613. localSDP.toJingle(
  614. init,
  615. this.initiator === this.me ? 'initiator' : 'responder');
  616. init = init.tree();
  617. this._markAsSSRCOwner(init);
  618. logger.info('Session-initiate: ', init);
  619. this.connection.sendIQ(init,
  620. () => {
  621. logger.info('Got RESULT for "session-initiate"');
  622. },
  623. error => {
  624. logger.error('"session-initiate" error', error);
  625. },
  626. IQ_TIMEOUT);
  627. };
  628. this.peerconnection.setLocalDescription(
  629. sdp, sendJingle,
  630. error => {
  631. logger.error('session-init setLocalDescription failed', error);
  632. }
  633. );
  634. }
  635. /**
  636. * Sets the answer received from the remote peer.
  637. * @param jingleAnswer
  638. */
  639. setAnswer(jingleAnswer) {
  640. if (!this.isInitiator) {
  641. throw new Error('Trying to set an answer on the responder session');
  642. }
  643. this.setOfferAnswerCycle(
  644. jingleAnswer,
  645. () => {
  646. this.state = JingleSessionState.ACTIVE;
  647. logger.info('setAnswer - succeeded');
  648. },
  649. error => {
  650. logger.error('setAnswer failed: ', error);
  651. });
  652. }
  653. /* eslint-disable max-params */
  654. /**
  655. * This is a setRemoteDescription/setLocalDescription cycle which starts at
  656. * converting Strophe Jingle IQ into remote offer SDP. Once converted
  657. * setRemoteDescription, createAnswer and setLocalDescription calls follow.
  658. * @param jingleOfferAnswerIq jQuery selector pointing to the jingle element
  659. * of the offer (or answer) IQ
  660. * @param success callback called when sRD/sLD cycle finishes successfully.
  661. * @param failure callback called with an error object as an argument if we
  662. * fail at any point during setRD, createAnswer, setLD.
  663. * @param {Array<JitsiLocalTrack>} [localTracks] the optional list of
  664. * the local tracks that will be added, before the offer/answer cycle
  665. * executes (for the local track addition to be an atomic operation together
  666. * with the offer/answer).
  667. */
  668. setOfferAnswerCycle(jingleOfferAnswerIq, success, failure, localTracks) {
  669. const workFunction = finishedCallback => {
  670. if (localTracks) {
  671. for (const track of localTracks) {
  672. this.peerconnection.addTrack(track);
  673. }
  674. }
  675. const newRemoteSdp
  676. = this._processNewJingleOfferIq(jingleOfferAnswerIq);
  677. this._renegotiate(newRemoteSdp)
  678. .then(() => {
  679. finishedCallback();
  680. }, error => {
  681. logger.error(
  682. `Error renegotiating after setting new remote ${
  683. (this.isInitiator ? 'answer: ' : 'offer: ')
  684. }${error}`, newRemoteSdp);
  685. JingleSessionPC.onJingleFatalError(this, error);
  686. finishedCallback(error);
  687. });
  688. };
  689. this.modificationQueue.push(
  690. workFunction,
  691. error => {
  692. error ? failure(error) : success();
  693. });
  694. }
  695. /* eslint-enable max-params */
  696. /**
  697. * Although it states "replace transport" it does accept full Jingle offer
  698. * which should contain new ICE transport details.
  699. * @param jingleOfferElem an element Jingle IQ that contains new offer and
  700. * transport info.
  701. * @param success callback called when we succeed to accept new offer.
  702. * @param failure function(error) called when we fail to accept new offer.
  703. */
  704. replaceTransport(jingleOfferElem, success, failure) {
  705. // We need to first set an offer without the 'data' section to have the
  706. // SCTP stack cleaned up. After that the original offer is set to have
  707. // the SCTP connection established with the new bridge.
  708. this.room.eventEmitter.emit(XMPPEvents.ICE_RESTARTING, this);
  709. const originalOffer = jingleOfferElem.clone();
  710. jingleOfferElem.find('>content[name=\'data\']').remove();
  711. // First set an offer without the 'data' section
  712. this.setOfferAnswerCycle(
  713. jingleOfferElem,
  714. () => {
  715. // Now set the original offer(with the 'data' section)
  716. this.setOfferAnswerCycle(
  717. originalOffer,
  718. () => {
  719. const localSDP
  720. = new SDP(this.peerconnection.localDescription.sdp);
  721. this.sendTransportAccept(localSDP, success, failure);
  722. },
  723. failure);
  724. },
  725. failure
  726. );
  727. }
  728. /**
  729. * Sends Jingle 'session-accept' message.
  730. * @param {function()} success callback called when we receive 'RESULT'
  731. * packet for the 'session-accept'
  732. * @param {function(error)} failure called when we receive an error response
  733. * or when the request has timed out.
  734. * @private
  735. */
  736. sendSessionAccept(success, failure) {
  737. // NOTE: since we're just reading from it, we don't need to be within
  738. // the modification queue to access the local description
  739. const localSDP = new SDP(this.peerconnection.localDescription.sdp);
  740. let accept = $iq({ to: this.peerjid,
  741. type: 'set' })
  742. .c('jingle', { xmlns: 'urn:xmpp:jingle:1',
  743. action: 'session-accept',
  744. initiator: this.initiator,
  745. responder: this.responder,
  746. sid: this.sid });
  747. if (this.webrtcIceTcpDisable) {
  748. localSDP.removeTcpCandidates = true;
  749. }
  750. if (this.webrtcIceUdpDisable) {
  751. localSDP.removeUdpCandidates = true;
  752. }
  753. if (this.failICE) {
  754. localSDP.failICE = true;
  755. }
  756. localSDP.toJingle(
  757. accept,
  758. this.initiator === this.localJid ? 'initiator' : 'responder',
  759. null);
  760. // Calling tree() to print something useful
  761. accept = accept.tree();
  762. this._markAsSSRCOwner(accept);
  763. logger.info('Sending session-accept', accept);
  764. this.connection.sendIQ(accept,
  765. success,
  766. this.newJingleErrorHandler(accept, error => {
  767. failure(error);
  768. // 'session-accept' is a critical timeout and we'll
  769. // have to restart
  770. this.room.eventEmitter.emit(
  771. XMPPEvents.SESSION_ACCEPT_TIMEOUT, this);
  772. }),
  773. IQ_TIMEOUT);
  774. // XXX Videobridge needs WebRTC's answer (ICE ufrag and pwd, DTLS
  775. // fingerprint and setup) ASAP in order to start the connection
  776. // establishment.
  777. //
  778. // FIXME Flushing the connection at this point triggers an issue with
  779. // BOSH request handling in Prosody on slow connections.
  780. //
  781. // The problem is that this request will be quite large and it may take
  782. // time before it reaches Prosody. In the meantime Strophe may decide
  783. // to send the next one. And it was observed that a small request with
  784. // 'transport-info' usually follows this one. It does reach Prosody
  785. // before the previous one was completely received. 'rid' on the server
  786. // is increased and Prosody ignores the request with 'session-accept'.
  787. // It will never reach Jicofo and everything in the request table is
  788. // lost. Removing the flush does not guarantee it will never happen, but
  789. // makes it much less likely('transport-info' is bundled with
  790. // 'session-accept' and any immediate requests).
  791. //
  792. // this.connection.flush();
  793. }
  794. /**
  795. * Sends Jingle 'transport-accept' message which is a response to
  796. * 'transport-replace'.
  797. * @param localSDP the 'SDP' object with local session description
  798. * @param success callback called when we receive 'RESULT' packet for
  799. * 'transport-replace'
  800. * @param failure function(error) called when we receive an error response
  801. * or when the request has timed out.
  802. * @private
  803. */
  804. sendTransportAccept(localSDP, success, failure) {
  805. let transportAccept = $iq({ to: this.peerjid,
  806. type: 'set' })
  807. .c('jingle', {
  808. xmlns: 'urn:xmpp:jingle:1',
  809. action: 'transport-accept',
  810. initiator: this.initiator,
  811. sid: this.sid
  812. });
  813. localSDP.media.forEach((medialines, idx) => {
  814. const mline = SDPUtil.parseMLine(medialines.split('\r\n')[0]);
  815. transportAccept.c('content',
  816. {
  817. creator:
  818. this.initiator === this.localJid
  819. ? 'initiator'
  820. : 'responder',
  821. name: mline.media
  822. }
  823. );
  824. localSDP.transportToJingle(idx, transportAccept);
  825. transportAccept.up();
  826. });
  827. // Calling tree() to print something useful to the logger
  828. transportAccept = transportAccept.tree();
  829. logger.info('Sending transport-accept: ', transportAccept);
  830. this.connection.sendIQ(transportAccept,
  831. success,
  832. this.newJingleErrorHandler(transportAccept, failure),
  833. IQ_TIMEOUT);
  834. }
  835. /**
  836. * Sends Jingle 'transport-reject' message which is a response to
  837. * 'transport-replace'.
  838. * @param success callback called when we receive 'RESULT' packet for
  839. * 'transport-replace'
  840. * @param failure function(error) called when we receive an error response
  841. * or when the request has timed out.
  842. *
  843. * FIXME method should be marked as private, but there's some spaghetti that
  844. * needs to be fixed prior doing that
  845. */
  846. sendTransportReject(success, failure) {
  847. // Send 'transport-reject', so that the focus will
  848. // know that we've failed
  849. let transportReject = $iq({ to: this.peerjid,
  850. type: 'set' })
  851. .c('jingle', {
  852. xmlns: 'urn:xmpp:jingle:1',
  853. action: 'transport-reject',
  854. initiator: this.initiator,
  855. sid: this.sid
  856. });
  857. transportReject = transportReject.tree();
  858. logger.info('Sending \'transport-reject', transportReject);
  859. this.connection.sendIQ(transportReject,
  860. success,
  861. this.newJingleErrorHandler(transportReject, failure),
  862. IQ_TIMEOUT);
  863. }
  864. /* eslint-disable max-params */
  865. /**
  866. * @inheritDoc
  867. */
  868. terminate(reason, text, success, failure) {
  869. let sessionTerminate = $iq({
  870. to: this.peerjid,
  871. type: 'set'
  872. })
  873. .c('jingle', {
  874. xmlns: 'urn:xmpp:jingle:1',
  875. action: 'session-terminate',
  876. initiator: this.initiator,
  877. sid: this.sid
  878. })
  879. .c('reason')
  880. .c(reason || 'success');
  881. if (text) {
  882. // eslint-disable-next-line newline-per-chained-call
  883. sessionTerminate.up().c('text').t(text);
  884. }
  885. // Calling tree() to print something useful
  886. sessionTerminate = sessionTerminate.tree();
  887. logger.info('Sending session-terminate', sessionTerminate);
  888. this.connection.sendIQ(
  889. sessionTerminate,
  890. success,
  891. this.newJingleErrorHandler(sessionTerminate, failure), IQ_TIMEOUT);
  892. // this should result in 'onTerminated' being called by strope.jingle.js
  893. this.connection.jingle.terminate(this.sid);
  894. }
  895. /* eslint-enable max-params */
  896. /**
  897. *
  898. * @param reasonCondition
  899. * @param reasonText
  900. */
  901. onTerminated(reasonCondition, reasonText) {
  902. this.state = JingleSessionState.ENDED;
  903. // Do something with reason and reasonCondition when we start to care
  904. // this.reasonCondition = reasonCondition;
  905. // this.reasonText = reasonText;
  906. logger.info(`Session terminated ${this}`, reasonCondition, reasonText);
  907. this.close();
  908. }
  909. /**
  910. * Parse the information from the xml sourceAddElem and translate it
  911. * into sdp lines
  912. * @param {jquery xml element} sourceAddElem the source-add
  913. * element from jingle
  914. * @param {SDP object} currentRemoteSdp the current remote
  915. * sdp (as of this new source-add)
  916. * @returns {list} a list of SDP line strings that should
  917. * be added to the remote SDP
  918. */
  919. _parseSsrcInfoFromSourceAdd(sourceAddElem, currentRemoteSdp) {
  920. const addSsrcInfo = [];
  921. $(sourceAddElem).each((i1, content) => {
  922. const name = $(content).attr('name');
  923. let lines = '';
  924. $(content)
  925. .find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]')
  926. .each(function() {
  927. // eslint-disable-next-line no-invalid-this
  928. const semantics = this.getAttribute('semantics');
  929. const ssrcs
  930. = $(this) // eslint-disable-line no-invalid-this
  931. .find('>source')
  932. .map(function() {
  933. // eslint-disable-next-line no-invalid-this
  934. return this.getAttribute('ssrc');
  935. })
  936. .get();
  937. if (ssrcs.length) {
  938. lines
  939. += `a=ssrc-group:${semantics} ${ssrcs.join(' ')
  940. }\r\n`;
  941. }
  942. });
  943. // handles both >source and >description>source
  944. const tmp
  945. = $(content).find(
  946. 'source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
  947. /* eslint-disable no-invalid-this */
  948. tmp.each(function() {
  949. const ssrc = $(this).attr('ssrc');
  950. if (currentRemoteSdp.containsSSRC(ssrc)) {
  951. logger.warn(
  952. `Source-add request for existing SSRC: ${ssrc}`);
  953. return;
  954. }
  955. // eslint-disable-next-line newline-per-chained-call
  956. $(this).find('>parameter').each(function() {
  957. lines += `a=ssrc:${ssrc} ${$(this).attr('name')}`;
  958. if ($(this).attr('value') && $(this).attr('value').length) {
  959. lines += `:${$(this).attr('value')}`;
  960. }
  961. lines += '\r\n';
  962. });
  963. });
  964. /* eslint-enable no-invalid-this */
  965. currentRemoteSdp.media.forEach((media, i2) => {
  966. if (!SDPUtil.findLine(media, `a=mid:${name}`)) {
  967. return;
  968. }
  969. if (!addSsrcInfo[i2]) {
  970. addSsrcInfo[i2] = '';
  971. }
  972. addSsrcInfo[i2] += lines;
  973. });
  974. });
  975. return addSsrcInfo;
  976. }
  977. /**
  978. * Handles a Jingle source-add message for this Jingle session.
  979. * @param elem An array of Jingle "content" elements.
  980. */
  981. addRemoteStream(elem) {
  982. this._addOrRemoveRemoteStream(true /* add */, elem);
  983. }
  984. /**
  985. * Handles a Jingle source-remove message for this Jingle session.
  986. * @param elem An array of Jingle "content" elements.
  987. */
  988. removeRemoteStream(elem) {
  989. this._addOrRemoveRemoteStream(false /* remove */, elem);
  990. }
  991. /**
  992. * Handles either Jingle 'source-add' or 'source-remove' message for this
  993. * Jingle session.
  994. * @param {boolean} isAdd <tt>true</tt> for 'source-add' or <tt>false</tt>
  995. * otherwise.
  996. * @param {Array<Element>} elem an array of Jingle "content" elements.
  997. * @private
  998. */
  999. _addOrRemoveRemoteStream(isAdd, elem) {
  1000. const logPrefix = isAdd ? 'addRemoteStream' : 'removeRemoteStream';
  1001. if (isAdd) {
  1002. this.readSsrcInfo(elem);
  1003. }
  1004. const workFunction = finishedCallback => {
  1005. if (!this.peerconnection.localDescription
  1006. || !this.peerconnection.localDescription.sdp) {
  1007. const errMsg = `${logPrefix} - localDescription not ready yet`;
  1008. logger.error(errMsg);
  1009. finishedCallback(errMsg);
  1010. return;
  1011. }
  1012. logger.log(`Processing ${logPrefix}`);
  1013. logger.log(
  1014. 'ICE connection state: ',
  1015. this.peerconnection.iceConnectionState);
  1016. const oldLocalSdp
  1017. = new SDP(this.peerconnection.localDescription.sdp);
  1018. const sdp = new SDP(this.peerconnection.remoteDescription.sdp);
  1019. const addOrRemoveSsrcInfo
  1020. = isAdd
  1021. ? this._parseSsrcInfoFromSourceAdd(elem, sdp)
  1022. : this._parseSsrcInfoFromSourceRemove(elem, sdp);
  1023. const newRemoteSdp
  1024. = isAdd
  1025. ? this._processRemoteAddSource(addOrRemoveSsrcInfo)
  1026. : this._processRemoteRemoveSource(addOrRemoveSsrcInfo);
  1027. this._renegotiate(newRemoteSdp)
  1028. .then(() => {
  1029. const newLocalSdp
  1030. = new SDP(this.peerconnection.localDescription.sdp);
  1031. logger.log(
  1032. `${logPrefix} - OK, SDPs: `, oldLocalSdp, newLocalSdp);
  1033. this.notifyMySSRCUpdate(oldLocalSdp, newLocalSdp);
  1034. finishedCallback();
  1035. }, error => {
  1036. logger.error(`${logPrefix} failed:`, error);
  1037. finishedCallback(error);
  1038. });
  1039. };
  1040. // Queue and execute
  1041. this.modificationQueue.push(workFunction);
  1042. }
  1043. /**
  1044. * The 'task' function will be given a callback it MUST call with either:
  1045. * 1) No arguments if it was successful or
  1046. * 2) An error argument if there was an error
  1047. * If the task wants to process the success or failure of the task, it
  1048. * should pass a handler to the .push function, e.g.:
  1049. * queue.push(task, (err) => {
  1050. * if (err) {
  1051. * // error handling
  1052. * } else {
  1053. * // success handling
  1054. * }
  1055. * });
  1056. */
  1057. _processQueueTasks(task, finishedCallback) {
  1058. task(finishedCallback);
  1059. }
  1060. /**
  1061. * Takes in a jingle offer iq, returns the new sdp offer
  1062. * @param {jquery xml element} offerIq the incoming offer
  1063. * @returns {SDP object} the jingle offer translated to SDP
  1064. */
  1065. _processNewJingleOfferIq(offerIq) {
  1066. const remoteSdp = new SDP('');
  1067. if (this.webrtcIceTcpDisable) {
  1068. remoteSdp.removeTcpCandidates = true;
  1069. }
  1070. if (this.webrtcIceUdpDisable) {
  1071. remoteSdp.removeUdpCandidates = true;
  1072. }
  1073. if (this.failICE) {
  1074. remoteSdp.failICE = true;
  1075. }
  1076. remoteSdp.fromJingle(offerIq);
  1077. this.readSsrcInfo($(offerIq).find('>content'));
  1078. return remoteSdp;
  1079. }
  1080. /**
  1081. * Remove the given ssrc lines from the current remote sdp
  1082. * @param {list} removeSsrcInfo a list of SDP line strings that
  1083. * should be removed from the remote SDP
  1084. * @returns type {SDP Object} the new remote SDP (after removing the lines
  1085. * in removeSsrcInfo
  1086. */
  1087. _processRemoteRemoveSource(removeSsrcInfo) {
  1088. const remoteSdp = new SDP(this.peerconnection.remoteDescription.sdp);
  1089. removeSsrcInfo.forEach((lines, idx) => {
  1090. // eslint-disable-next-line no-param-reassign
  1091. lines = lines.split('\r\n');
  1092. lines.pop(); // remove empty last element;
  1093. lines.forEach(line => {
  1094. remoteSdp.media[idx]
  1095. = remoteSdp.media[idx].replace(`${line}\r\n`, '');
  1096. });
  1097. });
  1098. remoteSdp.raw = remoteSdp.session + remoteSdp.media.join('');
  1099. return remoteSdp;
  1100. }
  1101. /**
  1102. * Add the given ssrc lines to the current remote sdp
  1103. * @param {list} addSsrcInfo a list of SDP line strings that
  1104. * should be added to the remote SDP
  1105. * @returns type {SDP Object} the new remote SDP (after removing the lines
  1106. * in removeSsrcInfo
  1107. */
  1108. _processRemoteAddSource(addSsrcInfo) {
  1109. const remoteSdp = new SDP(this.peerconnection.remoteDescription.sdp);
  1110. addSsrcInfo.forEach((lines, idx) => {
  1111. remoteSdp.media[idx] += lines;
  1112. });
  1113. remoteSdp.raw = remoteSdp.session + remoteSdp.media.join('');
  1114. return remoteSdp;
  1115. }
  1116. /**
  1117. * Do a new o/a flow using the existing remote description
  1118. * @param {SDP object} optionalRemoteSdp optional remote sdp
  1119. * to use. If not provided, the remote sdp from the
  1120. * peerconnection will be used
  1121. * @returns {Promise} promise which resolves when the
  1122. * o/a flow is complete with no arguments or
  1123. * rejects with an error {string}
  1124. */
  1125. _renegotiate(optionalRemoteSdp) {
  1126. const remoteSdp
  1127. = optionalRemoteSdp
  1128. || new SDP(this.peerconnection.remoteDescription.sdp);
  1129. const remoteDescription = new RTCSessionDescription({
  1130. type: this.isInitiator ? 'answer' : 'offer',
  1131. sdp: remoteSdp.raw
  1132. });
  1133. return new Promise((resolve, reject) => {
  1134. if (this.peerconnection.signalingState === 'closed') {
  1135. reject('Attempted to renegotiate in state closed');
  1136. return;
  1137. }
  1138. if (this.isInitiator) {
  1139. this._initiatorRenegotiate(remoteDescription, resolve, reject);
  1140. } else {
  1141. this._responderRenegotiate(remoteDescription, resolve, reject);
  1142. }
  1143. });
  1144. }
  1145. /**
  1146. * Renegotiate cycle implementation for the responder case.
  1147. * @param {object} remoteDescription the SDP object as defined by the WebRTC
  1148. * which will be used as remote description in the cycle.
  1149. * @param {function} resolve the success callback
  1150. * @param {function} reject the failure callback
  1151. * @private
  1152. */
  1153. _responderRenegotiate(remoteDescription, resolve, reject) {
  1154. // FIXME use WebRTC promise API to simplify things
  1155. logger.debug('Renegotiate: setting remote description');
  1156. this.peerconnection.setRemoteDescription(
  1157. remoteDescription,
  1158. () => {
  1159. logger.debug('Renegotiate: creating answer');
  1160. this.peerconnection.createAnswer(
  1161. answer => {
  1162. logger.debug('Renegotiate: setting local description');
  1163. this.peerconnection.setLocalDescription(
  1164. answer,
  1165. () => {
  1166. resolve();
  1167. },
  1168. error => {
  1169. reject(
  1170. `setLocalDescription failed: ${error}`);
  1171. }
  1172. );
  1173. },
  1174. error => reject(`createAnswer failed: ${error}`),
  1175. this.mediaConstraints
  1176. );
  1177. },
  1178. error => reject(`setRemoteDescription failed: ${error}`)
  1179. );
  1180. }
  1181. /**
  1182. * Renegotiate cycle implementation for the initiator's case.
  1183. * @param {object} remoteDescription the SDP object as defined by the WebRTC
  1184. * which will be used as remote description in the cycle.
  1185. * @param {function} resolve the success callback
  1186. * @param {function} reject the failure callback
  1187. * @private
  1188. */
  1189. _initiatorRenegotiate(remoteDescription, resolve, reject) {
  1190. // FIXME use WebRTC promise API to simplify things
  1191. if (this.peerconnection.signalingState === 'have-local-offer') {
  1192. // Skip createOffer and setLocalDescription or FF will fail
  1193. logger.debug(
  1194. 'Renegotiate: setting remote description');
  1195. this.peerconnection.setRemoteDescription(
  1196. remoteDescription,
  1197. () => {
  1198. resolve();
  1199. },
  1200. error => reject(`setRemoteDescription failed: ${error}`)
  1201. );
  1202. } else {
  1203. logger.debug('Renegotiate: creating offer');
  1204. this.peerconnection.createOffer(
  1205. offer => {
  1206. logger.debug('Renegotiate: setting local description');
  1207. this.peerconnection.setLocalDescription(offer,
  1208. () => {
  1209. logger.debug(
  1210. 'Renegotiate: setting remote description');
  1211. this.peerconnection.setRemoteDescription(
  1212. remoteDescription,
  1213. () => {
  1214. resolve();
  1215. },
  1216. error => reject(
  1217. `setRemoteDescription failed: ${error}`)
  1218. );
  1219. },
  1220. error => {
  1221. reject('setLocalDescription failed: ', error);
  1222. });
  1223. },
  1224. error => reject(`createOffer failed: ${error}`),
  1225. this.mediaConstraints);
  1226. }
  1227. }
  1228. /**
  1229. * Replaces <tt>oldTrack</tt> with <tt>newTrack</tt> and performs a single
  1230. * offer/answer cycle after both operations are done. Either
  1231. * <tt>oldTrack</tt> or <tt>newTrack</tt> can be null; replacing a valid
  1232. * <tt>oldTrack</tt> with a null <tt>newTrack</tt> effectively just removes
  1233. * <tt>oldTrack</tt>
  1234. * @param {JitsiLocalTrack|null} oldTrack the current track in use to be
  1235. * replaced
  1236. * @param {JitsiLocalTrack|null} newTrack the new track to use
  1237. * @returns {Promise} which resolves once the replacement is complete
  1238. * with no arguments or rejects with an error {string}
  1239. */
  1240. replaceTrack(oldTrack, newTrack) {
  1241. const workFunction = finishedCallback => {
  1242. const oldLocalSdp = this.peerconnection.localDescription.sdp;
  1243. // NOTE the code below assumes that no more than 1 video track
  1244. // can be added to the peer connection.
  1245. // Transition from no video to video (possibly screen sharing)
  1246. if (!oldTrack && newTrack && newTrack.isVideoTrack()) {
  1247. // Clearing current primary SSRC will make
  1248. // the SdpConsistency generate a new one which will result
  1249. // with:
  1250. // 1. source-remove for the recvonly
  1251. // 2. source-add for the new video stream
  1252. this.peerconnection.clearRecvonlySsrc();
  1253. // Transition from video to no video
  1254. } else if (oldTrack && oldTrack.isVideoTrack() && !newTrack) {
  1255. // Clearing current primary SSRC and generating the recvonly
  1256. // will result in:
  1257. // 1. source-remove for the old video stream
  1258. // 2. source-add for the recvonly stream
  1259. this.peerconnection.clearRecvonlySsrc();
  1260. this.peerconnection.generateRecvonlySsrc();
  1261. }
  1262. if (oldTrack) {
  1263. this.peerconnection.removeTrack(oldTrack);
  1264. }
  1265. if (newTrack) {
  1266. this.peerconnection.addTrack(newTrack);
  1267. }
  1268. if ((oldTrack || newTrack) && oldLocalSdp) {
  1269. this._renegotiate()
  1270. .then(() => {
  1271. const newLocalSDP
  1272. = new SDP(
  1273. this.peerconnection.localDescription.sdp);
  1274. this.notifyMySSRCUpdate(
  1275. new SDP(oldLocalSdp), newLocalSDP);
  1276. finishedCallback();
  1277. },
  1278. finishedCallback /* will be called with en error */);
  1279. } else {
  1280. finishedCallback();
  1281. }
  1282. };
  1283. this.modificationQueue.push(
  1284. workFunction,
  1285. error => {
  1286. if (error) {
  1287. logger.error('Replace track error:', error);
  1288. } else {
  1289. logger.info('Replace track done!');
  1290. }
  1291. });
  1292. }
  1293. /**
  1294. * Parse the information from the xml sourceRemoveElem and translate it
  1295. * into sdp lines
  1296. * @param {jquery xml element} sourceRemoveElem the source-remove
  1297. * element from jingle
  1298. * @param {SDP object} currentRemoteSdp the current remote
  1299. * sdp (as of this new source-remove)
  1300. * @returns {list} a list of SDP line strings that should
  1301. * be removed from the remote SDP
  1302. */
  1303. _parseSsrcInfoFromSourceRemove(sourceRemoveElem, currentRemoteSdp) {
  1304. const removeSsrcInfo = [];
  1305. $(sourceRemoveElem).each((i1, content) => {
  1306. const name = $(content).attr('name');
  1307. let lines = '';
  1308. $(content)
  1309. .find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]')
  1310. .each(function() {
  1311. /* eslint-disable no-invalid-this */
  1312. const semantics = this.getAttribute('semantics');
  1313. const ssrcs
  1314. = $(this)
  1315. .find('>source')
  1316. .map(function() {
  1317. return this.getAttribute('ssrc');
  1318. })
  1319. .get();
  1320. if (ssrcs.length) {
  1321. lines
  1322. += `a=ssrc-group:${semantics} ${ssrcs.join(' ')
  1323. }\r\n`;
  1324. }
  1325. /* eslint-enable no-invalid-this */
  1326. });
  1327. const ssrcs = [];
  1328. // handles both >source and >description>source versions
  1329. const tmp
  1330. = $(content).find(
  1331. 'source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
  1332. tmp.each(function() {
  1333. // eslint-disable-next-line no-invalid-this
  1334. const ssrc = $(this).attr('ssrc');
  1335. ssrcs.push(ssrc);
  1336. });
  1337. currentRemoteSdp.media.forEach((media, i2) => {
  1338. if (!SDPUtil.findLine(media, `a=mid:${name}`)) {
  1339. return;
  1340. }
  1341. if (!removeSsrcInfo[i2]) {
  1342. removeSsrcInfo[i2] = '';
  1343. }
  1344. ssrcs.forEach(ssrc => {
  1345. const ssrcLines
  1346. = SDPUtil.findLines(media, `a=ssrc:${ssrc}`);
  1347. if (ssrcLines.length) {
  1348. removeSsrcInfo[i2] += `${ssrcLines.join('\r\n')}\r\n`;
  1349. }
  1350. });
  1351. removeSsrcInfo[i2] += lines;
  1352. });
  1353. });
  1354. return removeSsrcInfo;
  1355. }
  1356. /**
  1357. * Will print an error if there is any difference, between the SSRCs given
  1358. * in the <tt>oldSDP</tt> and the ones currently described in
  1359. * the peerconnection's local description.
  1360. * @param {string} operationName the operation's name which will be printed
  1361. * in the error message.
  1362. * @param {SDP} oldSDP the old local SDP which will be compared with
  1363. * the current one.
  1364. * @return {boolean} <tt>true</tt> if there was any change or <tt>false</tt>
  1365. * otherwise.
  1366. * @private
  1367. */
  1368. _verifyNoSSRCChanged(operationName, oldSDP) {
  1369. const currentLocalSDP
  1370. = new SDP(this.peerconnection.localDescription.sdp);
  1371. let sdpDiff = new SDPDiffer(oldSDP, currentLocalSDP);
  1372. const addedMedia = sdpDiff.getNewMedia();
  1373. if (Object.keys(addedMedia).length) {
  1374. logger.error(
  1375. `Some SSRC were added on ${operationName}`, addedMedia);
  1376. return false;
  1377. }
  1378. sdpDiff = new SDPDiffer(currentLocalSDP, oldSDP);
  1379. const removedMedia = sdpDiff.getNewMedia();
  1380. if (Object.keys(removedMedia).length) {
  1381. logger.error(
  1382. `Some SSRCs were removed on ${operationName}`, removedMedia);
  1383. return false;
  1384. }
  1385. return true;
  1386. }
  1387. /**
  1388. * Adds local track back to this session, as part of the unmute operation.
  1389. * @param {JitsiLocalTrack} track
  1390. * @return {Promise} a promise that will resolve once the local track is
  1391. * added back to this session and renegotiation succeeds. Will be rejected
  1392. * with a <tt>string</tt> that provides some error details in case something
  1393. * goes wrong.
  1394. */
  1395. addTrackAsUnmute(track) {
  1396. return this._addRemoveTrackAsMuteUnmute(
  1397. false /* add as unmute */, track);
  1398. }
  1399. /**
  1400. * Remove local track as part of the mute operation.
  1401. * @param {JitsiLocalTrack} track the local track to be removed
  1402. * @return {Promise} a promise which will be resolved once the local track
  1403. * is removed from this session and the renegotiation is performed.
  1404. * The promise will be rejected with a <tt>string</tt> that the describes
  1405. * the error if anything goes wrong.
  1406. */
  1407. removeTrackAsMute(track) {
  1408. return this._addRemoveTrackAsMuteUnmute(
  1409. true /* remove as mute */, track);
  1410. }
  1411. /**
  1412. * See {@link addTrackAsUnmute} and {@link removeTrackAsMute}.
  1413. * @param {boolean} isMute <tt>true</tt> for "remove as mute" or
  1414. * <tt>false</tt> for "add as unmute".
  1415. * @param {JitsiLocalTrack} track the track that will be added/removed
  1416. * @private
  1417. */
  1418. _addRemoveTrackAsMuteUnmute(isMute, track) {
  1419. if (!track) {
  1420. return Promise.reject('invalid "track" argument value');
  1421. }
  1422. const operationName = isMute ? 'removeTrackMute' : 'addTrackUnmute';
  1423. const workFunction = finishedCallback => {
  1424. const tpc = this.peerconnection;
  1425. if (!tpc) {
  1426. finishedCallback(
  1427. `Error: tried ${operationName} track with no active peer`
  1428. + 'connection');
  1429. return;
  1430. }
  1431. const oldLocalSDP = tpc.localDescription.sdp;
  1432. const tpcOperation
  1433. = isMute
  1434. ? tpc.removeTrackMute.bind(tpc, track)
  1435. : tpc.addTrackUnmute.bind(tpc, track);
  1436. if (!tpcOperation()) {
  1437. finishedCallback(`${operationName} failed!`);
  1438. } else if (!oldLocalSDP || !tpc.remoteDescription.sdp) {
  1439. finishedCallback();
  1440. } else {
  1441. this._renegotiate()
  1442. .then(() => {
  1443. // The results are ignored, as this check failure is not
  1444. // enough to fail the whole operation. It will log
  1445. // an error inside.
  1446. this._verifyNoSSRCChanged(
  1447. operationName, new SDP(oldLocalSDP));
  1448. finishedCallback();
  1449. },
  1450. finishedCallback /* will be called with an error */);
  1451. }
  1452. };
  1453. return new Promise((resolve, reject) => {
  1454. this.modificationQueue.push(
  1455. workFunction,
  1456. error => {
  1457. if (error) {
  1458. reject(error);
  1459. } else {
  1460. resolve();
  1461. }
  1462. });
  1463. });
  1464. }
  1465. /**
  1466. * Resumes or suspends media transfer over the underlying peer connection.
  1467. * @param {boolean} active <tt>true</tt> to enable media transfer or
  1468. * <tt>false</tt> to suspend any media transmission.
  1469. * @return {Promise} a <tt>Promise</tt> which will resolve once
  1470. * the operation is done. It will be rejected with an error description as
  1471. * a string in case anything goes wrong.
  1472. */
  1473. setMediaTransferActive(active) {
  1474. const workFunction = finishedCallback => {
  1475. this.mediaTransferActive = active;
  1476. if (this.peerconnection) {
  1477. this.peerconnection.setMediaTransferActive(
  1478. this.mediaTransferActive);
  1479. // Will do the sRD/sLD cycle to update SDPs and adjust the media
  1480. // direction
  1481. this._renegotiate()
  1482. .then(
  1483. finishedCallback,
  1484. finishedCallback /* will be called with an error */);
  1485. } else {
  1486. finishedCallback();
  1487. }
  1488. };
  1489. const logStr = active ? 'active' : 'inactive';
  1490. logger.info(`Queued make media transfer ${logStr} task...`);
  1491. return new Promise((resolve, reject) => {
  1492. this.modificationQueue.push(
  1493. workFunction,
  1494. error => {
  1495. if (error) {
  1496. reject(error);
  1497. } else {
  1498. resolve();
  1499. }
  1500. });
  1501. });
  1502. }
  1503. /**
  1504. * Figures out added/removed ssrcs and send update IQs.
  1505. * @param oldSDP SDP object for old description.
  1506. * @param newSDP SDP object for new description.
  1507. */
  1508. notifyMySSRCUpdate(oldSDP, newSDP) {
  1509. if (this.state !== JingleSessionState.ACTIVE) {
  1510. logger.warn(`Skipping SSRC update in '${this.state} ' state.`);
  1511. return;
  1512. }
  1513. // send source-remove IQ.
  1514. let sdpDiffer = new SDPDiffer(newSDP, oldSDP);
  1515. const remove = $iq({ to: this.peerjid,
  1516. type: 'set' })
  1517. .c('jingle', {
  1518. xmlns: 'urn:xmpp:jingle:1',
  1519. action: 'source-remove',
  1520. initiator: this.initiator,
  1521. sid: this.sid
  1522. }
  1523. );
  1524. const removedAnySSRCs = sdpDiffer.toJingle(remove);
  1525. if (removedAnySSRCs) {
  1526. logger.info('Sending source-remove', remove.tree());
  1527. this.connection.sendIQ(
  1528. remove, null,
  1529. this.newJingleErrorHandler(remove, error => {
  1530. GlobalOnErrorHandler.callErrorHandler(
  1531. new Error(`Jingle error: ${JSON.stringify(error)}`));
  1532. }), IQ_TIMEOUT);
  1533. } else {
  1534. logger.log('removal not necessary');
  1535. }
  1536. // send source-add IQ.
  1537. sdpDiffer = new SDPDiffer(oldSDP, newSDP);
  1538. const add = $iq({ to: this.peerjid,
  1539. type: 'set' })
  1540. .c('jingle', {
  1541. xmlns: 'urn:xmpp:jingle:1',
  1542. action: 'source-add',
  1543. initiator: this.initiator,
  1544. sid: this.sid
  1545. }
  1546. );
  1547. const containsNewSSRCs = sdpDiffer.toJingle(add);
  1548. if (containsNewSSRCs) {
  1549. logger.info('Sending source-add', add.tree());
  1550. this.connection.sendIQ(
  1551. add, null, this.newJingleErrorHandler(add, error => {
  1552. GlobalOnErrorHandler.callErrorHandler(
  1553. new Error(`Jingle error: ${JSON.stringify(error)}`));
  1554. }), IQ_TIMEOUT);
  1555. } else {
  1556. logger.log('addition not necessary');
  1557. }
  1558. }
  1559. /**
  1560. * Method returns function(errorResponse) which is a callback to be passed
  1561. * to Strophe connection.sendIQ method. An 'error' structure is created that
  1562. * is passed as 1st argument to given <tt>failureCb</tt>. The format of this
  1563. * structure is as follows:
  1564. * {
  1565. * code: {XMPP error response code}
  1566. * reason: {the name of XMPP error reason element or 'timeout' if the
  1567. * request has timed out within <tt>IQ_TIMEOUT</tt> milliseconds}
  1568. * source: {request.tree() that provides original request}
  1569. * session: {JingleSessionPC instance on which the error occurred}
  1570. * }
  1571. * @param request Strophe IQ instance which is the request to be dumped into
  1572. * the error structure
  1573. * @param failureCb function(error) called when error response was returned
  1574. * or when a timeout has occurred.
  1575. * @returns {function(this:JingleSessionPC)}
  1576. */
  1577. newJingleErrorHandler(request, failureCb) {
  1578. return function(errResponse) {
  1579. const error = {};
  1580. // Get XMPP error code and condition(reason)
  1581. const errorElSel = $(errResponse).find('error');
  1582. if (errorElSel.length) {
  1583. error.code = errorElSel.attr('code');
  1584. const errorReasonSel = $(errResponse).find('error :first');
  1585. if (errorReasonSel.length) {
  1586. error.reason = errorReasonSel[0].tagName;
  1587. }
  1588. }
  1589. if (!errResponse) {
  1590. error.reason = 'timeout';
  1591. }
  1592. error.source = request;
  1593. if (request && typeof request.tree === 'function') {
  1594. error.source = request.tree();
  1595. }
  1596. if (error.source && error.source.outerHTML) {
  1597. error.source = error.source.outerHTML;
  1598. }
  1599. // Commented to fix JSON.stringify(error) exception for circular
  1600. // dependancies when we print that error.
  1601. // FIXME: Maybe we can include part of the session object
  1602. // error.session = this;
  1603. logger.error('Jingle error', error);
  1604. if (failureCb) {
  1605. failureCb(error);
  1606. }
  1607. };
  1608. }
  1609. /**
  1610. *
  1611. * @param session
  1612. * @param error
  1613. */
  1614. static onJingleFatalError(session, error) {
  1615. if (this.room) {
  1616. this.room.eventEmitter.emit(
  1617. XMPPEvents.CONFERENCE_SETUP_FAILED, session, error);
  1618. this.room.eventEmitter.emit(
  1619. XMPPEvents.JINGLE_FATAL_ERROR, session, error);
  1620. }
  1621. }
  1622. /**
  1623. * Returns the ice connection state for the peer connection.
  1624. * @returns the ice connection state for the peer connection.
  1625. */
  1626. getIceConnectionState() {
  1627. return this.peerconnection.iceConnectionState;
  1628. }
  1629. /**
  1630. * Closes the peerconnection.
  1631. */
  1632. close() {
  1633. this.closed = true;
  1634. // The signaling layer will remove it's listeners
  1635. this.signalingLayer.setChatRoom(null);
  1636. // do not try to close if already closed.
  1637. this.peerconnection
  1638. && ((this.peerconnection.signalingState
  1639. && this.peerconnection.signalingState !== 'closed')
  1640. || (this.peerconnection.connectionState
  1641. && this.peerconnection.connectionState !== 'closed'))
  1642. && this.peerconnection.close();
  1643. }
  1644. /**
  1645. * Converts to string with minor summary.
  1646. * @return {string}
  1647. */
  1648. toString() {
  1649. return `JingleSessionPC[p2p=${this.isP2P},`
  1650. + `initiator=${this.isInitiator},sid=${this.sid}]`;
  1651. }
  1652. }