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

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