您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

strophe.jingle.js 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. /* global $, $build, __filename */
  2. import { getLogger } from 'jitsi-meet-logger';
  3. import { $iq, Strophe } from 'strophe.js';
  4. import * as MediaType from '../../service/RTC/MediaType';
  5. import {
  6. ACTION_JINGLE_TR_RECEIVED,
  7. ACTION_JINGLE_TR_SUCCESS,
  8. createJingleEvent
  9. } from '../../service/statistics/AnalyticsEvents';
  10. import XMPPEvents from '../../service/xmpp/XMPPEvents';
  11. import FeatureFlags from '../flags/FeatureFlags';
  12. import Statistics from '../statistics/statistics';
  13. import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
  14. import RandomUtil from '../util/RandomUtil';
  15. import ConnectionPlugin from './ConnectionPlugin';
  16. import JingleSessionPC from './JingleSessionPC';
  17. const logger = getLogger(__filename);
  18. // XXX Strophe is build around the idea of chaining function calls so allow long
  19. // function call chains.
  20. /* eslint-disable newline-per-chained-call */
  21. /**
  22. * Creates a "source" XML element for the source described in compact JSON format in [sourceCompactJson].
  23. * @param {*} owner the endpoint ID of the owner of the source.
  24. * @param {*} sourceCompactJson the compact JSON representation of the source.
  25. * @returns the created "source" XML element.
  26. */
  27. function _createSourceExtension(owner, sourceCompactJson) {
  28. const node = $build('source', {
  29. xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
  30. ssrc: sourceCompactJson.s,
  31. name: FeatureFlags.isSourceNameSignalingEnabled() ? sourceCompactJson.n : undefined
  32. });
  33. if (sourceCompactJson.m) {
  34. node.c('parameter', {
  35. name: 'msid',
  36. value: sourceCompactJson.m
  37. }).up();
  38. }
  39. node.c('ssrc-info', {
  40. xmlns: 'http://jitsi.org/jitmeet',
  41. owner
  42. }).up();
  43. return node.node;
  44. }
  45. /**
  46. * Creates an "ssrc-group" XML element for the SSRC group described in compact JSON format in [ssrcGroupCompactJson].
  47. * @param {*} ssrcGroupCompactJson the compact JSON representation of the SSRC group.
  48. * @returns the created "ssrc-group" element.
  49. */
  50. function _createSsrcGroupExtension(ssrcGroupCompactJson) {
  51. const node = $build('ssrc-group', {
  52. xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
  53. semantics: _getSemantics(ssrcGroupCompactJson[0])
  54. });
  55. for (let i = 1; i < ssrcGroupCompactJson.length; i++) {
  56. node.c('source', {
  57. xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
  58. ssrc: ssrcGroupCompactJson[i]
  59. }).up();
  60. }
  61. return node.node;
  62. }
  63. /**
  64. * Reads a JSON-encoded message (from a "json-message" element) and extracts source descriptions. Adds the extracted
  65. * source descriptions to the given Jingle IQ in the standard Jingle format.
  66. *
  67. * Encoding sources in this compact JSON format instead of standard Jingle was introduced in order to reduce the
  68. * network traffic and load on the XMPP server. The format is described in Jicofo [TODO: insert link].
  69. *
  70. * @param {*} iq the IQ to which source descriptions will be added.
  71. * @param {*} jsonMessageXml The XML node for the "json-message" element.
  72. * @returns {Map<string, Array<string>} The audio and video ssrcs extracted from the JSON-encoded message with remote
  73. * endpoint id as the key.
  74. */
  75. function _expandSourcesFromJson(iq, jsonMessageXml) {
  76. let json;
  77. try {
  78. json = JSON.parse(jsonMessageXml.textContent);
  79. } catch (error) {
  80. logger.error(`json-message XML contained invalid JSON, ignoring: ${jsonMessageXml.textContent}`);
  81. return null;
  82. }
  83. if (!json?.sources) {
  84. // It might be a message of a different type, no need to log.
  85. return null;
  86. }
  87. // This is where we'll add "source" and "ssrc-group" elements. Create them elements if they don't exist.
  88. const audioRtpDescription = _getOrCreateRtpDescription(iq, MediaType.AUDIO);
  89. const videoRtpDescription = _getOrCreateRtpDescription(iq, MediaType.VIDEO);
  90. const ssrcMap = new Map();
  91. for (const owner in json.sources) {
  92. if (json.sources.hasOwnProperty(owner)) {
  93. const ssrcs = [];
  94. const ownerSources = json.sources[owner];
  95. // The video sources, video ssrc-groups, audio sources and audio ssrc-groups are encoded in that order in
  96. // the elements of the array.
  97. const videoSources = ownerSources?.length && ownerSources[0];
  98. const videoSsrcGroups = ownerSources?.length > 1 && ownerSources[1];
  99. const audioSources = ownerSources?.length > 2 && ownerSources[2];
  100. const audioSsrcGroups = ownerSources?.length > 3 && ownerSources[3];
  101. if (videoSources?.length) {
  102. for (let i = 0; i < videoSources.length; i++) {
  103. videoRtpDescription.appendChild(_createSourceExtension(owner, videoSources[i]));
  104. }
  105. // Log only the first video ssrc per endpoint.
  106. ssrcs.push(videoSources[0]?.s);
  107. }
  108. if (videoSsrcGroups?.length) {
  109. for (let i = 0; i < videoSsrcGroups.length; i++) {
  110. videoRtpDescription.appendChild(_createSsrcGroupExtension(videoSsrcGroups[i]));
  111. }
  112. }
  113. if (audioSources?.length) {
  114. for (let i = 0; i < audioSources.length; i++) {
  115. audioRtpDescription.appendChild(_createSourceExtension(owner, audioSources[i]));
  116. }
  117. ssrcs.push(audioSources[0]?.s);
  118. }
  119. if (audioSsrcGroups?.length) {
  120. for (let i = 0; i < audioSsrcGroups.length; i++) {
  121. audioRtpDescription.appendChild(_createSsrcGroupExtension(audioSsrcGroups[i]));
  122. }
  123. }
  124. ssrcMap.set(owner, ssrcs);
  125. }
  126. }
  127. return ssrcMap;
  128. }
  129. /**
  130. * Finds in a Jingle IQ the RTP description element with the given media type. If one does not exists, create it (as
  131. * well as the required "content" parent element) and adds it to the IQ.
  132. * @param {*} iq
  133. * @param {*} mediaType The media type, "audio" or "video".
  134. * @returns the RTP description element with the given media type.
  135. */
  136. function _getOrCreateRtpDescription(iq, mediaType) {
  137. const jingle = $(iq).find('jingle')[0];
  138. let content = $(jingle).find(`content[name="${mediaType}"]`);
  139. let description;
  140. if (content.length) {
  141. content = content[0];
  142. } else {
  143. // I'm not suree if "creator" and "senders" are required.
  144. content = $build('content', {
  145. name: mediaType
  146. }).node;
  147. jingle.appendChild(content);
  148. }
  149. description = $(content).find('description');
  150. if (description.length) {
  151. description = description[0];
  152. } else {
  153. description = $build('description', {
  154. xmlns: 'urn:xmpp:jingle:apps:rtp:1',
  155. media: mediaType
  156. }).node;
  157. content.appendChild(description);
  158. }
  159. return description;
  160. }
  161. /**
  162. * Converts the short string representing SSRC group semantics in compact JSON format to the standard representation
  163. * (i.e. convert "f" to "FID" and "s" to "SIM").
  164. * @param {*} str the compact JSON format representation of an SSRC group's semantics.
  165. * @returns the SSRC group semantics corresponding to [str].
  166. */
  167. function _getSemantics(str) {
  168. if (str === 'f') {
  169. return 'FID';
  170. } else if (str === 's') {
  171. return 'SIM';
  172. }
  173. return null;
  174. }
  175. /**
  176. * Parses the transport XML element and returns the list of ICE candidates formatted as text.
  177. *
  178. * @param {*} transport Transport XML element extracted from the IQ.
  179. * @returns {Array<string>}
  180. */
  181. function _parseIceCandidates(transport) {
  182. const candidates = $(transport).find('>candidate');
  183. const parseCandidates = [];
  184. // Extract the candidate information from the IQ.
  185. candidates.each((_, candidate) => {
  186. const attributes = candidate.attributes;
  187. const candidateAttrs = [];
  188. for (let i = 0; i < attributes.length; i++) {
  189. const attr = attributes[i];
  190. candidateAttrs.push(`${attr.name}: ${attr.value}`);
  191. }
  192. parseCandidates.push(candidateAttrs.join(' '));
  193. });
  194. return parseCandidates;
  195. }
  196. /**
  197. *
  198. */
  199. export default class JingleConnectionPlugin extends ConnectionPlugin {
  200. /**
  201. * Creates new <tt>JingleConnectionPlugin</tt>
  202. * @param {XMPP} xmpp
  203. * @param {EventEmitter} eventEmitter
  204. * @param {Object} iceConfig an object that holds the iceConfig to be passed
  205. * to the p2p and the jvb <tt>PeerConnection</tt>.
  206. */
  207. constructor(xmpp, eventEmitter, iceConfig) {
  208. super();
  209. this.xmpp = xmpp;
  210. this.eventEmitter = eventEmitter;
  211. this.sessions = {};
  212. this.jvbIceConfig = iceConfig.jvb;
  213. this.p2pIceConfig = iceConfig.p2p;
  214. this.mediaConstraints = {
  215. offerToReceiveAudio: true,
  216. offerToReceiveVideo: true
  217. };
  218. }
  219. /**
  220. *
  221. * @param connection
  222. */
  223. init(connection) {
  224. super.init(connection);
  225. this.connection.addHandler(this.onJingle.bind(this),
  226. 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
  227. }
  228. /**
  229. *
  230. * @param iq
  231. */
  232. onJingle(iq) {
  233. const sid = $(iq).find('jingle').attr('sid');
  234. const action = $(iq).find('jingle').attr('action');
  235. const fromJid = iq.getAttribute('from');
  236. // send ack first
  237. const ack = $iq({ type: 'result',
  238. to: fromJid,
  239. id: iq.getAttribute('id')
  240. });
  241. let sess = this.sessions[sid];
  242. if (action !== 'session-initiate') {
  243. if (!sess) {
  244. ack.attrs({ type: 'error' });
  245. ack.c('error', { type: 'cancel' })
  246. .c('item-not-found', {
  247. xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
  248. })
  249. .up()
  250. .c('unknown-session', {
  251. xmlns: 'urn:xmpp:jingle:errors:1'
  252. });
  253. logger.warn(`invalid session id: ${sid}`);
  254. logger.debug(iq);
  255. this.connection.send(ack);
  256. return true;
  257. }
  258. // local jid is not checked
  259. if (fromJid !== sess.remoteJid) {
  260. logger.warn(
  261. 'jid mismatch for session id', sid, sess.remoteJid, iq);
  262. ack.attrs({ type: 'error' });
  263. ack.c('error', { type: 'cancel' })
  264. .c('item-not-found', {
  265. xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
  266. })
  267. .up()
  268. .c('unknown-session', {
  269. xmlns: 'urn:xmpp:jingle:errors:1'
  270. });
  271. this.connection.send(ack);
  272. return true;
  273. }
  274. } else if (sess !== undefined) {
  275. // Existing session with same session id. This might be out-of-order
  276. // if the sess.remoteJid is the same as from.
  277. ack.attrs({ type: 'error' });
  278. ack.c('error', { type: 'cancel' })
  279. .c('service-unavailable', {
  280. xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
  281. })
  282. .up();
  283. logger.warn('duplicate session id', sid, iq);
  284. this.connection.send(ack);
  285. return true;
  286. }
  287. const now = window.performance.now();
  288. // FIXME that should work most of the time, but we'd have to
  289. // think how secure it is to assume that user with "focus"
  290. // nickname is Jicofo.
  291. const isP2P = Strophe.getResourceFromJid(fromJid) !== 'focus';
  292. // see http://xmpp.org/extensions/xep-0166.html#concepts-session
  293. const jsonMessages = $(iq).find('jingle>json-message');
  294. if (jsonMessages?.length) {
  295. let audioVideoSsrcs;
  296. logger.info(`Found a JSON-encoded element in ${action}, translating to standard Jingle.`);
  297. for (let i = 0; i < jsonMessages.length; i++) {
  298. // Currently there is always a single json-message in the IQ with the source information.
  299. audioVideoSsrcs = _expandSourcesFromJson(iq, jsonMessages[i]);
  300. }
  301. if (audioVideoSsrcs?.size) {
  302. const logMessage = [];
  303. for (const endpoint of audioVideoSsrcs.keys()) {
  304. logMessage.push(`${endpoint}:[${audioVideoSsrcs.get(endpoint)}]`);
  305. }
  306. logger.debug(`Received ${action} from ${fromJid} with sources=${logMessage.join(', ')}`);
  307. }
  308. // TODO: is there a way to remove the json-message elements once we've extracted the information?
  309. // removeChild doesn't seem to work.
  310. }
  311. switch (action) {
  312. case 'session-initiate': {
  313. logger.log('(TIME) received session-initiate:\t', now);
  314. const startMuted = $(iq).find('jingle>startmuted');
  315. isP2P && logger.debug(`Received ${action} from ${fromJid}`);
  316. if (startMuted?.length) {
  317. const audioMuted = startMuted.attr(MediaType.AUDIO);
  318. const videoMuted = startMuted.attr(MediaType.VIDEO);
  319. this.eventEmitter.emit(
  320. XMPPEvents.START_MUTED_FROM_FOCUS,
  321. audioMuted === 'true',
  322. videoMuted === 'true');
  323. }
  324. const pcConfig = isP2P ? this.p2pIceConfig : this.jvbIceConfig;
  325. sess
  326. = new JingleSessionPC(
  327. $(iq).find('jingle').attr('sid'),
  328. $(iq).attr('to'),
  329. fromJid,
  330. this.connection,
  331. this.mediaConstraints,
  332. // Makes a copy in order to prevent exception thrown on RN when either this.p2pIceConfig or
  333. // this.jvbIceConfig is modified and there's a PeerConnection instance holding a reference
  334. JSON.parse(JSON.stringify(pcConfig)),
  335. isP2P,
  336. /* initiator */ false);
  337. this.sessions[sess.sid] = sess;
  338. this.eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess, $(iq).find('>jingle'), now);
  339. break;
  340. }
  341. case 'session-accept': {
  342. const ssrcs = [];
  343. const contents = $(iq).find('jingle>content');
  344. // Extract the SSRCs from the session-accept received from a p2p peer.
  345. for (const content of contents) {
  346. const ssrc = $(content).find('description').attr('ssrc');
  347. ssrc && ssrcs.push(ssrc);
  348. }
  349. logger.debug(`Received ${action} from ${fromJid} with ssrcs=${ssrcs}`);
  350. this.eventEmitter.emit(XMPPEvents.CALL_ACCEPTED, sess, $(iq).find('>jingle'));
  351. break;
  352. }
  353. case 'content-modify': {
  354. const height = $(iq).find('jingle>content[name="video"]>max-frame-height');
  355. logger.debug(`Received ${action} from ${fromJid} with a max-frame-height=${height?.text()}`);
  356. sess.modifyContents($(iq).find('>jingle'));
  357. break;
  358. }
  359. case 'transport-info': {
  360. const candidates = _parseIceCandidates($(iq).find('jingle>content>transport'));
  361. logger.debug(`Received ${action} from ${fromJid} for candidates=${candidates.join(', ')}`);
  362. this.eventEmitter.emit(XMPPEvents.TRANSPORT_INFO, sess, $(iq).find('>jingle'));
  363. break;
  364. }
  365. case 'session-terminate': {
  366. logger.log('terminating...', sess.sid);
  367. let reasonCondition = null;
  368. let reasonText = null;
  369. if ($(iq).find('>jingle>reason').length) {
  370. reasonCondition
  371. = $(iq).find('>jingle>reason>:first')[0].tagName;
  372. reasonText = $(iq).find('>jingle>reason>text').text();
  373. }
  374. logger.debug(`Received ${action} from ${fromJid} disconnect reason=${reasonText}`);
  375. this.terminate(sess.sid, reasonCondition, reasonText);
  376. this.eventEmitter.emit(XMPPEvents.CALL_ENDED, sess, reasonCondition, reasonText);
  377. break;
  378. }
  379. case 'transport-replace': {
  380. logger.info('(TIME) Start transport replace:\t', now);
  381. const transport = $(iq).find('jingle>content>transport');
  382. const candidates = _parseIceCandidates(transport);
  383. const iceUfrag = $(transport).attr('ufrag');
  384. const icePwd = $(transport).attr('pwd');
  385. const dtlsFingerprint = $(transport).find('>fingerprint')?.text();
  386. logger.debug(`Received ${action} from ${fromJid} with iceUfrag=${iceUfrag},`
  387. + ` icePwd=${icePwd}, DTLS fingerprint=${dtlsFingerprint}, candidates=${candidates.join(', ')}`);
  388. Statistics.sendAnalytics(createJingleEvent(
  389. ACTION_JINGLE_TR_RECEIVED,
  390. {
  391. p2p: isP2P,
  392. value: now
  393. }));
  394. sess.replaceTransport($(iq).find('>jingle'), () => {
  395. const successTime = window.performance.now();
  396. logger.info('(TIME) Transport replace success:\t', successTime);
  397. Statistics.sendAnalytics(createJingleEvent(
  398. ACTION_JINGLE_TR_SUCCESS,
  399. {
  400. p2p: isP2P,
  401. value: successTime
  402. }));
  403. }, error => {
  404. GlobalOnErrorHandler.callErrorHandler(error);
  405. logger.error('Transport replace failed', error);
  406. sess.sendTransportReject();
  407. });
  408. break;
  409. }
  410. case 'source-add':
  411. sess.addRemoteStream($(iq).find('>jingle>content'));
  412. break;
  413. case 'source-remove':
  414. sess.removeRemoteStream($(iq).find('>jingle>content'));
  415. break;
  416. default:
  417. logger.warn('jingle action not implemented', action);
  418. ack.attrs({ type: 'error' });
  419. ack.c('error', { type: 'cancel' })
  420. .c('bad-request',
  421. { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
  422. .up();
  423. break;
  424. }
  425. this.connection.send(ack);
  426. return true;
  427. }
  428. /**
  429. * Creates new <tt>JingleSessionPC</tt> meant to be used in a direct P2P
  430. * connection, configured as 'initiator'.
  431. * @param {string} me our JID
  432. * @param {string} peer remote participant's JID
  433. * @return {JingleSessionPC}
  434. */
  435. newP2PJingleSession(me, peer) {
  436. const sess
  437. = new JingleSessionPC(
  438. RandomUtil.randomHexString(12),
  439. me,
  440. peer,
  441. this.connection,
  442. this.mediaConstraints,
  443. this.p2pIceConfig,
  444. /* P2P */ true,
  445. /* initiator */ true);
  446. this.sessions[sess.sid] = sess;
  447. return sess;
  448. }
  449. /**
  450. *
  451. * @param sid
  452. * @param reasonCondition
  453. * @param reasonText
  454. */
  455. terminate(sid, reasonCondition, reasonText) {
  456. if (this.sessions.hasOwnProperty(sid)) {
  457. if (this.sessions[sid].state !== 'ended') {
  458. this.sessions[sid].onTerminated(reasonCondition, reasonText);
  459. }
  460. delete this.sessions[sid];
  461. }
  462. }
  463. /**
  464. *
  465. */
  466. getStunAndTurnCredentials() {
  467. // get stun and turn configuration from server via xep-0215
  468. // uses time-limited credentials as described in
  469. // http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
  470. //
  471. // See https://modules.prosody.im/mod_turncredentials.html
  472. // for a prosody module which implements this.
  473. // Or the new implementation https://modules.prosody.im/mod_external_services which will be in prosody 0.12
  474. //
  475. // Currently, this doesn't work with updateIce and therefore credentials
  476. // with a long validity have to be fetched before creating the
  477. // peerconnection.
  478. // TODO: implement refresh via updateIce as described in
  479. // https://code.google.com/p/webrtc/issues/detail?id=1650
  480. this.connection.sendIQ(
  481. $iq({ type: 'get',
  482. to: this.xmpp.options.hosts.domain })
  483. .c('services', { xmlns: 'urn:xmpp:extdisco:2' }),
  484. v2Res => this.onReceiveStunAndTurnCredentials(v2Res),
  485. v2Err => {
  486. logger.warn('getting turn credentials with extdisco:2 failed, trying extdisco:1', v2Err);
  487. this.connection.sendIQ(
  488. $iq({ type: 'get',
  489. to: this.xmpp.options.hosts.domain })
  490. .c('services', { xmlns: 'urn:xmpp:extdisco:1' }),
  491. v1Res => this.onReceiveStunAndTurnCredentials(v1Res),
  492. v1Err => {
  493. logger.warn('getting turn credentials failed', v1Err);
  494. logger.warn('is mod_turncredentials or similar installed and configured?');
  495. }
  496. );
  497. });
  498. }
  499. /**
  500. * Parses response when querying for services using urn:xmpp:extdisco:1 or urn:xmpp:extdisco:2.
  501. * Stores results in jvbIceConfig and p2pIceConfig.
  502. * @param res The response iq.
  503. * @return {boolean} Whether something was processed from the supplied message.
  504. */
  505. onReceiveStunAndTurnCredentials(res) {
  506. const iceservers = [];
  507. $(res).find('>services>service').each((idx, el) => {
  508. // eslint-disable-next-line no-param-reassign
  509. el = $(el);
  510. const dict = {};
  511. const type = el.attr('type');
  512. switch (type) {
  513. case 'stun':
  514. dict.urls = `stun:${el.attr('host')}`;
  515. if (el.attr('port')) {
  516. dict.urls += `:${el.attr('port')}`;
  517. }
  518. iceservers.push(dict);
  519. break;
  520. case 'turn':
  521. case 'turns': {
  522. dict.urls = `${type}:`;
  523. dict.username = el.attr('username');
  524. dict.urls += el.attr('host');
  525. const port = el.attr('port');
  526. if (port) {
  527. dict.urls += `:${el.attr('port')}`;
  528. }
  529. const transport = el.attr('transport');
  530. if (transport && transport !== 'udp') {
  531. dict.urls += `?transport=${transport}`;
  532. }
  533. dict.credential = el.attr('password')
  534. || dict.credential;
  535. iceservers.push(dict);
  536. break;
  537. }
  538. }
  539. });
  540. const options = this.xmpp.options;
  541. // Shuffle ICEServers for loadbalancing
  542. for (let i = iceservers.length - 1; i > 0; i--) {
  543. const j = Math.floor(Math.random() * (i + 1));
  544. const temp = iceservers[i];
  545. iceservers[i] = iceservers[j];
  546. iceservers[j] = temp;
  547. }
  548. let filter;
  549. if (options.useTurnUdp) {
  550. filter = s => s.urls.startsWith('turn');
  551. } else {
  552. // By default we filter out STUN and TURN/UDP and leave only TURN/TCP.
  553. filter = s => s.urls.startsWith('turn') && (s.urls.indexOf('transport=tcp') >= 0);
  554. }
  555. this.jvbIceConfig.iceServers = iceservers.filter(filter);
  556. this.p2pIceConfig.iceServers = iceservers;
  557. return iceservers.length > 0;
  558. }
  559. /**
  560. * Returns the data saved in 'updateLog' in a format to be logged.
  561. */
  562. getLog() {
  563. const data = {};
  564. Object.keys(this.sessions).forEach(sid => {
  565. const session = this.sessions[sid];
  566. const pc = session.peerconnection;
  567. if (pc && pc.updateLog) {
  568. // FIXME: should probably be a .dump call
  569. data[`jingle_${sid}`] = {
  570. updateLog: pc.updateLog,
  571. stats: pc.stats,
  572. url: window.location.href
  573. };
  574. }
  575. });
  576. return data;
  577. }
  578. }
  579. /* eslint-enable newline-per-chained-call */