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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. /* jshint -W117 */
  2. var JingleSession = require("./JingleSession");
  3. var XMPPEvents = require("../../service/xmpp/XMPPEvents");
  4. var RTCBrowserType = require("../RTC/RTCBrowserType");
  5. module.exports = function(XMPP, eventEmitter)
  6. {
  7. function CallIncomingJingle(sid, connection) {
  8. var sess = connection.jingle.sessions[sid];
  9. // TODO: do we check activecall == null?
  10. connection.jingle.activecall = sess;
  11. eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess);
  12. // TODO: check affiliation and/or role
  13. console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
  14. sess.usedrip = true; // not-so-naive trickle ice
  15. sess.sendAnswer();
  16. sess.accept();
  17. };
  18. Strophe.addConnectionPlugin('jingle', {
  19. connection: null,
  20. sessions: {},
  21. jid2session: {},
  22. ice_config: {iceServers: []},
  23. pc_constraints: {},
  24. activecall: null,
  25. media_constraints: {
  26. mandatory: {
  27. 'OfferToReceiveAudio': true,
  28. 'OfferToReceiveVideo': true
  29. }
  30. // MozDontOfferDataChannel: true when this is firefox
  31. },
  32. init: function (conn) {
  33. this.connection = conn;
  34. if (this.connection.disco) {
  35. // http://xmpp.org/extensions/xep-0167.html#support
  36. // http://xmpp.org/extensions/xep-0176.html#support
  37. this.connection.disco.addFeature('urn:xmpp:jingle:1');
  38. this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:1');
  39. this.connection.disco.addFeature('urn:xmpp:jingle:transports:ice-udp:1');
  40. this.connection.disco.addFeature('urn:xmpp:jingle:apps:dtls:0');
  41. this.connection.disco.addFeature('urn:xmpp:jingle:transports:dtls-sctp:1');
  42. this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:audio');
  43. this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:video');
  44. if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()
  45. || RTCBrowserType.isTemasysPluginUsed()) {
  46. this.connection.disco.addFeature('urn:ietf:rfc:4588');
  47. }
  48. // this is dealt with by SDP O/A so we don't need to announce this
  49. //this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293
  50. //this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294
  51. this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
  52. this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
  53. //this.connection.disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
  54. }
  55. this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
  56. },
  57. onJingle: function (iq) {
  58. var sid = $(iq).find('jingle').attr('sid');
  59. var action = $(iq).find('jingle').attr('action');
  60. var fromJid = iq.getAttribute('from');
  61. // send ack first
  62. var ack = $iq({type: 'result',
  63. to: fromJid,
  64. id: iq.getAttribute('id')
  65. });
  66. console.log('on jingle ' + action + ' from ' + fromJid, iq);
  67. var sess = this.sessions[sid];
  68. if ('session-initiate' != action) {
  69. if (sess === null) {
  70. ack.type = 'error';
  71. ack.c('error', {type: 'cancel'})
  72. .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
  73. .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
  74. this.connection.send(ack);
  75. return true;
  76. }
  77. // compare from to sess.peerjid (bare jid comparison for later compat with message-mode)
  78. // local jid is not checked
  79. if (Strophe.getBareJidFromJid(fromJid) != Strophe.getBareJidFromJid(sess.peerjid)) {
  80. console.warn('jid mismatch for session id', sid, fromJid, sess.peerjid);
  81. ack.type = 'error';
  82. ack.c('error', {type: 'cancel'})
  83. .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
  84. .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
  85. this.connection.send(ack);
  86. return true;
  87. }
  88. } else if (sess !== undefined) {
  89. // existing session with same session id
  90. // this might be out-of-order if the sess.peerjid is the same as from
  91. ack.type = 'error';
  92. ack.c('error', {type: 'cancel'})
  93. .c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
  94. console.warn('duplicate session id', sid);
  95. this.connection.send(ack);
  96. return true;
  97. }
  98. // FIXME: check for a defined action
  99. this.connection.send(ack);
  100. // see http://xmpp.org/extensions/xep-0166.html#concepts-session
  101. switch (action) {
  102. case 'session-initiate':
  103. var startMuted = $(iq).find('jingle>startmuted');
  104. if(startMuted && startMuted.length > 0)
  105. {
  106. var audioMuted = startMuted.attr("audio");
  107. var videoMuted = startMuted.attr("video");
  108. eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
  109. autioMuted === "true", videoMuted === "true");
  110. }
  111. sess = new JingleSession(
  112. $(iq).attr('to'), $(iq).find('jingle').attr('sid'),
  113. this.connection, XMPP, eventEmitter);
  114. // configure session
  115. sess.media_constraints = this.media_constraints;
  116. sess.pc_constraints = this.pc_constraints;
  117. sess.ice_config = this.ice_config;
  118. sess.initiate(fromJid, false);
  119. // FIXME: setRemoteDescription should only be done when this call is to be accepted
  120. sess.setRemoteDescription($(iq).find('>jingle'), 'offer');
  121. this.sessions[sess.sid] = sess;
  122. this.jid2session[sess.peerjid] = sess;
  123. // the callback should either
  124. // .sendAnswer and .accept
  125. // or .sendTerminate -- not necessarily synchronus
  126. CallIncomingJingle(sess.sid, this.connection);
  127. break;
  128. case 'session-accept':
  129. sess.setRemoteDescription($(iq).find('>jingle'), 'answer');
  130. sess.accept();
  131. $(document).trigger('callaccepted.jingle', [sess.sid]);
  132. break;
  133. case 'session-terminate':
  134. // If this is not the focus sending the terminate, we have
  135. // nothing more to do here.
  136. if (Object.keys(this.sessions).length < 1
  137. || !(this.sessions[Object.keys(this.sessions)[0]]
  138. instanceof JingleSession))
  139. {
  140. break;
  141. }
  142. console.log('terminating...', sess.sid);
  143. sess.terminate();
  144. this.terminate(sess.sid);
  145. if ($(iq).find('>jingle>reason').length) {
  146. $(document).trigger('callterminated.jingle', [
  147. sess.sid,
  148. sess.peerjid,
  149. $(iq).find('>jingle>reason>:first')[0].tagName,
  150. $(iq).find('>jingle>reason>text').text()
  151. ]);
  152. } else {
  153. $(document).trigger('callterminated.jingle',
  154. [sess.sid, sess.peerjid]);
  155. }
  156. break;
  157. case 'transport-info':
  158. sess.addIceCandidate($(iq).find('>jingle>content'));
  159. break;
  160. case 'session-info':
  161. var affected;
  162. if ($(iq).find('>jingle>ringing[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
  163. $(document).trigger('ringing.jingle', [sess.sid]);
  164. } else if ($(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
  165. affected = $(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
  166. $(document).trigger('mute.jingle', [sess.sid, affected]);
  167. } else if ($(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
  168. affected = $(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
  169. $(document).trigger('unmute.jingle', [sess.sid, affected]);
  170. }
  171. break;
  172. case 'addsource': // FIXME: proprietary, un-jingleish
  173. case 'source-add': // FIXME: proprietary
  174. sess.addSource($(iq).find('>jingle>content'), fromJid);
  175. break;
  176. case 'removesource': // FIXME: proprietary, un-jingleish
  177. case 'source-remove': // FIXME: proprietary
  178. sess.removeSource($(iq).find('>jingle>content'), fromJid);
  179. break;
  180. default:
  181. console.warn('jingle action not implemented', action);
  182. break;
  183. }
  184. return true;
  185. },
  186. initiate: function (peerjid, myjid) { // initiate a new jinglesession to peerjid
  187. var sess = new JingleSession(myjid || this.connection.jid,
  188. Math.random().toString(36).substr(2, 12), // random string
  189. this.connection, XMPP, eventEmitter);
  190. // configure session
  191. sess.media_constraints = this.media_constraints;
  192. sess.pc_constraints = this.pc_constraints;
  193. sess.ice_config = this.ice_config;
  194. sess.initiate(peerjid, true);
  195. this.sessions[sess.sid] = sess;
  196. this.jid2session[sess.peerjid] = sess;
  197. sess.sendOffer();
  198. return sess;
  199. },
  200. terminate: function (sid, reason, text) { // terminate by sessionid (or all sessions)
  201. if (sid === null || sid === undefined) {
  202. for (sid in this.sessions) {
  203. if (this.sessions[sid].state != 'ended') {
  204. this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
  205. this.sessions[sid].terminate();
  206. }
  207. delete this.jid2session[this.sessions[sid].peerjid];
  208. delete this.sessions[sid];
  209. }
  210. } else if (this.sessions.hasOwnProperty(sid)) {
  211. if (this.sessions[sid].state != 'ended') {
  212. this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
  213. this.sessions[sid].terminate();
  214. }
  215. delete this.jid2session[this.sessions[sid].peerjid];
  216. delete this.sessions[sid];
  217. }
  218. },
  219. // Used to terminate a session when an unavailable presence is received.
  220. terminateByJid: function (jid) {
  221. if (this.jid2session.hasOwnProperty(jid)) {
  222. var sess = this.jid2session[jid];
  223. if (sess) {
  224. sess.terminate();
  225. console.log('peer went away silently', jid);
  226. delete this.sessions[sess.sid];
  227. delete this.jid2session[jid];
  228. $(document).trigger('callterminated.jingle',
  229. [sess.sid, jid], 'gone');
  230. }
  231. }
  232. },
  233. terminateRemoteByJid: function (jid, reason) {
  234. if (this.jid2session.hasOwnProperty(jid)) {
  235. var sess = this.jid2session[jid];
  236. if (sess) {
  237. sess.sendTerminate(reason || (!sess.active()) ? 'kick' : null);
  238. sess.terminate();
  239. console.log('terminate peer with jid', sess.sid, jid);
  240. delete this.sessions[sess.sid];
  241. delete this.jid2session[jid];
  242. $(document).trigger('callterminated.jingle',
  243. [sess.sid, jid, 'kicked']);
  244. }
  245. }
  246. },
  247. getStunAndTurnCredentials: function () {
  248. // get stun and turn configuration from server via xep-0215
  249. // uses time-limited credentials as described in
  250. // http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
  251. //
  252. // see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
  253. // for a prosody module which implements this
  254. //
  255. // currently, this doesn't work with updateIce and therefore credentials with a long
  256. // validity have to be fetched before creating the peerconnection
  257. // TODO: implement refresh via updateIce as described in
  258. // https://code.google.com/p/webrtc/issues/detail?id=1650
  259. var self = this;
  260. this.connection.sendIQ(
  261. $iq({type: 'get', to: this.connection.domain})
  262. .c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
  263. function (res) {
  264. var iceservers = [];
  265. $(res).find('>services>service').each(function (idx, el) {
  266. el = $(el);
  267. var dict = {};
  268. var type = el.attr('type');
  269. switch (type) {
  270. case 'stun':
  271. dict.url = 'stun:' + el.attr('host');
  272. if (el.attr('port')) {
  273. dict.url += ':' + el.attr('port');
  274. }
  275. iceservers.push(dict);
  276. break;
  277. case 'turn':
  278. case 'turns':
  279. dict.url = type + ':';
  280. if (el.attr('username')) { // https://code.google.com/p/webrtc/issues/detail?id=1508
  281. if (navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10) < 28) {
  282. dict.url += el.attr('username') + '@';
  283. } else {
  284. dict.username = el.attr('username'); // only works in M28
  285. }
  286. }
  287. dict.url += el.attr('host');
  288. if (el.attr('port') && el.attr('port') != '3478') {
  289. dict.url += ':' + el.attr('port');
  290. }
  291. if (el.attr('transport') && el.attr('transport') != 'udp') {
  292. dict.url += '?transport=' + el.attr('transport');
  293. }
  294. if (el.attr('password')) {
  295. dict.credential = el.attr('password');
  296. }
  297. iceservers.push(dict);
  298. break;
  299. }
  300. });
  301. self.ice_config.iceServers = iceservers;
  302. },
  303. function (err) {
  304. console.warn('getting turn credentials failed', err);
  305. console.warn('is mod_turncredentials or similar installed?');
  306. }
  307. );
  308. // implement push?
  309. },
  310. /**
  311. * Populates the log data
  312. */
  313. populateData: function () {
  314. var data = {};
  315. var self = this;
  316. Object.keys(this.sessions).forEach(function (sid) {
  317. var session = self.sessions[sid];
  318. if (session.peerconnection && session.peerconnection.updateLog) {
  319. // FIXME: should probably be a .dump call
  320. data["jingle_" + session.sid] = {
  321. updateLog: session.peerconnection.updateLog,
  322. stats: session.peerconnection.stats,
  323. url: window.location.href
  324. };
  325. }
  326. });
  327. return data;
  328. }
  329. });
  330. };