Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

strophe.jingle.js 12KB

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