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

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