選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

strophe.jingle.js 14KB

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