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

strophe.jingle.js 21KB

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