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

xmpp.js 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  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. var unload = (function () {
  170. var unloaded = false;
  171. return function () {
  172. if (unloaded) { return; }
  173. unloaded = true;
  174. if (connection && connection.connected) {
  175. // ensure signout
  176. $.ajax({
  177. type: 'POST',
  178. url: config.bosh,
  179. async: false,
  180. cache: false,
  181. contentType: 'application/xml',
  182. data: "<body rid='" + (connection.rid || connection._proto.rid) +
  183. "' xmlns='http://jabber.org/protocol/httpbind' sid='" +
  184. (connection.sid || connection._proto.sid) +
  185. "' type='terminate'>" +
  186. "<presence xmlns='jabber:client' type='unavailable'/>" +
  187. "</body>",
  188. success: function (data) {
  189. console.log('signed out');
  190. console.log(data);
  191. },
  192. error: function (XMLHttpRequest, textStatus, errorThrown) {
  193. console.log('signout error',
  194. textStatus + ' (' + errorThrown + ')');
  195. }
  196. });
  197. }
  198. XMPP.disposeConference(true);
  199. };
  200. })();
  201. function setupEvents() {
  202. // In recent versions of FF the 'beforeunload' event is not fired when the
  203. // window or the tab is closed. It is only fired when we leave the page
  204. // (change URL). If this participant doesn't unload properly, then it
  205. // becomes a ghost for the rest of the participants that stay in the
  206. // conference. Thankfully handling the 'unload' event in addition to the
  207. // 'beforeunload' event seems to garante the execution of the 'unload'
  208. // method at least once.
  209. //
  210. // The 'unload' method can safely be run multiple times, it will actually do
  211. // something only the first time that it's run, so we're don't have to worry
  212. // about browsers that fire both events.
  213. $(window).bind('beforeunload', unload);
  214. $(window).bind('unload', unload);
  215. }
  216. var XMPP = {
  217. getConnection: function(){ return connection; },
  218. sessionTerminated: false,
  219. /**
  220. * XMPP connection status
  221. */
  222. Status: Strophe.Status,
  223. /**
  224. * Remembers if we were muted by the focus.
  225. * @type {boolean}
  226. */
  227. forceMuted: false,
  228. start: function () {
  229. setupEvents();
  230. initStrophePlugins();
  231. registerListeners();
  232. Moderator.init(this, eventEmitter);
  233. var configDomain = config.hosts.anonymousdomain || config.hosts.domain;
  234. // Force authenticated domain if room is appended with '?login=true'
  235. if (config.hosts.anonymousdomain &&
  236. window.location.search.indexOf("login=true") !== -1) {
  237. configDomain = config.hosts.domain;
  238. }
  239. var jid = configDomain || window.location.hostname;
  240. connect(jid, null);
  241. },
  242. createConnection: function () {
  243. var bosh = config.bosh || '/http-bind';
  244. return new Strophe.Connection(bosh);
  245. },
  246. getStatusString: function (status) {
  247. return Strophe.getStatusString(status);
  248. },
  249. promptLogin: function () {
  250. // FIXME: re-use LoginDialog which supports retries
  251. APP.UI.showLoginPopup(connect);
  252. },
  253. joinRoom: function(roomName, useNicks, nick)
  254. {
  255. var roomjid;
  256. roomjid = roomName;
  257. if (useNicks) {
  258. if (nick) {
  259. roomjid += '/' + nick;
  260. } else {
  261. roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
  262. }
  263. } else {
  264. var tmpJid = Strophe.getNodeFromJid(connection.jid);
  265. if(!authenticatedUser)
  266. tmpJid = tmpJid.substr(0, 8);
  267. roomjid += '/' + tmpJid;
  268. }
  269. connection.emuc.doJoin(roomjid);
  270. },
  271. myJid: function () {
  272. if(!connection)
  273. return null;
  274. return connection.emuc.myroomjid;
  275. },
  276. myResource: function () {
  277. if(!connection || ! connection.emuc.myroomjid)
  278. return null;
  279. return Strophe.getResourceFromJid(connection.emuc.myroomjid);
  280. },
  281. disposeConference: function (onUnload) {
  282. eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE, onUnload);
  283. var handler = connection.jingle.activecall;
  284. if (handler && handler.peerconnection) {
  285. // FIXME: probably removing streams is not required and close() should
  286. // be enough
  287. if (APP.RTC.localAudio) {
  288. handler.peerconnection.removeStream(
  289. APP.RTC.localAudio.getOriginalStream(), onUnload);
  290. }
  291. if (APP.RTC.localVideo) {
  292. handler.peerconnection.removeStream(
  293. APP.RTC.localVideo.getOriginalStream(), onUnload);
  294. }
  295. handler.peerconnection.close();
  296. }
  297. connection.jingle.activecall = null;
  298. if(!onUnload)
  299. {
  300. this.sessionTerminated = true;
  301. connection.emuc.doLeave();
  302. }
  303. },
  304. addListener: function(type, listener)
  305. {
  306. eventEmitter.on(type, listener);
  307. },
  308. removeListener: function (type, listener) {
  309. eventEmitter.removeListener(type, listener);
  310. },
  311. allocateConferenceFocus: function(roomName, callback) {
  312. Moderator.allocateConferenceFocus(roomName, callback);
  313. },
  314. getLoginUrl: function (roomName, callback) {
  315. Moderator.getLoginUrl(roomName, callback);
  316. },
  317. getPopupLoginUrl: function (roomName, callback) {
  318. Moderator.getPopupLoginUrl(roomName, callback);
  319. },
  320. isModerator: function () {
  321. return Moderator.isModerator();
  322. },
  323. isSipGatewayEnabled: function () {
  324. return Moderator.isSipGatewayEnabled();
  325. },
  326. isExternalAuthEnabled: function () {
  327. return Moderator.isExternalAuthEnabled();
  328. },
  329. switchStreams: function (stream, oldStream, callback, isAudio) {
  330. if (connection && connection.jingle.activecall) {
  331. // FIXME: will block switchInProgress on true value in case of exception
  332. connection.jingle.activecall.switchStreams(stream, oldStream, callback, isAudio);
  333. } else {
  334. // We are done immediately
  335. console.warn("No conference handler or conference not started yet");
  336. callback();
  337. }
  338. },
  339. sendVideoInfoPresence: function (mute) {
  340. if(!connection)
  341. return;
  342. connection.emuc.addVideoInfoToPresence(mute);
  343. connection.emuc.sendPresence();
  344. },
  345. setVideoMute: function (mute, callback, options) {
  346. if(!connection)
  347. return;
  348. var self = this;
  349. var localCallback = function (mute) {
  350. self.sendVideoInfoPresence(mute);
  351. return callback(mute);
  352. };
  353. if(connection.jingle.activecall)
  354. {
  355. connection.jingle.activecall.setVideoMute(
  356. mute, localCallback, options);
  357. }
  358. else {
  359. localCallback(mute);
  360. }
  361. },
  362. setAudioMute: function (mute, callback) {
  363. if (!(connection && APP.RTC.localAudio)) {
  364. return false;
  365. }
  366. if (this.forceMuted && !mute) {
  367. console.info("Asking focus for unmute");
  368. connection.moderate.setMute(connection.emuc.myroomjid, mute);
  369. // FIXME: wait for result before resetting muted status
  370. this.forceMuted = false;
  371. }
  372. if (mute == APP.RTC.localAudio.isMuted()) {
  373. // Nothing to do
  374. return true;
  375. }
  376. // It is not clear what is the right way to handle multiple tracks.
  377. // So at least make sure that they are all muted or all unmuted and
  378. // that we send presence just once.
  379. APP.RTC.localAudio.setMute(!mute);
  380. // isMuted is the opposite of audioEnabled
  381. this.sendAudioInfoPresence(mute, callback);
  382. return true;
  383. },
  384. sendAudioInfoPresence: function(mute, callback)
  385. {
  386. if(connection) {
  387. connection.emuc.addAudioInfoToPresence(mute);
  388. connection.emuc.sendPresence();
  389. }
  390. callback();
  391. return true;
  392. },
  393. // Really mute video, i.e. dont even send black frames
  394. muteVideo: function (pc, unmute) {
  395. // FIXME: this probably needs another of those lovely state safeguards...
  396. // which checks for iceconn == connected and sigstate == stable
  397. pc.setRemoteDescription(pc.remoteDescription,
  398. function () {
  399. pc.createAnswer(
  400. function (answer) {
  401. var sdp = new SDP(answer.sdp);
  402. if (sdp.media.length > 1) {
  403. if (unmute)
  404. sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
  405. else
  406. sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
  407. sdp.raw = sdp.session + sdp.media.join('');
  408. answer.sdp = sdp.raw;
  409. }
  410. pc.setLocalDescription(answer,
  411. function () {
  412. console.log('mute SLD ok');
  413. },
  414. function (error) {
  415. console.log('mute SLD error');
  416. APP.UI.messageHandler.showError("dialog.error",
  417. "dialog.SLDFailure");
  418. }
  419. );
  420. },
  421. function (error) {
  422. console.log(error);
  423. APP.UI.messageHandler.showError();
  424. }
  425. );
  426. },
  427. function (error) {
  428. console.log('muteVideo SRD error');
  429. APP.UI.messageHandler.showError("dialog.error",
  430. "dialog.SRDFailure");
  431. }
  432. );
  433. },
  434. toggleRecording: function (tokenEmptyCallback,
  435. startingCallback, startedCallback) {
  436. Recording.toggleRecording(tokenEmptyCallback,
  437. startingCallback, startedCallback, connection);
  438. },
  439. addToPresence: function (name, value, dontSend) {
  440. switch (name)
  441. {
  442. case "displayName":
  443. connection.emuc.addDisplayNameToPresence(value);
  444. break;
  445. case "etherpad":
  446. connection.emuc.addEtherpadToPresence(value);
  447. break;
  448. case "prezi":
  449. connection.emuc.addPreziToPresence(value, 0);
  450. break;
  451. case "preziSlide":
  452. connection.emuc.addCurrentSlideToPresence(value);
  453. break;
  454. case "connectionQuality":
  455. connection.emuc.addConnectionInfoToPresence(value);
  456. break;
  457. case "email":
  458. connection.emuc.addEmailToPresence(value);
  459. break;
  460. case "devices":
  461. connection.emuc.addDevicesToPresence(value);
  462. break;
  463. case "startMuted":
  464. if(!Moderator.isModerator())
  465. return;
  466. connection.emuc.addStartMutedToPresence(value[0],
  467. value[1]);
  468. break;
  469. default :
  470. console.log("Unknown tag for presence: " + name);
  471. return;
  472. }
  473. if (!dontSend)
  474. connection.emuc.sendPresence();
  475. },
  476. /**
  477. * Sends 'data' as a log message to the focus. Returns true iff a message
  478. * was sent.
  479. * @param data
  480. * @returns {boolean} true iff a message was sent.
  481. */
  482. sendLogs: function (data) {
  483. if(!connection.emuc.focusMucJid)
  484. return false;
  485. var deflate = true;
  486. var content = JSON.stringify(data);
  487. if (deflate) {
  488. content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
  489. }
  490. content = Base64.encode(content);
  491. // XEP-0337-ish
  492. var message = $msg({to: connection.emuc.focusMucJid, type: 'normal'});
  493. message.c('log', { xmlns: 'urn:xmpp:eventlog',
  494. id: 'PeerConnectionStats'});
  495. message.c('message').t(content).up();
  496. if (deflate) {
  497. message.c('tag', {name: "deflated", value: "true"}).up();
  498. }
  499. message.up();
  500. connection.send(message);
  501. return true;
  502. },
  503. populateData: function () {
  504. var data = {};
  505. if (connection.jingle) {
  506. data = connection.jingle.populateData();
  507. }
  508. return data;
  509. },
  510. getLogger: function () {
  511. if(connection.logger)
  512. return connection.logger.log;
  513. return null;
  514. },
  515. getPrezi: function () {
  516. return connection.emuc.getPrezi(this.myJid());
  517. },
  518. removePreziFromPresence: function () {
  519. connection.emuc.removePreziFromPresence();
  520. connection.emuc.sendPresence();
  521. },
  522. sendChatMessage: function (message, nickname) {
  523. connection.emuc.sendMessage(message, nickname);
  524. },
  525. setSubject: function (topic) {
  526. connection.emuc.setSubject(topic);
  527. },
  528. lockRoom: function (key, onSuccess, onError, onNotSupported) {
  529. connection.emuc.lockRoom(key, onSuccess, onError, onNotSupported);
  530. },
  531. dial: function (to, from, roomName,roomPass) {
  532. connection.rayo.dial(to, from, roomName,roomPass);
  533. },
  534. setMute: function (jid, mute) {
  535. connection.moderate.setMute(jid, mute);
  536. },
  537. eject: function (jid) {
  538. connection.moderate.eject(jid);
  539. },
  540. logout: function (callback) {
  541. Moderator.logout(callback);
  542. },
  543. findJidFromResource: function (resource) {
  544. return connection.emuc.findJidFromResource(resource);
  545. },
  546. getMembers: function () {
  547. return connection.emuc.members;
  548. },
  549. getJidFromSSRC: function (ssrc) {
  550. if(!connection)
  551. return null;
  552. return connection.emuc.ssrc2jid[ssrc];
  553. },
  554. getMUCJoined: function () {
  555. return connection.emuc.joined;
  556. },
  557. getSessions: function () {
  558. return connection.jingle.sessions;
  559. },
  560. removeStream: function (stream) {
  561. if(!connection || !connection.jingle.activecall ||
  562. !connection.jingle.activecall.peerconnection)
  563. return;
  564. connection.jingle.activecall.peerconnection.removeStream(stream);
  565. }
  566. };
  567. module.exports = XMPP;