Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

strophe.jingle.js 15KB

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