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

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