Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

strophe.jingle.js 14KB

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