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

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