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

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