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

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