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.

strophe.jingle.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. /* global $, $iq, __filename, Strophe */
  2. import { getLogger } from 'jitsi-meet-logger';
  3. const logger = getLogger(__filename);
  4. import JingleSessionPC from './JingleSessionPC';
  5. import * as JingleSessionState from './JingleSessionState';
  6. import XMPPEvents from '../../service/xmpp/XMPPEvents';
  7. import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
  8. import RandomUtil from '../util/RandomUtil';
  9. import Statistics from '../statistics/statistics';
  10. import ConnectionPlugin from './ConnectionPlugin';
  11. // XXX Strophe is build around the idea of chaining function calls so allow long
  12. // function call chains.
  13. /* eslint-disable newline-per-chained-call */
  14. /**
  15. *
  16. */
  17. class JingleConnectionPlugin extends ConnectionPlugin {
  18. /**
  19. * Creates new <tt>JingleConnectionPlugin</tt>
  20. * @param {XMPP} xmpp
  21. * @param {EventEmitter} eventEmitter
  22. * @param {Array<Object>} p2pStunServers an array which is part of the ice
  23. * config passed to the <tt>PeerConnection</tt> with the structure defined
  24. * by the WebRTC standard.
  25. */
  26. constructor(xmpp, eventEmitter, p2pStunServers) {
  27. super();
  28. this.xmpp = xmpp;
  29. this.eventEmitter = eventEmitter;
  30. this.sessions = {};
  31. this.jvbIceConfig = { iceServers: [ ] };
  32. this.p2pIceConfig = { iceServers: [ ] };
  33. if (Array.isArray(p2pStunServers)) {
  34. logger.info('Configured STUN servers: ', p2pStunServers);
  35. this.p2pIceConfig.iceServers = p2pStunServers;
  36. }
  37. this.mediaConstraints = {
  38. mandatory: {
  39. 'OfferToReceiveAudio': true,
  40. 'OfferToReceiveVideo': true
  41. }
  42. // MozDontOfferDataChannel: true when this is firefox
  43. };
  44. }
  45. /**
  46. *
  47. * @param connection
  48. */
  49. init(connection) {
  50. super.init(connection);
  51. this.connection.addHandler(this.onJingle.bind(this),
  52. 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
  53. }
  54. /**
  55. *
  56. * @param iq
  57. */
  58. onJingle(iq) {
  59. const sid = $(iq).find('jingle').attr('sid');
  60. const action = $(iq).find('jingle').attr('action');
  61. const fromJid = iq.getAttribute('from');
  62. // send ack first
  63. const ack = $iq({ type: 'result',
  64. to: fromJid,
  65. id: iq.getAttribute('id')
  66. });
  67. logger.log(`on jingle ${action} from ${fromJid}`, iq);
  68. let sess = this.sessions[sid];
  69. if (action !== 'session-initiate') {
  70. if (!sess) {
  71. ack.attrs({ type: 'error' });
  72. ack.c('error', { type: 'cancel' })
  73. .c('item-not-found', {
  74. xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
  75. })
  76. .up()
  77. .c('unknown-session', {
  78. xmlns: 'urn:xmpp:jingle:errors:1'
  79. });
  80. logger.warn('invalid session id', iq);
  81. this.connection.send(ack);
  82. return true;
  83. }
  84. // local jid is not checked
  85. if (fromJid !== sess.peerjid) {
  86. logger.warn(
  87. 'jid mismatch for session id', sid, sess.peerjid, iq);
  88. ack.attrs({ type: 'error' });
  89. ack.c('error', { type: 'cancel' })
  90. .c('item-not-found', {
  91. xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
  92. })
  93. .up()
  94. .c('unknown-session', {
  95. xmlns: 'urn:xmpp:jingle:errors:1'
  96. });
  97. this.connection.send(ack);
  98. return true;
  99. }
  100. } else if (sess !== undefined) {
  101. // Existing session with same session id. This might be out-of-order
  102. // if the sess.peerjid is the same as from.
  103. ack.attrs({ type: 'error' });
  104. ack.c('error', { type: 'cancel' })
  105. .c('service-unavailable', {
  106. xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
  107. })
  108. .up();
  109. logger.warn('duplicate session id', sid, iq);
  110. this.connection.send(ack);
  111. return true;
  112. }
  113. const now = window.performance.now();
  114. // see http://xmpp.org/extensions/xep-0166.html#concepts-session
  115. switch (action) {
  116. case 'session-initiate': {
  117. logger.log('(TIME) received session-initiate:\t', now);
  118. const startMuted = $(iq).find('jingle>startmuted');
  119. if (startMuted && startMuted.length > 0) {
  120. const audioMuted = startMuted.attr('audio');
  121. const videoMuted = startMuted.attr('video');
  122. this.eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
  123. audioMuted === 'true', videoMuted === 'true');
  124. }
  125. // FIXME that should work most of the time, but we'd have to
  126. // think how secure it is to assume that user with "focus"
  127. // nickname is Jicofo.
  128. const isP2P = Strophe.getResourceFromJid(fromJid) !== 'focus';
  129. logger.info(
  130. `Marking session from ${fromJid
  131. } as ${isP2P ? '' : '*not*'} P2P`);
  132. sess = new JingleSessionPC(
  133. $(iq).find('jingle').attr('sid'),
  134. $(iq).attr('to'),
  135. fromJid,
  136. this.connection,
  137. this.mediaConstraints,
  138. isP2P ? this.p2pIceConfig : this.jvbIceConfig,
  139. isP2P /* P2P */,
  140. false /* initiator */,
  141. this.xmpp.options);
  142. this.sessions[sess.sid] = sess;
  143. this.eventEmitter.emit(XMPPEvents.CALL_INCOMING,
  144. sess, $(iq).find('>jingle'), now);
  145. Statistics.analytics.sendEvent(
  146. 'xmpp.session-initiate', { value: now });
  147. break;
  148. }
  149. case 'session-accept': {
  150. this.eventEmitter.emit(
  151. XMPPEvents.CALL_ACCEPTED, sess, $(iq).find('>jingle'));
  152. break;
  153. }
  154. case 'transport-info': {
  155. this.eventEmitter.emit(
  156. XMPPEvents.TRANSPORT_INFO, sess, $(iq).find('>jingle'));
  157. break;
  158. }
  159. case 'session-terminate': {
  160. logger.log('terminating...', sess.sid);
  161. let reasonCondition = null;
  162. let reasonText = null;
  163. if ($(iq).find('>jingle>reason').length) {
  164. reasonCondition
  165. = $(iq).find('>jingle>reason>:first')[0].tagName;
  166. reasonText = $(iq).find('>jingle>reason>text').text();
  167. }
  168. sess.state = JingleSessionState.ENDED;
  169. this.terminate(sess.sid, reasonCondition, reasonText);
  170. this.eventEmitter.emit(XMPPEvents.CALL_ENDED,
  171. sess, reasonCondition, reasonText);
  172. break;
  173. }
  174. case 'transport-replace':
  175. logger.info('(TIME) Start transport replace', now);
  176. Statistics.analytics.sendEvent(
  177. 'xmpp.transport-replace.start', { value: now });
  178. sess.replaceTransport($(iq).find('>jingle'), () => {
  179. const successTime = window.performance.now();
  180. logger.info(
  181. '(TIME) Transport replace success!', successTime);
  182. Statistics.analytics.sendEvent(
  183. 'xmpp.transport-replace.success',
  184. { value: successTime });
  185. }, error => {
  186. GlobalOnErrorHandler.callErrorHandler(error);
  187. logger.error('Transport replace failed', error);
  188. sess.sendTransportReject();
  189. });
  190. break;
  191. case 'addsource': // FIXME: proprietary, un-jingleish
  192. case 'source-add': // FIXME: proprietary
  193. sess.addRemoteStream($(iq).find('>jingle>content'));
  194. break;
  195. case 'removesource': // FIXME: proprietary, un-jingleish
  196. case 'source-remove': // FIXME: proprietary
  197. sess.removeRemoteStream($(iq).find('>jingle>content'));
  198. break;
  199. default:
  200. logger.warn('jingle action not implemented', action);
  201. ack.attrs({ type: 'error' });
  202. ack.c('error', { type: 'cancel' })
  203. .c('bad-request',
  204. { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
  205. .up();
  206. break;
  207. }
  208. this.connection.send(ack);
  209. return true;
  210. }
  211. /**
  212. * Creates new <tt>JingleSessionPC</tt> meant to be used in a direct P2P
  213. * connection, configured as 'initiator'.
  214. * @param {string} me our JID
  215. * @param {string} peer remote participant's JID
  216. * @return {JingleSessionPC}
  217. */
  218. newP2PJingleSession(me, peer) {
  219. const sess
  220. = new JingleSessionPC(
  221. RandomUtil.randomHexString(12),
  222. me,
  223. peer,
  224. this.connection,
  225. this.mediaConstraints,
  226. this.p2pIceConfig,
  227. true /* P2P */,
  228. true /* initiator */,
  229. this.xmpp.options);
  230. this.sessions[sess.sid] = sess;
  231. return sess;
  232. }
  233. /**
  234. *
  235. * @param sid
  236. * @param reasonCondition
  237. * @param reasonText
  238. */
  239. terminate(sid, reasonCondition, reasonText) {
  240. if (this.sessions.hasOwnProperty(sid)) {
  241. if (this.sessions[sid].state !== 'ended') {
  242. this.sessions[sid].onTerminated(reasonCondition, reasonText);
  243. }
  244. delete this.sessions[sid];
  245. }
  246. }
  247. /**
  248. *
  249. */
  250. getStunAndTurnCredentials() {
  251. // get stun and turn configuration from server via xep-0215
  252. // uses time-limited credentials as described in
  253. // http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
  254. //
  255. // See https://code.google.com/p/prosody-modules/source/browse/
  256. // mod_turncredentials/mod_turncredentials.lua
  257. // for a prosody module which implements this.
  258. //
  259. // Currently, this doesn't work with updateIce and therefore credentials
  260. // with a long validity have to be fetched before creating the
  261. // peerconnection.
  262. // TODO: implement refresh via updateIce as described in
  263. // https://code.google.com/p/webrtc/issues/detail?id=1650
  264. this.connection.sendIQ(
  265. $iq({ type: 'get',
  266. to: this.connection.domain })
  267. .c('services', { xmlns: 'urn:xmpp:extdisco:1' })
  268. .c('service', { host: `turn.${this.connection.domain}` }),
  269. res => {
  270. const iceservers = [];
  271. $(res).find('>services>service').each((idx, el) => {
  272. // eslint-disable-next-line no-param-reassign
  273. el = $(el);
  274. const dict = {};
  275. const type = el.attr('type');
  276. switch (type) {
  277. case 'stun':
  278. dict.url = `stun:${el.attr('host')}`;
  279. if (el.attr('port')) {
  280. dict.url += `:${el.attr('port')}`;
  281. }
  282. iceservers.push(dict);
  283. break;
  284. case 'turn':
  285. case 'turns': {
  286. dict.url = `${type}:`;
  287. const username = el.attr('username');
  288. // https://code.google.com/p/webrtc/issues/detail
  289. // ?id=1508
  290. if (username) {
  291. if (navigator.userAgent.match(
  292. /Chrom(e|ium)\/([0-9]+)\./)
  293. && parseInt(
  294. navigator.userAgent.match(
  295. /Chrom(e|ium)\/([0-9]+)\./)[2],
  296. 10) < 28) {
  297. dict.url += `${username}@`;
  298. } else {
  299. // only works in M28
  300. dict.username = username;
  301. }
  302. }
  303. dict.url += el.attr('host');
  304. const port = el.attr('port');
  305. if (port && port !== '3478') {
  306. dict.url += `:${el.attr('port')}`;
  307. }
  308. const transport = el.attr('transport');
  309. if (transport && transport !== 'udp') {
  310. dict.url += `?transport=${transport}`;
  311. }
  312. dict.credential = el.attr('password')
  313. || dict.credential;
  314. iceservers.push(dict);
  315. break;
  316. }
  317. }
  318. });
  319. this.jvbIceConfig.iceServers = iceservers;
  320. }, err => {
  321. logger.warn('getting turn credentials failed', err);
  322. logger.warn('is mod_turncredentials or similar installed?');
  323. });
  324. // implement push?
  325. }
  326. /**
  327. * Returns the data saved in 'updateLog' in a format to be logged.
  328. */
  329. getLog() {
  330. const data = {};
  331. Object.keys(this.sessions).forEach(sid => {
  332. const session = this.sessions[sid];
  333. const pc = session.peerconnection;
  334. if (pc && pc.updateLog) {
  335. // FIXME: should probably be a .dump call
  336. data[`jingle_${sid}`] = {
  337. updateLog: pc.updateLog,
  338. stats: pc.stats,
  339. url: window.location.href
  340. };
  341. }
  342. });
  343. return data;
  344. }
  345. }
  346. /* eslint-enable newline-per-chained-call */
  347. module.exports = function(XMPP, eventEmitter, p2pStunServers) {
  348. Strophe.addConnectionPlugin(
  349. 'jingle',
  350. new JingleConnectionPlugin(XMPP, eventEmitter, p2pStunServers));
  351. };