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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. /* jshint -W117 */
  2. import {getLogger} from "jitsi-meet-logger";
  3. const logger = getLogger(__filename);
  4. import JingleSession from "./JingleSessionPC";
  5. import XMPPEvents from "../../service/xmpp/XMPPEvents";
  6. import RTCBrowserType from "../RTC/RTCBrowserType";
  7. import GlobalOnErrorHandler from "../util/GlobalOnErrorHandler";
  8. import Statistics from "../statistics/statistics";
  9. import ConnectionPlugin from "./ConnectionPlugin";
  10. class JingleConnectionPlugin extends ConnectionPlugin {
  11. constructor(xmpp, eventEmitter) {
  12. super();
  13. this.xmpp = xmpp;
  14. this.eventEmitter = eventEmitter;
  15. this.sessions = {};
  16. this.ice_config = {iceServers: []};
  17. this.media_constraints = {
  18. mandatory: {
  19. 'OfferToReceiveAudio': true,
  20. 'OfferToReceiveVideo': true
  21. }
  22. // MozDontOfferDataChannel: true when this is firefox
  23. };
  24. }
  25. init (connection) {
  26. super.init(connection);
  27. this.connection.addHandler(this.onJingle.bind(this),
  28. 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
  29. }
  30. onJingle (iq) {
  31. const sid = $(iq).find('jingle').attr('sid');
  32. const action = $(iq).find('jingle').attr('action');
  33. const fromJid = iq.getAttribute('from');
  34. // send ack first
  35. const ack = $iq({type: 'result',
  36. to: fromJid,
  37. id: iq.getAttribute('id')
  38. });
  39. logger.log('on jingle ' + action + ' from ' + fromJid, iq);
  40. let sess = this.sessions[sid];
  41. if ('session-initiate' != action) {
  42. if (!sess) {
  43. ack.attrs({ type: 'error' });
  44. ack.c('error', {type: 'cancel'})
  45. .c('item-not-found', {
  46. xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
  47. .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
  48. logger.warn('invalid session id', iq);
  49. this.connection.send(ack);
  50. return true;
  51. }
  52. // local jid is not checked
  53. if (fromJid != sess.peerjid) {
  54. logger.warn(
  55. 'jid mismatch for session id', sid, sess.peerjid, iq);
  56. ack.attrs({ 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. } else if (sess !== undefined) {
  64. // existing session with same session id
  65. // this might be out-of-order if the sess.peerjid is the same as from
  66. ack.attrs({ type: 'error' });
  67. ack.c('error', {type: 'cancel'})
  68. .c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
  69. logger.warn('duplicate session id', sid, iq);
  70. this.connection.send(ack);
  71. return true;
  72. }
  73. const now = window.performance.now();
  74. // see http://xmpp.org/extensions/xep-0166.html#concepts-session
  75. switch (action) {
  76. case 'session-initiate':
  77. logger.log("(TIME) received session-initiate:\t", now);
  78. const startMuted = $(iq).find('jingle>startmuted');
  79. if (startMuted && startMuted.length > 0) {
  80. const audioMuted = startMuted.attr("audio");
  81. const videoMuted = startMuted.attr("video");
  82. this.eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
  83. audioMuted === "true", videoMuted === "true");
  84. }
  85. sess = new JingleSession(
  86. $(iq).attr('to'), $(iq).find('jingle').attr('sid'),
  87. fromJid,
  88. this.connection,
  89. this.media_constraints,
  90. this.ice_config, this.xmpp);
  91. this.sessions[sess.sid] = sess;
  92. let jingleOffer = $(iq).find('>jingle');
  93. // FIXME there's no nice way with event to get the reason
  94. // why the call was rejected
  95. this.eventEmitter.emit(XMPPEvents.CALL_INCOMING,
  96. sess, jingleOffer, now);
  97. if (!sess.active())
  98. {
  99. // Call not accepted
  100. ack.attrs({ type: 'error' });
  101. ack.c('error', {type: 'cancel'})
  102. .c('bad-request',
  103. { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
  104. .up();
  105. this.terminate(sess.sid);
  106. }
  107. Statistics.analytics.sendEvent(
  108. 'xmpp.session-initiate', now);
  109. break;
  110. case 'session-terminate':
  111. logger.log('terminating...', sess.sid);
  112. let reasonCondition = null;
  113. let reasonText = null;
  114. if ($(iq).find('>jingle>reason').length) {
  115. reasonCondition
  116. = $(iq).find('>jingle>reason>:first')[0].tagName;
  117. reasonText = $(iq).find('>jingle>reason>text').text();
  118. }
  119. this.terminate(sess.sid, reasonCondition, reasonText);
  120. this.eventEmitter.emit(XMPPEvents.CALL_ENDED,
  121. sess, reasonCondition, reasonText);
  122. break;
  123. case 'transport-replace':
  124. logger.info("(TIME) Start transport replace", now);
  125. Statistics.analytics.sendEvent(
  126. 'xmpp.transport-replace.start', now);
  127. sess.replaceTransport($(iq).find('>jingle'), () => {
  128. const successTime = window.performance.now();
  129. logger.info(
  130. "(TIME) Transport replace success!", successTime);
  131. Statistics.analytics.sendEvent(
  132. 'xmpp.transport-replace.success', successTime);
  133. }, (error) => {
  134. GlobalOnErrorHandler.callErrorHandler(error);
  135. logger.error('Transport replace failed', error);
  136. sess.sendTransportReject();
  137. });
  138. break;
  139. case 'addsource': // FIXME: proprietary, un-jingleish
  140. case 'source-add': // FIXME: proprietary
  141. sess.addSource($(iq).find('>jingle>content'));
  142. break;
  143. case 'removesource': // FIXME: proprietary, un-jingleish
  144. case 'source-remove': // FIXME: proprietary
  145. sess.removeSource($(iq).find('>jingle>content'));
  146. break;
  147. default:
  148. logger.warn('jingle action not implemented', action);
  149. ack.attrs({ type: 'error' });
  150. ack.c('error', {type: 'cancel'})
  151. .c('bad-request',
  152. { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
  153. .up();
  154. break;
  155. }
  156. this.connection.send(ack);
  157. return true;
  158. }
  159. terminate (sid, reasonCondition, reasonText) {
  160. if (this.sessions.hasOwnProperty(sid)) {
  161. if (this.sessions[sid].state != 'ended') {
  162. this.sessions[sid].onTerminated(reasonCondition, reasonText);
  163. }
  164. delete this.sessions[sid];
  165. }
  166. }
  167. getStunAndTurnCredentials () {
  168. // get stun and turn configuration from server via xep-0215
  169. // uses time-limited credentials as described in
  170. // http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
  171. //
  172. // see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
  173. // for a prosody module which implements this
  174. //
  175. // currently, this doesn't work with updateIce and therefore credentials with a long
  176. // validity have to be fetched before creating the peerconnection
  177. // TODO: implement refresh via updateIce as described in
  178. // https://code.google.com/p/webrtc/issues/detail?id=1650
  179. this.connection.sendIQ(
  180. $iq({type: 'get', to: this.connection.domain})
  181. .c('services', {xmlns: 'urn:xmpp:extdisco:1'})
  182. .c('service', {host: 'turn.' + this.connection.domain}),
  183. (res) => {
  184. let iceservers = [];
  185. $(res).find('>services>service').each((idx, el) => {
  186. el = $(el);
  187. let dict = {};
  188. const type = el.attr('type');
  189. switch (type) {
  190. case 'stun':
  191. dict.url = 'stun:' + el.attr('host');
  192. if (el.attr('port')) {
  193. dict.url += ':' + el.attr('port');
  194. }
  195. iceservers.push(dict);
  196. break;
  197. case 'turn':
  198. case 'turns':
  199. dict.url = type + ':';
  200. const username = el.attr('username');
  201. // https://code.google.com/p/webrtc/issues/detail?id=1508
  202. if (username) {
  203. if (navigator.userAgent.match(
  204. /Chrom(e|ium)\/([0-9]+)\./)
  205. && parseInt(
  206. navigator.userAgent.match(
  207. /Chrom(e|ium)\/([0-9]+)\./)[2],
  208. 10) < 28) {
  209. dict.url += username + '@';
  210. } else {
  211. // only works in M28
  212. dict.username = username;
  213. }
  214. }
  215. dict.url += el.attr('host');
  216. const port = el.attr('port');
  217. if (port && port != '3478') {
  218. dict.url += ':' + el.attr('port');
  219. }
  220. const transport = el.attr('transport');
  221. if (transport && transport != 'udp') {
  222. dict.url += '?transport=' + transport;
  223. }
  224. dict.credential = el.attr('password')
  225. || dict.credential;
  226. iceservers.push(dict);
  227. break;
  228. }
  229. });
  230. self.ice_config.iceServers = iceservers;
  231. }, (err) => {
  232. logger.warn('getting turn credentials failed', err);
  233. logger.warn('is mod_turncredentials or similar installed?');
  234. });
  235. // implement push?
  236. }
  237. /**
  238. * Returns the data saved in 'updateLog' in a format to be logged.
  239. */
  240. getLog () {
  241. const data = {};
  242. Object.keys(this.sessions).forEach((sid) => {
  243. const session = self.sessions[sid];
  244. const pc = session.peerconnection
  245. if (pc && pc.updateLog) {
  246. // FIXME: should probably be a .dump call
  247. data["jingle_" + sid] = {
  248. updateLog: pc.updateLog,
  249. stats: pc.stats,
  250. url: window.location.href
  251. };
  252. }
  253. });
  254. return data;
  255. }
  256. }
  257. module.exports = function(XMPP, eventEmitter) {
  258. Strophe.addConnectionPlugin('jingle',
  259. new JingleConnectionPlugin(XMPP, eventEmitter));
  260. };