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.

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