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.

xmpp.js 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. /* global $, APP, config, Strophe*/
  2. var Moderator = require("./moderator");
  3. var EventEmitter = require("events");
  4. var Recording = require("./recording");
  5. var SDP = require("./SDP");
  6. var Settings = require("../settings/Settings");
  7. var Pako = require("pako");
  8. var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
  9. var RTCEvents = require("../../service/RTC/RTCEvents");
  10. var UIEvents = require("../../service/UI/UIEvents");
  11. var XMPPEvents = require("../../service/xmpp/XMPPEvents");
  12. var retry = require('retry');
  13. var eventEmitter = new EventEmitter();
  14. var connection = null;
  15. var authenticatedUser = false;
  16. function connect(jid, password) {
  17. var faultTolerantConnect = retry.operation({
  18. retries: 3
  19. });
  20. // fault tolerant connect
  21. faultTolerantConnect.attempt(function () {
  22. connection = XMPP.createConnection();
  23. Moderator.setConnection(connection);
  24. if (connection.disco) {
  25. // for chrome, add multistream cap
  26. }
  27. connection.jingle.pc_constraints = APP.RTC.getPCConstraints();
  28. if (config.useIPv6) {
  29. // https://code.google.com/p/webrtc/issues/detail?id=2828
  30. if (!connection.jingle.pc_constraints.optional)
  31. connection.jingle.pc_constraints.optional = [];
  32. connection.jingle.pc_constraints.optional.push({googIPv6: true});
  33. }
  34. // Include user info in MUC presence
  35. var settings = Settings.getSettings();
  36. if (settings.email) {
  37. connection.emuc.addEmailToPresence(settings.email);
  38. }
  39. if (settings.uid) {
  40. connection.emuc.addUserIdToPresence(settings.uid);
  41. }
  42. if (settings.displayName) {
  43. connection.emuc.addDisplayNameToPresence(settings.displayName);
  44. }
  45. // connection.connect() starts the connection process.
  46. //
  47. // As the connection process proceeds, the user supplied callback will
  48. // be triggered multiple times with status updates. The callback should
  49. // take two arguments - the status code and the error condition.
  50. //
  51. // The status code will be one of the values in the Strophe.Status
  52. // constants. The error condition will be one of the conditions defined
  53. // in RFC 3920 or the condition ‘strophe-parsererror’.
  54. //
  55. // The Parameters wait, hold and route are optional and only relevant
  56. // for BOSH connections. Please see XEP 124 for a more detailed
  57. // explanation of the optional parameters.
  58. //
  59. // Connection status constants for use by the connection handler
  60. // callback.
  61. //
  62. // Status.ERROR - An error has occurred (websockets specific)
  63. // Status.CONNECTING - The connection is currently being made
  64. // Status.CONNFAIL - The connection attempt failed
  65. // Status.AUTHENTICATING - The connection is authenticating
  66. // Status.AUTHFAIL - The authentication attempt failed
  67. // Status.CONNECTED - The connection has succeeded
  68. // Status.DISCONNECTED - The connection has been terminated
  69. // Status.DISCONNECTING - The connection is currently being terminated
  70. // Status.ATTACHED - The connection has been attached
  71. var anonymousConnectionFailed = false;
  72. var connectionFailed = false;
  73. var lastErrorMsg;
  74. connection.connect(jid, password, function (status, msg) {
  75. console.log('Strophe status changed to',
  76. Strophe.getStatusString(status), msg);
  77. if (status === Strophe.Status.CONNECTED) {
  78. if (config.useStunTurn) {
  79. connection.jingle.getStunAndTurnCredentials();
  80. }
  81. console.info("My Jabber ID: " + connection.jid);
  82. if (password)
  83. authenticatedUser = true;
  84. maybeDoJoin();
  85. } else if (status === Strophe.Status.CONNFAIL) {
  86. if (msg === 'x-strophe-bad-non-anon-jid') {
  87. anonymousConnectionFailed = true;
  88. } else {
  89. connectionFailed = true;
  90. }
  91. lastErrorMsg = msg;
  92. } else if (status === Strophe.Status.DISCONNECTED) {
  93. if (anonymousConnectionFailed) {
  94. // prompt user for username and password
  95. XMPP.promptLogin();
  96. } else {
  97. // Strophe already has built-in HTTP/BOSH error handling and
  98. // request retry logic. Requests are resent automatically
  99. // until their error count reaches 5. Strophe.js disconnects
  100. // if the error count is > 5. We are not replicating this
  101. // here.
  102. //
  103. // The "problem" is that failed HTTP/BOSH requests don't
  104. // trigger a callback with a status update, so when a
  105. // callback with status Strophe.Status.DISCONNECTED arrives,
  106. // we can't be sure if it's a graceful disconnect or if it's
  107. // triggered by some HTTP/BOSH error.
  108. //
  109. // But that's a minor issue in Jitsi Meet as we never
  110. // disconnect anyway, not even when the user closes the
  111. // browser window (which is kind of wrong, but the point is
  112. // that we should never ever get disconnected).
  113. //
  114. // On the other hand, failed connections due to XMPP layer
  115. // errors, trigger a callback with status Strophe.Status.CONNFAIL.
  116. //
  117. // Here we implement retry logic for failed connections due
  118. // to XMPP layer errors and we display an error to the user
  119. // if we get disconnected from the XMPP server permanently.
  120. // If the connection failed, retry.
  121. if (connectionFailed
  122. && faultTolerantConnect.retry("connection-failed")) {
  123. return;
  124. }
  125. // If we failed to connect to the XMPP server, fire an event
  126. // to let all the interested module now about it.
  127. eventEmitter.emit(XMPPEvents.CONNECTION_FAILED,
  128. msg ? msg : lastErrorMsg);
  129. }
  130. } else if (status === Strophe.Status.AUTHFAIL) {
  131. // wrong password or username, prompt user
  132. XMPP.promptLogin();
  133. }
  134. });
  135. });
  136. }
  137. function maybeDoJoin() {
  138. if (connection && connection.connected &&
  139. Strophe.getResourceFromJid(connection.jid)
  140. && (APP.RTC.localAudio || APP.RTC.localVideo)) {
  141. // .connected is true while connecting?
  142. doJoin();
  143. }
  144. }
  145. function doJoin() {
  146. var roomName = APP.UI.generateRoomName();
  147. Moderator.allocateConferenceFocus(
  148. roomName, APP.UI.checkForNicknameAndJoin);
  149. }
  150. function initStrophePlugins()
  151. {
  152. require("./strophe.emuc")(XMPP, eventEmitter);
  153. require("./strophe.jingle")(XMPP, eventEmitter);
  154. require("./strophe.moderate")(XMPP);
  155. require("./strophe.util")();
  156. require("./strophe.rayo")();
  157. require("./strophe.logger")();
  158. }
  159. function registerListeners() {
  160. APP.RTC.addStreamListener(maybeDoJoin,
  161. StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
  162. APP.RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED, function (devices) {
  163. XMPP.addToPresence("devices", devices);
  164. })
  165. APP.UI.addListener(UIEvents.NICKNAME_CHANGED, function (nickname) {
  166. XMPP.addToPresence("displayName", nickname);
  167. });
  168. }
  169. function setupEvents() {
  170. $(window).bind('beforeunload', function () {
  171. if (connection && connection.connected) {
  172. // ensure signout
  173. $.ajax({
  174. type: 'POST',
  175. url: config.bosh,
  176. async: false,
  177. cache: false,
  178. contentType: 'application/xml',
  179. data: "<body rid='" + (connection.rid || connection._proto.rid)
  180. + "' xmlns='http://jabber.org/protocol/httpbind' sid='"
  181. + (connection.sid || connection._proto.sid)
  182. + "' type='terminate'>" +
  183. "<presence xmlns='jabber:client' type='unavailable'/>" +
  184. "</body>",
  185. success: function (data) {
  186. console.log('signed out');
  187. console.log(data);
  188. },
  189. error: function (XMLHttpRequest, textStatus, errorThrown) {
  190. console.log('signout error',
  191. textStatus + ' (' + errorThrown + ')');
  192. }
  193. });
  194. }
  195. XMPP.disposeConference(true);
  196. });
  197. }
  198. var XMPP = {
  199. getConnection: function(){ return connection; },
  200. sessionTerminated: false,
  201. /**
  202. * XMPP connection status
  203. */
  204. Status: Strophe.Status,
  205. /**
  206. * Remembers if we were muted by the focus.
  207. * @type {boolean}
  208. */
  209. forceMuted: false,
  210. start: function () {
  211. setupEvents();
  212. initStrophePlugins();
  213. registerListeners();
  214. Moderator.init(this, eventEmitter);
  215. var configDomain = config.hosts.anonymousdomain || config.hosts.domain;
  216. // Force authenticated domain if room is appended with '?login=true'
  217. if (config.hosts.anonymousdomain &&
  218. window.location.search.indexOf("login=true") !== -1) {
  219. configDomain = config.hosts.domain;
  220. }
  221. var jid = configDomain || window.location.hostname;
  222. connect(jid, null);
  223. },
  224. createConnection: function () {
  225. var bosh = config.bosh || '/http-bind';
  226. return new Strophe.Connection(bosh);
  227. },
  228. getStatusString: function (status) {
  229. return Strophe.getStatusString(status);
  230. },
  231. promptLogin: function () {
  232. // FIXME: re-use LoginDialog which supports retries
  233. APP.UI.showLoginPopup(connect);
  234. },
  235. joinRoom: function(roomName, useNicks, nick)
  236. {
  237. var roomjid;
  238. roomjid = roomName;
  239. if (useNicks) {
  240. if (nick) {
  241. roomjid += '/' + nick;
  242. } else {
  243. roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
  244. }
  245. } else {
  246. var tmpJid = Strophe.getNodeFromJid(connection.jid);
  247. if(!authenticatedUser)
  248. tmpJid = tmpJid.substr(0, 8);
  249. roomjid += '/' + tmpJid;
  250. }
  251. connection.emuc.doJoin(roomjid);
  252. },
  253. myJid: function () {
  254. if(!connection)
  255. return null;
  256. return connection.emuc.myroomjid;
  257. },
  258. myResource: function () {
  259. if(!connection || ! connection.emuc.myroomjid)
  260. return null;
  261. return Strophe.getResourceFromJid(connection.emuc.myroomjid);
  262. },
  263. disposeConference: function (onUnload) {
  264. eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE, onUnload);
  265. var handler = connection.jingle.activecall;
  266. if (handler && handler.peerconnection) {
  267. // FIXME: probably removing streams is not required and close() should
  268. // be enough
  269. if (APP.RTC.localAudio) {
  270. handler.peerconnection.removeStream(
  271. APP.RTC.localAudio.getOriginalStream(), onUnload);
  272. }
  273. if (APP.RTC.localVideo) {
  274. handler.peerconnection.removeStream(
  275. APP.RTC.localVideo.getOriginalStream(), onUnload);
  276. }
  277. handler.peerconnection.close();
  278. }
  279. connection.jingle.activecall = null;
  280. if(!onUnload)
  281. {
  282. this.sessionTerminated = true;
  283. connection.emuc.doLeave();
  284. }
  285. },
  286. addListener: function(type, listener)
  287. {
  288. eventEmitter.on(type, listener);
  289. },
  290. removeListener: function (type, listener) {
  291. eventEmitter.removeListener(type, listener);
  292. },
  293. allocateConferenceFocus: function(roomName, callback) {
  294. Moderator.allocateConferenceFocus(roomName, callback);
  295. },
  296. getLoginUrl: function (roomName, callback) {
  297. Moderator.getLoginUrl(roomName, callback);
  298. },
  299. getPopupLoginUrl: function (roomName, callback) {
  300. Moderator.getPopupLoginUrl(roomName, callback);
  301. },
  302. isModerator: function () {
  303. return Moderator.isModerator();
  304. },
  305. isSipGatewayEnabled: function () {
  306. return Moderator.isSipGatewayEnabled();
  307. },
  308. isExternalAuthEnabled: function () {
  309. return Moderator.isExternalAuthEnabled();
  310. },
  311. switchStreams: function (stream, oldStream, callback, isAudio) {
  312. if (connection && connection.jingle.activecall) {
  313. // FIXME: will block switchInProgress on true value in case of exception
  314. connection.jingle.activecall.switchStreams(stream, oldStream, callback, isAudio);
  315. } else {
  316. // We are done immediately
  317. console.warn("No conference handler or conference not started yet");
  318. callback();
  319. }
  320. },
  321. sendVideoInfoPresence: function (mute) {
  322. if(!connection)
  323. return;
  324. connection.emuc.addVideoInfoToPresence(mute);
  325. connection.emuc.sendPresence();
  326. },
  327. setVideoMute: function (mute, callback, options) {
  328. if(!connection)
  329. return;
  330. var self = this;
  331. var localCallback = function (mute) {
  332. self.sendVideoInfoPresence(mute);
  333. return callback(mute);
  334. };
  335. if(connection.jingle.activecall)
  336. {
  337. connection.jingle.activecall.setVideoMute(
  338. mute, localCallback, options);
  339. }
  340. else {
  341. localCallback(mute);
  342. }
  343. },
  344. setAudioMute: function (mute, callback) {
  345. if (!(connection && APP.RTC.localAudio)) {
  346. return false;
  347. }
  348. if (this.forceMuted && !mute) {
  349. console.info("Asking focus for unmute");
  350. connection.moderate.setMute(connection.emuc.myroomjid, mute);
  351. // FIXME: wait for result before resetting muted status
  352. this.forceMuted = false;
  353. }
  354. if (mute == APP.RTC.localAudio.isMuted()) {
  355. // Nothing to do
  356. return true;
  357. }
  358. // It is not clear what is the right way to handle multiple tracks.
  359. // So at least make sure that they are all muted or all unmuted and
  360. // that we send presence just once.
  361. APP.RTC.localAudio.setMute(!mute);
  362. // isMuted is the opposite of audioEnabled
  363. this.sendAudioInfoPresence(mute, callback);
  364. return true;
  365. },
  366. sendAudioInfoPresence: function(mute, callback)
  367. {
  368. if(connection) {
  369. connection.emuc.addAudioInfoToPresence(mute);
  370. connection.emuc.sendPresence();
  371. }
  372. callback();
  373. return true;
  374. },
  375. // Really mute video, i.e. dont even send black frames
  376. muteVideo: function (pc, unmute) {
  377. // FIXME: this probably needs another of those lovely state safeguards...
  378. // which checks for iceconn == connected and sigstate == stable
  379. pc.setRemoteDescription(pc.remoteDescription,
  380. function () {
  381. pc.createAnswer(
  382. function (answer) {
  383. var sdp = new SDP(answer.sdp);
  384. if (sdp.media.length > 1) {
  385. if (unmute)
  386. sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
  387. else
  388. sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
  389. sdp.raw = sdp.session + sdp.media.join('');
  390. answer.sdp = sdp.raw;
  391. }
  392. pc.setLocalDescription(answer,
  393. function () {
  394. console.log('mute SLD ok');
  395. },
  396. function (error) {
  397. console.log('mute SLD error');
  398. APP.UI.messageHandler.showError("dialog.error",
  399. "dialog.SLDFailure");
  400. }
  401. );
  402. },
  403. function (error) {
  404. console.log(error);
  405. APP.UI.messageHandler.showError();
  406. }
  407. );
  408. },
  409. function (error) {
  410. console.log('muteVideo SRD error');
  411. APP.UI.messageHandler.showError("dialog.error",
  412. "dialog.SRDFailure");
  413. }
  414. );
  415. },
  416. toggleRecording: function (tokenEmptyCallback,
  417. startingCallback, startedCallback) {
  418. Recording.toggleRecording(tokenEmptyCallback,
  419. startingCallback, startedCallback, connection);
  420. },
  421. addToPresence: function (name, value, dontSend) {
  422. switch (name)
  423. {
  424. case "displayName":
  425. connection.emuc.addDisplayNameToPresence(value);
  426. break;
  427. case "etherpad":
  428. connection.emuc.addEtherpadToPresence(value);
  429. break;
  430. case "prezi":
  431. connection.emuc.addPreziToPresence(value, 0);
  432. break;
  433. case "preziSlide":
  434. connection.emuc.addCurrentSlideToPresence(value);
  435. break;
  436. case "connectionQuality":
  437. connection.emuc.addConnectionInfoToPresence(value);
  438. break;
  439. case "email":
  440. connection.emuc.addEmailToPresence(value);
  441. break;
  442. case "devices":
  443. connection.emuc.addDevicesToPresence(value);
  444. break;
  445. default :
  446. console.log("Unknown tag for presence: " + name);
  447. return;
  448. }
  449. if (!dontSend)
  450. connection.emuc.sendPresence();
  451. },
  452. /**
  453. * Sends 'data' as a log message to the focus. Returns true iff a message
  454. * was sent.
  455. * @param data
  456. * @returns {boolean} true iff a message was sent.
  457. */
  458. sendLogs: function (data) {
  459. if(!connection.emuc.focusMucJid)
  460. return false;
  461. var deflate = true;
  462. var content = JSON.stringify(data);
  463. if (deflate) {
  464. content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
  465. }
  466. content = Base64.encode(content);
  467. // XEP-0337-ish
  468. var message = $msg({to: connection.emuc.focusMucJid, type: 'normal'});
  469. message.c('log', { xmlns: 'urn:xmpp:eventlog',
  470. id: 'PeerConnectionStats'});
  471. message.c('message').t(content).up();
  472. if (deflate) {
  473. message.c('tag', {name: "deflated", value: "true"}).up();
  474. }
  475. message.up();
  476. connection.send(message);
  477. return true;
  478. },
  479. populateData: function () {
  480. var data = {};
  481. if (connection.jingle) {
  482. data = connection.jingle.populateData();
  483. }
  484. return data;
  485. },
  486. getLogger: function () {
  487. if(connection.logger)
  488. return connection.logger.log;
  489. return null;
  490. },
  491. getPrezi: function () {
  492. return connection.emuc.getPrezi(this.myJid());
  493. },
  494. removePreziFromPresence: function () {
  495. connection.emuc.removePreziFromPresence();
  496. connection.emuc.sendPresence();
  497. },
  498. sendChatMessage: function (message, nickname) {
  499. connection.emuc.sendMessage(message, nickname);
  500. },
  501. setSubject: function (topic) {
  502. connection.emuc.setSubject(topic);
  503. },
  504. lockRoom: function (key, onSuccess, onError, onNotSupported) {
  505. connection.emuc.lockRoom(key, onSuccess, onError, onNotSupported);
  506. },
  507. dial: function (to, from, roomName,roomPass) {
  508. connection.rayo.dial(to, from, roomName,roomPass);
  509. },
  510. setMute: function (jid, mute) {
  511. connection.moderate.setMute(jid, mute);
  512. },
  513. eject: function (jid) {
  514. connection.moderate.eject(jid);
  515. },
  516. logout: function (callback) {
  517. Moderator.logout(callback);
  518. },
  519. findJidFromResource: function (resource) {
  520. return connection.emuc.findJidFromResource(resource);
  521. },
  522. getMembers: function () {
  523. return connection.emuc.members;
  524. },
  525. getJidFromSSRC: function (ssrc) {
  526. if(!connection)
  527. return null;
  528. return connection.emuc.ssrc2jid[ssrc];
  529. },
  530. getMUCJoined: function () {
  531. return connection.emuc.joined;
  532. },
  533. getSessions: function () {
  534. return connection.jingle.sessions;
  535. },
  536. removeStream: function (stream) {
  537. if(!connection || !connection.jingle.activecall ||
  538. !connection.jingle.activecall.peerconnection)
  539. return;
  540. connection.jingle.activecall.peerconnection.removeStream(stream);
  541. }
  542. };
  543. module.exports = XMPP;