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

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