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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. /* global $, $iq, Strophe */
  2. import { getLogger } from "jitsi-meet-logger";
  3. const logger = getLogger(__filename);
  4. import JingleSession 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 JingleSession(
  85. $(iq).attr('to'), $(iq).find('jingle').attr('sid'),
  86. fromJid,
  87. this.connection,
  88. this.media_constraints,
  89. this.ice_config, this.xmpp);
  90. this.sessions[sess.sid] = sess;
  91. let jingleOffer = $(iq).find('>jingle');
  92. // FIXME there's no nice way with event to get the reason
  93. // why the call was rejected
  94. this.eventEmitter.emit(XMPPEvents.CALL_INCOMING,
  95. sess, jingleOffer, now);
  96. if (!sess.active())
  97. {
  98. // Call not accepted
  99. ack.attrs({ type: 'error' });
  100. ack.c('error', {type: 'cancel'})
  101. .c('bad-request',
  102. { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
  103. .up();
  104. this.terminate(sess.sid);
  105. }
  106. Statistics.analytics.sendEvent(
  107. 'xmpp.session-initiate', now);
  108. break;
  109. }
  110. case 'session-terminate': {
  111. logger.log('terminating...', sess.sid);
  112. let reasonCondition = null;
  113. let reasonText = null;
  114. if ($(iq).find('>jingle>reason').length) {
  115. reasonCondition
  116. = $(iq).find('>jingle>reason>:first')[0].tagName;
  117. reasonText = $(iq).find('>jingle>reason>text').text();
  118. }
  119. this.terminate(sess.sid, reasonCondition, reasonText);
  120. this.eventEmitter.emit(XMPPEvents.CALL_ENDED,
  121. sess, reasonCondition, reasonText);
  122. break;
  123. }
  124. case 'transport-replace':
  125. logger.info("(TIME) Start transport replace", now);
  126. Statistics.analytics.sendEvent(
  127. 'xmpp.transport-replace.start', now);
  128. sess.replaceTransport($(iq).find('>jingle'), () => {
  129. const successTime = window.performance.now();
  130. logger.info(
  131. "(TIME) Transport replace success!", successTime);
  132. Statistics.analytics.sendEvent(
  133. 'xmpp.transport-replace.success', successTime);
  134. }, (error) => {
  135. GlobalOnErrorHandler.callErrorHandler(error);
  136. logger.error('Transport replace failed', error);
  137. sess.sendTransportReject();
  138. });
  139. break;
  140. case 'addsource': // FIXME: proprietary, un-jingleish
  141. case 'source-add': // FIXME: proprietary
  142. sess.addSource($(iq).find('>jingle>content'));
  143. break;
  144. case 'removesource': // FIXME: proprietary, un-jingleish
  145. case 'source-remove': // FIXME: proprietary
  146. sess.removeSource($(iq).find('>jingle>content'));
  147. break;
  148. default:
  149. logger.warn('jingle action not implemented', action);
  150. ack.attrs({ type: 'error' });
  151. ack.c('error', {type: 'cancel'})
  152. .c('bad-request',
  153. { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
  154. .up();
  155. break;
  156. }
  157. this.connection.send(ack);
  158. return true;
  159. }
  160. terminate (sid, reasonCondition, reasonText) {
  161. if (this.sessions.hasOwnProperty(sid)) {
  162. if (this.sessions[sid].state != 'ended') {
  163. this.sessions[sid].onTerminated(reasonCondition, reasonText);
  164. }
  165. delete this.sessions[sid];
  166. }
  167. }
  168. getStunAndTurnCredentials () {
  169. // get stun and turn configuration from server via xep-0215
  170. // uses time-limited credentials as described in
  171. // http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
  172. //
  173. // see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
  174. // for a prosody module which implements this
  175. //
  176. // currently, this doesn't work with updateIce and therefore credentials with a long
  177. // validity have to be fetched before creating the peerconnection
  178. // TODO: implement refresh via updateIce as described in
  179. // https://code.google.com/p/webrtc/issues/detail?id=1650
  180. this.connection.sendIQ(
  181. $iq({type: 'get', to: this.connection.domain})
  182. .c('services', {xmlns: 'urn:xmpp:extdisco:1'})
  183. .c('service', {host: 'turn.' + this.connection.domain}),
  184. (res) => {
  185. let iceservers = [];
  186. $(res).find('>services>service').each((idx, el) => {
  187. el = $(el);
  188. let dict = {};
  189. const type = el.attr('type');
  190. switch (type) {
  191. case 'stun':
  192. dict.url = 'stun:' + el.attr('host');
  193. if (el.attr('port')) {
  194. dict.url += ':' + el.attr('port');
  195. }
  196. iceservers.push(dict);
  197. break;
  198. case 'turn':
  199. case 'turns': {
  200. dict.url = type + ':';
  201. const username = el.attr('username');
  202. // https://code.google.com/p/webrtc/issues/detail?id=1508
  203. if (username) {
  204. if (navigator.userAgent.match(
  205. /Chrom(e|ium)\/([0-9]+)\./)
  206. && parseInt(
  207. navigator.userAgent.match(
  208. /Chrom(e|ium)\/([0-9]+)\./)[2],
  209. 10) < 28) {
  210. dict.url += username + '@';
  211. } else {
  212. // only works in M28
  213. dict.username = username;
  214. }
  215. }
  216. dict.url += el.attr('host');
  217. const port = el.attr('port');
  218. if (port && port != '3478') {
  219. dict.url += ':' + el.attr('port');
  220. }
  221. const transport = el.attr('transport');
  222. if (transport && transport != 'udp') {
  223. dict.url += '?transport=' + transport;
  224. }
  225. dict.credential = el.attr('password')
  226. || dict.credential;
  227. iceservers.push(dict);
  228. break;
  229. }
  230. }
  231. });
  232. self.ice_config.iceServers = iceservers;
  233. }, (err) => {
  234. logger.warn('getting turn credentials failed', err);
  235. logger.warn('is mod_turncredentials or similar installed?');
  236. });
  237. // implement push?
  238. }
  239. /**
  240. * Returns the data saved in 'updateLog' in a format to be logged.
  241. */
  242. getLog () {
  243. const data = {};
  244. Object.keys(this.sessions).forEach((sid) => {
  245. const session = self.sessions[sid];
  246. const pc = session.peerconnection;
  247. if (pc && pc.updateLog) {
  248. // FIXME: should probably be a .dump call
  249. data["jingle_" + sid] = {
  250. updateLog: pc.updateLog,
  251. stats: pc.stats,
  252. url: window.location.href
  253. };
  254. }
  255. });
  256. return data;
  257. }
  258. }
  259. module.exports = function(XMPP, eventEmitter) {
  260. Strophe.addConnectionPlugin('jingle',
  261. new JingleConnectionPlugin(XMPP, eventEmitter));
  262. };