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 11KB

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