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.

JitsiConference.js 39KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239
  1. /* global Strophe, $, Promise */
  2. /* jshint -W101 */
  3. var logger = require("jitsi-meet-logger").getLogger(__filename);
  4. var RTC = require("./modules/RTC/RTC");
  5. var XMPPEvents = require("./service/xmpp/XMPPEvents");
  6. var EventEmitter = require("events");
  7. var JitsiConferenceEvents = require("./JitsiConferenceEvents");
  8. var JitsiConferenceErrors = require("./JitsiConferenceErrors");
  9. var JitsiParticipant = require("./JitsiParticipant");
  10. var Statistics = require("./modules/statistics/statistics");
  11. var JitsiDTMFManager = require('./modules/DTMF/JitsiDTMFManager');
  12. var JitsiTrackEvents = require("./JitsiTrackEvents");
  13. var JitsiTrackErrors = require("./JitsiTrackErrors");
  14. var JitsiTrackError = require("./JitsiTrackError");
  15. var Settings = require("./modules/settings/Settings");
  16. var ComponentsVersions = require("./modules/version/ComponentsVersions");
  17. var GlobalOnErrorHandler = require("./modules/util/GlobalOnErrorHandler");
  18. var JitsiConferenceEventManager = require("./JitsiConferenceEventManager");
  19. /**
  20. * Creates a JitsiConference object with the given name and properties.
  21. * Note: this constructor is not a part of the public API (objects should be
  22. * created using JitsiConnection.createConference).
  23. * @param options.config properties / settings related to the conference that will be created.
  24. * @param options.name the name of the conference
  25. * @param options.connection the JitsiConnection object for this JitsiConference.
  26. * @constructor
  27. */
  28. function JitsiConference(options) {
  29. if(!options.name || options.name.toLowerCase() !== options.name) {
  30. var errmsg
  31. = "Invalid conference name (no conference name passed or it "
  32. + "contains invalid characters like capital letters)!";
  33. logger.error(errmsg);
  34. throw new Error(errmsg);
  35. }
  36. this.eventEmitter = new EventEmitter();
  37. this.settings = new Settings();
  38. this.options = options;
  39. this.eventManager = new JitsiConferenceEventManager(this);
  40. this._init(options);
  41. this.componentsVersions = new ComponentsVersions(this);
  42. this.participants = {};
  43. this.lastDominantSpeaker = null;
  44. this.dtmfManager = null;
  45. this.somebodySupportsDTMF = false;
  46. this.authEnabled = false;
  47. this.authIdentity;
  48. this.startAudioMuted = false;
  49. this.startVideoMuted = false;
  50. this.startMutedPolicy = {audio: false, video: false};
  51. this.availableDevices = {
  52. audio: undefined,
  53. video: undefined
  54. };
  55. this.isMutedByFocus = false;
  56. this.reportedAudioSSRCs = {};
  57. }
  58. /**
  59. * Initializes the conference object properties
  60. * @param options {object}
  61. * @param connection {JitsiConnection} overrides this.connection
  62. */
  63. JitsiConference.prototype._init = function (options) {
  64. if(!options)
  65. options = {};
  66. // Override connection and xmpp properties (Usefull if the connection
  67. // reloaded)
  68. if(options.connection) {
  69. this.connection = options.connection;
  70. this.xmpp = this.connection.xmpp;
  71. // Setup XMPP events only if we have new connection object.
  72. this.eventManager.setupXMPPListeners();
  73. }
  74. this.room = this.xmpp.createRoom(this.options.name, this.options.config,
  75. this.settings);
  76. this.room.updateDeviceAvailability(RTC.getDeviceAvailability());
  77. if(!this.rtc) {
  78. this.rtc = new RTC(this, options);
  79. this.eventManager.setupRTCListeners();
  80. }
  81. if(!this.statistics) {
  82. this.statistics = new Statistics(this.xmpp, {
  83. callStatsID: this.options.config.callStatsID,
  84. callStatsSecret: this.options.config.callStatsSecret,
  85. roomName: this.options.name
  86. });
  87. }
  88. this.eventManager.setupChatRoomListeners();
  89. // Always add listeners because on reload we are executing leave and the
  90. // listeners are removed from statistics module.
  91. this.eventManager.setupStatisticsListeners();
  92. }
  93. /**
  94. * Joins the conference.
  95. * @param password {string} the password
  96. */
  97. JitsiConference.prototype.join = function (password) {
  98. if(this.room)
  99. this.room.join(password);
  100. };
  101. /**
  102. * Check if joined to the conference.
  103. */
  104. JitsiConference.prototype.isJoined = function () {
  105. return this.room && this.room.joined;
  106. };
  107. /**
  108. * Leaves the conference and calls onMemberLeft for every participant.
  109. */
  110. JitsiConference.prototype._leaveRoomAndRemoveParticipants = function () {
  111. // remove all participants
  112. this.getParticipants().forEach(function (participant) {
  113. this.onMemberLeft(participant.getJid());
  114. }.bind(this));
  115. // leave the conference
  116. if (this.room) {
  117. this.room.leave();
  118. }
  119. this.room = null;
  120. this.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_LEFT);
  121. }
  122. /**
  123. * Leaves the conference.
  124. * @returns {Promise}
  125. */
  126. JitsiConference.prototype.leave = function () {
  127. var conference = this;
  128. this.statistics.stopCallStats();
  129. this.rtc.closeAllDataChannels();
  130. return Promise.all(
  131. conference.getLocalTracks().map(function (track) {
  132. return conference.removeTrack(track);
  133. })
  134. ).then(this._leaveRoomAndRemoveParticipants.bind(this))
  135. .catch(function (error) {
  136. logger.error(error);
  137. GlobalOnErrorHandler.callUnhandledRejectionHandler(
  138. {promise: this, reason: error});
  139. // We are proceeding with leaving the conference because room.leave may
  140. // succeed.
  141. this._leaveRoomAndRemoveParticipants();
  142. return Promise.resolve();
  143. }.bind(this));
  144. };
  145. /**
  146. * Returns name of this conference.
  147. */
  148. JitsiConference.prototype.getName = function () {
  149. return this.options.name;
  150. };
  151. /**
  152. * Check if authentication is enabled for this conference.
  153. */
  154. JitsiConference.prototype.isAuthEnabled = function () {
  155. return this.authEnabled;
  156. };
  157. /**
  158. * Check if user is logged in.
  159. */
  160. JitsiConference.prototype.isLoggedIn = function () {
  161. return !!this.authIdentity;
  162. };
  163. /**
  164. * Get authorized login.
  165. */
  166. JitsiConference.prototype.getAuthLogin = function () {
  167. return this.authIdentity;
  168. };
  169. /**
  170. * Check if external authentication is enabled for this conference.
  171. */
  172. JitsiConference.prototype.isExternalAuthEnabled = function () {
  173. return this.room && this.room.moderator.isExternalAuthEnabled();
  174. };
  175. /**
  176. * Get url for external authentication.
  177. * @param {boolean} [urlForPopup] if true then return url for login popup,
  178. * else url of login page.
  179. * @returns {Promise}
  180. */
  181. JitsiConference.prototype.getExternalAuthUrl = function (urlForPopup) {
  182. return new Promise(function (resolve, reject) {
  183. if (!this.isExternalAuthEnabled()) {
  184. reject();
  185. return;
  186. }
  187. if (urlForPopup) {
  188. this.room.moderator.getPopupLoginUrl(resolve, reject);
  189. } else {
  190. this.room.moderator.getLoginUrl(resolve, reject);
  191. }
  192. }.bind(this));
  193. };
  194. /**
  195. * Returns the local tracks.
  196. */
  197. JitsiConference.prototype.getLocalTracks = function () {
  198. if (this.rtc) {
  199. return this.rtc.localTracks.slice();
  200. } else {
  201. return [];
  202. }
  203. };
  204. /**
  205. * Attaches a handler for events(For example - "participant joined".) in the conference. All possible event are defined
  206. * in JitsiConferenceEvents.
  207. * @param eventId the event ID.
  208. * @param handler handler for the event.
  209. *
  210. * Note: consider adding eventing functionality by extending an EventEmitter impl, instead of rolling ourselves
  211. */
  212. JitsiConference.prototype.on = function (eventId, handler) {
  213. if(this.eventEmitter)
  214. this.eventEmitter.on(eventId, handler);
  215. };
  216. /**
  217. * Removes event listener
  218. * @param eventId the event ID.
  219. * @param [handler] optional, the specific handler to unbind
  220. *
  221. * Note: consider adding eventing functionality by extending an EventEmitter impl, instead of rolling ourselves
  222. */
  223. JitsiConference.prototype.off = function (eventId, handler) {
  224. if(this.eventEmitter)
  225. this.eventEmitter.removeListener(eventId, handler);
  226. };
  227. // Common aliases for event emitter
  228. JitsiConference.prototype.addEventListener = JitsiConference.prototype.on;
  229. JitsiConference.prototype.removeEventListener = JitsiConference.prototype.off;
  230. /**
  231. * Receives notifications from other participants about commands / custom events
  232. * (sent by sendCommand or sendCommandOnce methods).
  233. * @param command {String} the name of the command
  234. * @param handler {Function} handler for the command
  235. */
  236. JitsiConference.prototype.addCommandListener = function (command, handler) {
  237. if(this.room)
  238. this.room.addPresenceListener(command, handler);
  239. };
  240. /**
  241. * Removes command listener
  242. * @param command {String} the name of the command
  243. */
  244. JitsiConference.prototype.removeCommandListener = function (command) {
  245. if(this.room)
  246. this.room.removePresenceListener(command);
  247. };
  248. /**
  249. * Sends text message to the other participants in the conference
  250. * @param message the text message.
  251. */
  252. JitsiConference.prototype.sendTextMessage = function (message) {
  253. if(this.room)
  254. this.room.sendMessage(message);
  255. };
  256. /**
  257. * Send presence command.
  258. * @param name {String} the name of the command.
  259. * @param values {Object} with keys and values that will be sent.
  260. **/
  261. JitsiConference.prototype.sendCommand = function (name, values) {
  262. if(this.room) {
  263. this.room.addToPresence(name, values);
  264. this.room.sendPresence();
  265. }
  266. };
  267. /**
  268. * Send presence command one time.
  269. * @param name {String} the name of the command.
  270. * @param values {Object} with keys and values that will be sent.
  271. **/
  272. JitsiConference.prototype.sendCommandOnce = function (name, values) {
  273. this.sendCommand(name, values);
  274. this.removeCommand(name);
  275. };
  276. /**
  277. * Removes presence command.
  278. * @param name {String} the name of the command.
  279. **/
  280. JitsiConference.prototype.removeCommand = function (name) {
  281. if(this.room)
  282. this.room.removeFromPresence(name);
  283. };
  284. /**
  285. * Sets the display name for this conference.
  286. * @param name the display name to set
  287. */
  288. JitsiConference.prototype.setDisplayName = function(name) {
  289. if(this.room){
  290. // remove previously set nickname
  291. this.room.removeFromPresence("nick");
  292. this.room.addToPresence("nick", {attributes: {xmlns: 'http://jabber.org/protocol/nick'}, value: name});
  293. this.room.sendPresence();
  294. }
  295. };
  296. /**
  297. * Set new subject for this conference. (available only for moderator)
  298. * @param {string} subject new subject
  299. */
  300. JitsiConference.prototype.setSubject = function (subject) {
  301. if (this.room && this.isModerator()) {
  302. this.room.setSubject(subject);
  303. }
  304. };
  305. /**
  306. * Adds JitsiLocalTrack object to the conference.
  307. * @param track the JitsiLocalTrack object.
  308. * @returns {Promise<JitsiLocalTrack>}
  309. * @throws {Error} if the specified track is a video track and there is already
  310. * another video track in the conference.
  311. */
  312. JitsiConference.prototype.addTrack = function (track) {
  313. if (track.disposed) {
  314. return Promise.reject(
  315. new JitsiTrackError(JitsiTrackErrors.TRACK_IS_DISPOSED));
  316. }
  317. if (track.isVideoTrack()) {
  318. // Ensure there's exactly 1 local video track in the conference.
  319. var localVideoTrack = this.rtc.getLocalVideoTrack();
  320. if (localVideoTrack) {
  321. // Don't be excessively harsh and severe if the API client happens
  322. // to attempt to add the same local video track twice.
  323. if (track === localVideoTrack) {
  324. return Promise.resolve(track);
  325. } else {
  326. return Promise.reject(new Error(
  327. "cannot add second video track to the conference"));
  328. }
  329. }
  330. }
  331. track.ssrcHandler = function (conference, ssrcMap) {
  332. if(ssrcMap[this.getMSID()]){
  333. this._setSSRC(ssrcMap[this.getMSID()]);
  334. conference.room.removeListener(XMPPEvents.SENDRECV_STREAMS_CHANGED,
  335. this.ssrcHandler);
  336. }
  337. }.bind(track, this);
  338. this.room.addListener(XMPPEvents.SENDRECV_STREAMS_CHANGED,
  339. track.ssrcHandler);
  340. if(track.isAudioTrack() || (track.isVideoTrack() &&
  341. track.videoType !== "desktop")) {
  342. // Report active device to statistics
  343. var devices = RTC.getCurrentlyAvailableMediaDevices();
  344. device = devices.find(function (d) {
  345. return d.kind === track.getTrack().kind + 'input'
  346. && d.label === track.getTrack().label;
  347. });
  348. if(device)
  349. Statistics.sendActiveDeviceListEvent(
  350. RTC.getEventDataForActiveDevice(device));
  351. }
  352. return new Promise(function (resolve, reject) {
  353. this.room.addStream(track.getOriginalStream(), function () {
  354. if (track.isVideoTrack()) {
  355. this.removeCommand("videoType");
  356. this.sendCommand("videoType", {
  357. value: track.videoType,
  358. attributes: {
  359. xmlns: 'http://jitsi.org/jitmeet/video'
  360. }
  361. });
  362. }
  363. this.rtc.addLocalTrack(track);
  364. if (track.startMuted) {
  365. track.mute();
  366. }
  367. // ensure that we're sharing proper "is muted" state
  368. if (track.isAudioTrack()) {
  369. this.room.setAudioMute(track.isMuted());
  370. } else {
  371. this.room.setVideoMute(track.isMuted());
  372. }
  373. track.muteHandler = this._fireMuteChangeEvent.bind(this, track);
  374. track.audioLevelHandler = this._fireAudioLevelChangeEvent.bind(this);
  375. track.addEventListener(JitsiTrackEvents.TRACK_MUTE_CHANGED,
  376. track.muteHandler);
  377. track.addEventListener(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
  378. track.audioLevelHandler);
  379. track._setConference(this);
  380. // send event for starting screen sharing
  381. // FIXME: we assume we have only one screen sharing track
  382. // if we change this we need to fix this check
  383. if (track.isVideoTrack() && track.videoType === "desktop")
  384. this.statistics.sendScreenSharingEvent(true);
  385. this.eventEmitter.emit(JitsiConferenceEvents.TRACK_ADDED, track);
  386. resolve(track);
  387. }.bind(this), function (error) {
  388. reject(error);
  389. });
  390. }.bind(this));
  391. };
  392. /**
  393. * Fires TRACK_AUDIO_LEVEL_CHANGED change conference event.
  394. * @param audioLevel the audio level
  395. */
  396. JitsiConference.prototype._fireAudioLevelChangeEvent = function (audioLevel) {
  397. this.eventEmitter.emit(
  398. JitsiConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED,
  399. this.myUserId(), audioLevel);
  400. };
  401. /**
  402. * Fires TRACK_MUTE_CHANGED change conference event.
  403. * @param track the JitsiTrack object related to the event.
  404. */
  405. JitsiConference.prototype._fireMuteChangeEvent = function (track) {
  406. // check if track was muted by focus and now is unmuted by user
  407. if (this.isMutedByFocus && track.isAudioTrack() && !track.isMuted()) {
  408. this.isMutedByFocus = false;
  409. // unmute local user on server
  410. this.room.muteParticipant(this.room.myroomjid, false);
  411. }
  412. this.eventEmitter.emit(JitsiConferenceEvents.TRACK_MUTE_CHANGED, track);
  413. };
  414. /**
  415. * Clear JitsiLocalTrack properties and listeners.
  416. * @param track the JitsiLocalTrack object.
  417. */
  418. JitsiConference.prototype.onTrackRemoved = function (track) {
  419. track._setSSRC(null);
  420. track._setConference(null);
  421. this.rtc.removeLocalTrack(track);
  422. track.removeEventListener(JitsiTrackEvents.TRACK_MUTE_CHANGED,
  423. track.muteHandler);
  424. track.removeEventListener(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
  425. track.audioLevelHandler);
  426. this.room.removeListener(XMPPEvents.SENDRECV_STREAMS_CHANGED,
  427. track.ssrcHandler);
  428. // send event for stopping screen sharing
  429. // FIXME: we assume we have only one screen sharing track
  430. // if we change this we need to fix this check
  431. if (track.isVideoTrack() && track.videoType === "desktop")
  432. this.statistics.sendScreenSharingEvent(false);
  433. this.eventEmitter.emit(JitsiConferenceEvents.TRACK_REMOVED, track);
  434. }
  435. /**
  436. * Removes JitsiLocalTrack object to the conference.
  437. * @param track the JitsiLocalTrack object.
  438. * @returns {Promise}
  439. */
  440. JitsiConference.prototype.removeTrack = function (track) {
  441. if (track.disposed) {
  442. return Promise.reject(
  443. new JitsiTrackError(JitsiTrackErrors.TRACK_IS_DISPOSED));
  444. }
  445. if(!this.room){
  446. if(this.rtc) {
  447. this.onTrackRemoved(track);
  448. }
  449. return Promise.resolve();
  450. }
  451. return new Promise(function (resolve, reject) {
  452. this.room.removeStream(track.getOriginalStream(), function(){
  453. this.onTrackRemoved(track);
  454. resolve();
  455. }.bind(this), function (error) {
  456. reject(error);
  457. }, {
  458. mtype: track.getType(),
  459. type: "remove",
  460. ssrc: track.ssrc});
  461. }.bind(this));
  462. };
  463. /**
  464. * Get role of the local user.
  465. * @returns {string} user role: 'moderator' or 'none'
  466. */
  467. JitsiConference.prototype.getRole = function () {
  468. return this.room.role;
  469. };
  470. /**
  471. * Check if local user is moderator.
  472. * @returns {boolean} true if local user is moderator, false otherwise.
  473. */
  474. JitsiConference.prototype.isModerator = function () {
  475. return this.room.isModerator();
  476. };
  477. /**
  478. * Set password for the room.
  479. * @param {string} password new password for the room.
  480. * @returns {Promise}
  481. */
  482. JitsiConference.prototype.lock = function (password) {
  483. if (!this.isModerator()) {
  484. return Promise.reject();
  485. }
  486. var conference = this;
  487. return new Promise(function (resolve, reject) {
  488. conference.room.lockRoom(password || "", function () {
  489. resolve();
  490. }, function (err) {
  491. reject(err);
  492. }, function () {
  493. reject(JitsiConferenceErrors.PASSWORD_NOT_SUPPORTED);
  494. });
  495. });
  496. };
  497. /**
  498. * Remove password from the room.
  499. * @returns {Promise}
  500. */
  501. JitsiConference.prototype.unlock = function () {
  502. return this.lock();
  503. };
  504. /**
  505. * Elects the participant with the given id to be the selected participant in
  506. * order to receive higher video quality (if simulcast is enabled).
  507. * @param participantId the identifier of the participant
  508. * @throws NetworkError or InvalidStateError or Error if the operation fails.
  509. */
  510. JitsiConference.prototype.selectParticipant = function(participantId) {
  511. this.rtc.selectEndpoint(participantId);
  512. };
  513. /**
  514. * Elects the participant with the given id to be the pinned participant in
  515. * order to always receive video for this participant (even when last n is
  516. * enabled).
  517. * @param participantId the identifier of the participant
  518. * @throws NetworkError or InvalidStateError or Error if the operation fails.
  519. */
  520. JitsiConference.prototype.pinParticipant = function(participantId) {
  521. this.rtc.pinEndpoint(participantId);
  522. };
  523. /**
  524. * Returns the list of participants for this conference.
  525. * @return Array<JitsiParticipant> a list of participant identifiers containing all conference participants.
  526. */
  527. JitsiConference.prototype.getParticipants = function() {
  528. return Object.keys(this.participants).map(function (key) {
  529. return this.participants[key];
  530. }, this);
  531. };
  532. /**
  533. * @returns {JitsiParticipant} the participant in this conference with the specified id (or
  534. * undefined if there isn't one).
  535. * @param id the id of the participant.
  536. */
  537. JitsiConference.prototype.getParticipantById = function(id) {
  538. return this.participants[id];
  539. };
  540. /**
  541. * Kick participant from this conference.
  542. * @param {string} id id of the participant to kick
  543. */
  544. JitsiConference.prototype.kickParticipant = function (id) {
  545. var participant = this.getParticipantById(id);
  546. if (!participant) {
  547. return;
  548. }
  549. this.room.kick(participant.getJid());
  550. };
  551. /**
  552. * Kick participant from this conference.
  553. * @param {string} id id of the participant to kick
  554. */
  555. JitsiConference.prototype.muteParticipant = function (id) {
  556. var participant = this.getParticipantById(id);
  557. if (!participant) {
  558. return;
  559. }
  560. this.room.muteParticipant(participant.getJid(), true);
  561. };
  562. /**
  563. * Indicates that a participant has joined the conference.
  564. *
  565. * @param jid the jid of the participant in the MUC
  566. * @param nick the display name of the participant
  567. * @param role the role of the participant in the MUC
  568. * @param isHidden indicates if this is a hidden participant (sysem participant,
  569. * for example a recorder).
  570. */
  571. JitsiConference.prototype.onMemberJoined
  572. = function (jid, nick, role, isHidden) {
  573. var id = Strophe.getResourceFromJid(jid);
  574. if (id === 'focus' || this.myUserId() === id) {
  575. return;
  576. }
  577. var participant = new JitsiParticipant(jid, this, nick, isHidden);
  578. participant._role = role;
  579. this.participants[id] = participant;
  580. this.eventEmitter.emit(JitsiConferenceEvents.USER_JOINED, id, participant);
  581. // XXX Since disco is checked in multiple places (e.g.
  582. // modules/xmpp/strophe.jingle.js, modules/xmpp/strophe.rayo.js), check it
  583. // here as well.
  584. var disco = this.xmpp.connection.disco;
  585. if (disco) {
  586. disco.info(
  587. jid, "node", function(iq) {
  588. participant._supportsDTMF = $(iq).find(
  589. '>query>feature[var="urn:xmpp:jingle:dtmf:0"]').length > 0;
  590. this.updateDTMFSupport();
  591. }.bind(this)
  592. );
  593. } else {
  594. // FIXME Should participant._supportsDTMF be assigned false here (and
  595. // this.updateDTMFSupport invoked)?
  596. }
  597. };
  598. JitsiConference.prototype.onMemberLeft = function (jid) {
  599. var id = Strophe.getResourceFromJid(jid);
  600. if (id === 'focus' || this.myUserId() === id) {
  601. return;
  602. }
  603. var participant = this.participants[id];
  604. delete this.participants[id];
  605. var removedTracks = this.rtc.removeRemoteTracks(id);
  606. removedTracks.forEach(function (track) {
  607. this.eventEmitter.emit(JitsiConferenceEvents.TRACK_REMOVED, track);
  608. }.bind(this));
  609. this.eventEmitter.emit(JitsiConferenceEvents.USER_LEFT, id, participant);
  610. };
  611. JitsiConference.prototype.onUserRoleChanged = function (jid, role) {
  612. var id = Strophe.getResourceFromJid(jid);
  613. var participant = this.getParticipantById(id);
  614. if (!participant) {
  615. return;
  616. }
  617. participant._role = role;
  618. this.eventEmitter.emit(JitsiConferenceEvents.USER_ROLE_CHANGED, id, role);
  619. };
  620. JitsiConference.prototype.onDisplayNameChanged = function (jid, displayName) {
  621. var id = Strophe.getResourceFromJid(jid);
  622. var participant = this.getParticipantById(id);
  623. if (!participant) {
  624. return;
  625. }
  626. if (participant._displayName === displayName)
  627. return;
  628. participant._displayName = displayName;
  629. this.eventEmitter.emit(JitsiConferenceEvents.DISPLAY_NAME_CHANGED, id, displayName);
  630. };
  631. /**
  632. * Notifies this JitsiConference that a JitsiRemoteTrack was added (into the
  633. * ChatRoom of this JitsiConference).
  634. *
  635. * @param {JitsiRemoteTrack} track the JitsiRemoteTrack which was added to this
  636. * JitsiConference
  637. */
  638. JitsiConference.prototype.onTrackAdded = function (track) {
  639. var id = track.getParticipantId();
  640. var participant = this.getParticipantById(id);
  641. if (!participant) {
  642. return;
  643. }
  644. // Add track to JitsiParticipant.
  645. participant._tracks.push(track);
  646. var emitter = this.eventEmitter;
  647. track.addEventListener(
  648. JitsiTrackEvents.TRACK_MUTE_CHANGED,
  649. function () {
  650. emitter.emit(JitsiConferenceEvents.TRACK_MUTE_CHANGED, track);
  651. }
  652. );
  653. track.addEventListener(
  654. JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
  655. function (audioLevel) {
  656. emitter.emit(
  657. JitsiConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED,
  658. id,
  659. audioLevel);
  660. }
  661. );
  662. emitter.emit(JitsiConferenceEvents.TRACK_ADDED, track);
  663. };
  664. /**
  665. * Handles incoming call event.
  666. */
  667. JitsiConference.prototype.onIncomingCall =
  668. function (jingleSession, jingleOffer, now) {
  669. if (!this.room.isFocus(jingleSession.peerjid)) {
  670. // Error cause this should never happen unless something is wrong!
  671. var errmsg = "Rejecting session-initiate from non-focus user: "
  672. + jingleSession.peerjid;
  673. GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
  674. logger.error(errmsg);
  675. return;
  676. }
  677. // Accept incoming call
  678. this.room.setJingleSession(jingleSession);
  679. this.room.connectionTimes["session.initiate"] = now;
  680. Statistics.analytics.sendEvent("muc.idle",
  681. (now - this.room.connectionTimes["muc.joined"]));
  682. try{
  683. jingleSession.initialize(false /* initiator */,this.room);
  684. } catch (error) {
  685. GlobalOnErrorHandler.callErrorHandler(error);
  686. };
  687. this.rtc.onIncommingCall(jingleSession);
  688. // Add local Tracks to the ChatRoom
  689. this.rtc.localTracks.forEach(function(localTrack) {
  690. var ssrcInfo = null;
  691. if(localTrack.isVideoTrack() && localTrack.isMuted()) {
  692. /**
  693. * Handles issues when the stream is added before the peerconnection
  694. * is created. The peerconnection is created when second participant
  695. * enters the call. In that use case the track doesn't have
  696. * information about it's ssrcs and no jingle packets are sent. That
  697. * can cause inconsistent behavior later.
  698. *
  699. * For example:
  700. * If we mute the stream and than second participant enter it's
  701. * remote SDP won't include that track. On unmute we are not sending
  702. * any jingle packets which will brake the unmute.
  703. *
  704. * In order to solve issues like the above one here we have to
  705. * generate the ssrc information for the track .
  706. */
  707. localTrack._setSSRC(
  708. this.room.generateNewStreamSSRCInfo());
  709. ssrcInfo = {
  710. mtype: localTrack.getType(),
  711. type: "addMuted",
  712. ssrc: localTrack.ssrc,
  713. msid: localTrack.initialMSID
  714. };
  715. }
  716. try {
  717. this.room.addStream(
  718. localTrack.getOriginalStream(), function () {}, function () {},
  719. ssrcInfo, true);
  720. } catch(e) {
  721. GlobalOnErrorHandler.callErrorHandler(e);
  722. logger.error(e);
  723. }
  724. }.bind(this));
  725. jingleSession.acceptOffer(jingleOffer, null,
  726. function (error) {
  727. GlobalOnErrorHandler.callErrorHandler(error);
  728. logger.error(
  729. "Failed to accept incoming Jingle session", error);
  730. }
  731. );
  732. // Start callstats as soon as peerconnection is initialized,
  733. // do not wait for XMPPEvents.PEERCONNECTION_READY, as it may never
  734. // happen in case if user doesn't have or denied permission to
  735. // both camera and microphone.
  736. this.statistics.startCallStats(jingleSession, this.settings);
  737. this.statistics.startRemoteStats(jingleSession.peerconnection);
  738. }
  739. JitsiConference.prototype.updateDTMFSupport = function () {
  740. var somebodySupportsDTMF = false;
  741. var participants = this.getParticipants();
  742. // check if at least 1 participant supports DTMF
  743. for (var i = 0; i < participants.length; i += 1) {
  744. if (participants[i].supportsDTMF()) {
  745. somebodySupportsDTMF = true;
  746. break;
  747. }
  748. }
  749. if (somebodySupportsDTMF !== this.somebodySupportsDTMF) {
  750. this.somebodySupportsDTMF = somebodySupportsDTMF;
  751. this.eventEmitter.emit(JitsiConferenceEvents.DTMF_SUPPORT_CHANGED, somebodySupportsDTMF);
  752. }
  753. };
  754. /**
  755. * Allows to check if there is at least one user in the conference
  756. * that supports DTMF.
  757. * @returns {boolean} true if somebody supports DTMF, false otherwise
  758. */
  759. JitsiConference.prototype.isDTMFSupported = function () {
  760. return this.somebodySupportsDTMF;
  761. };
  762. /**
  763. * Returns the local user's ID
  764. * @return {string} local user's ID
  765. */
  766. JitsiConference.prototype.myUserId = function () {
  767. return (this.room && this.room.myroomjid)? Strophe.getResourceFromJid(this.room.myroomjid) : null;
  768. };
  769. JitsiConference.prototype.sendTones = function (tones, duration, pause) {
  770. if (!this.dtmfManager) {
  771. var connection = this.xmpp.connection.jingle.activecall.peerconnection;
  772. if (!connection) {
  773. logger.warn("cannot sendTones: no conneciton");
  774. return;
  775. }
  776. var tracks = this.getLocalTracks().filter(function (track) {
  777. return track.isAudioTrack();
  778. });
  779. if (!tracks.length) {
  780. logger.warn("cannot sendTones: no local audio stream");
  781. return;
  782. }
  783. this.dtmfManager = new JitsiDTMFManager(tracks[0], connection);
  784. }
  785. this.dtmfManager.sendTones(tones, duration, pause);
  786. };
  787. /**
  788. * Returns true if the recording is supproted and false if not.
  789. */
  790. JitsiConference.prototype.isRecordingSupported = function () {
  791. if(this.room)
  792. return this.room.isRecordingSupported();
  793. return false;
  794. };
  795. /**
  796. * Returns null if the recording is not supported, "on" if the recording started
  797. * and "off" if the recording is not started.
  798. */
  799. JitsiConference.prototype.getRecordingState = function () {
  800. return (this.room) ? this.room.getRecordingState() : undefined;
  801. }
  802. /**
  803. * Returns the url of the recorded video.
  804. */
  805. JitsiConference.prototype.getRecordingURL = function () {
  806. return (this.room) ? this.room.getRecordingURL() : null;
  807. }
  808. /**
  809. * Starts/stops the recording
  810. */
  811. JitsiConference.prototype.toggleRecording = function (options) {
  812. if(this.room)
  813. return this.room.toggleRecording(options, function (status, error) {
  814. this.eventEmitter.emit(
  815. JitsiConferenceEvents.RECORDER_STATE_CHANGED, status, error);
  816. }.bind(this));
  817. this.eventEmitter.emit(
  818. JitsiConferenceEvents.RECORDER_STATE_CHANGED, "error",
  819. new Error("The conference is not created yet!"));
  820. }
  821. /**
  822. * Returns true if the SIP calls are supported and false otherwise
  823. */
  824. JitsiConference.prototype.isSIPCallingSupported = function () {
  825. if(this.room)
  826. return this.room.isSIPCallingSupported();
  827. return false;
  828. }
  829. /**
  830. * Dials a number.
  831. * @param number the number
  832. */
  833. JitsiConference.prototype.dial = function (number) {
  834. if(this.room)
  835. return this.room.dial(number);
  836. return new Promise(function(resolve, reject){
  837. reject(new Error("The conference is not created yet!"))});
  838. }
  839. /**
  840. * Hangup an existing call
  841. */
  842. JitsiConference.prototype.hangup = function () {
  843. if(this.room)
  844. return this.room.hangup();
  845. return new Promise(function(resolve, reject){
  846. reject(new Error("The conference is not created yet!"))});
  847. }
  848. /**
  849. * Returns the phone number for joining the conference.
  850. */
  851. JitsiConference.prototype.getPhoneNumber = function () {
  852. if(this.room)
  853. return this.room.getPhoneNumber();
  854. return null;
  855. }
  856. /**
  857. * Returns the pin for joining the conference with phone.
  858. */
  859. JitsiConference.prototype.getPhonePin = function () {
  860. if(this.room)
  861. return this.room.getPhonePin();
  862. return null;
  863. }
  864. /**
  865. * Returns the connection state for the current room. Its ice connection state
  866. * for its session.
  867. */
  868. JitsiConference.prototype.getConnectionState = function () {
  869. if(this.room)
  870. return this.room.getConnectionState();
  871. return null;
  872. }
  873. /**
  874. * Make all new participants mute their audio/video on join.
  875. * @param policy {Object} object with 2 boolean properties for video and audio:
  876. * @param {boolean} audio if audio should be muted.
  877. * @param {boolean} video if video should be muted.
  878. */
  879. JitsiConference.prototype.setStartMutedPolicy = function (policy) {
  880. if (!this.isModerator()) {
  881. return;
  882. }
  883. this.startMutedPolicy = policy;
  884. this.room.removeFromPresence("startmuted");
  885. this.room.addToPresence("startmuted", {
  886. attributes: {
  887. audio: policy.audio,
  888. video: policy.video,
  889. xmlns: 'http://jitsi.org/jitmeet/start-muted'
  890. }
  891. });
  892. this.room.sendPresence();
  893. };
  894. /**
  895. * Returns current start muted policy
  896. * @returns {Object} with 2 proprties - audio and video.
  897. */
  898. JitsiConference.prototype.getStartMutedPolicy = function () {
  899. return this.startMutedPolicy;
  900. };
  901. /**
  902. * Check if audio is muted on join.
  903. */
  904. JitsiConference.prototype.isStartAudioMuted = function () {
  905. return this.startAudioMuted;
  906. };
  907. /**
  908. * Check if video is muted on join.
  909. */
  910. JitsiConference.prototype.isStartVideoMuted = function () {
  911. return this.startVideoMuted;
  912. };
  913. /**
  914. * Get object with internal logs.
  915. */
  916. JitsiConference.prototype.getLogs = function () {
  917. var data = this.xmpp.getJingleLog();
  918. var metadata = {};
  919. metadata.time = new Date();
  920. metadata.url = window.location.href;
  921. metadata.ua = navigator.userAgent;
  922. var log = this.xmpp.getXmppLog();
  923. if (log) {
  924. metadata.xmpp = log;
  925. }
  926. data.metadata = metadata;
  927. return data;
  928. };
  929. /**
  930. * Returns measured connectionTimes.
  931. */
  932. JitsiConference.prototype.getConnectionTimes = function () {
  933. return this.room.connectionTimes;
  934. };
  935. /**
  936. * Sets a property for the local participant.
  937. */
  938. JitsiConference.prototype.setLocalParticipantProperty = function(name, value) {
  939. this.sendCommand("jitsi_participant_" + name, {value: value});
  940. };
  941. /**
  942. * Sends the given feedback through CallStats if enabled.
  943. *
  944. * @param overallFeedback an integer between 1 and 5 indicating the
  945. * user feedback
  946. * @param detailedFeedback detailed feedback from the user. Not yet used
  947. */
  948. JitsiConference.prototype.sendFeedback =
  949. function(overallFeedback, detailedFeedback){
  950. this.statistics.sendFeedback(overallFeedback, detailedFeedback);
  951. }
  952. /**
  953. * Returns true if the callstats integration is enabled, otherwise returns
  954. * false.
  955. *
  956. * @returns true if the callstats integration is enabled, otherwise returns
  957. * false.
  958. */
  959. JitsiConference.prototype.isCallstatsEnabled = function () {
  960. return this.statistics.isCallstatsEnabled();
  961. }
  962. /**
  963. * Handles track attached to container (Calls associateStreamWithVideoTag method
  964. * from statistics module)
  965. * @param track the track
  966. * @param container the container
  967. */
  968. JitsiConference.prototype._onTrackAttach = function(track, container) {
  969. var ssrc = track.getSSRC();
  970. if (!container.id || !ssrc) {
  971. return;
  972. }
  973. this.statistics.associateStreamWithVideoTag(
  974. ssrc, track.isLocal(), track.getUsageLabel(), container.id);
  975. }
  976. /**
  977. * Reports detected audio problem with the media stream related to the passed
  978. * ssrc.
  979. * @param ssrc {string} the ssrc
  980. * NOTE: all logger.log calls are there only to be able to see the info in
  981. * torture
  982. */
  983. JitsiConference.prototype._reportAudioProblem = function (ssrc) {
  984. if(this.reportedAudioSSRCs[ssrc])
  985. return;
  986. var track = this.rtc.getRemoteTrackBySSRC(ssrc);
  987. if(!track || !track.isAudioTrack())
  988. return;
  989. var id = track.getParticipantId();
  990. var displayName = null;
  991. if(id) {
  992. var participant = this.getParticipantById(id);
  993. if(participant) {
  994. displayName = participant.getDisplayName();
  995. }
  996. }
  997. this.reportedAudioSSRCs[ssrc] = true;
  998. var errorContent = {
  999. errMsg: "The audio is received but not played",
  1000. ssrc: ssrc,
  1001. jid: id,
  1002. displayName: displayName
  1003. };
  1004. logger.log("=================The audio is received but not played" +
  1005. "======================");
  1006. logger.log("ssrc: ", ssrc);
  1007. logger.log("jid: ", id);
  1008. logger.log("displayName: ", displayName);
  1009. var mstream = track.stream, mtrack = track.track;
  1010. if(mstream) {
  1011. logger.log("MediaStream:");
  1012. errorContent.MediaStream = {
  1013. active: mstream.active,
  1014. id: mstream.id
  1015. };
  1016. logger.log("active: ", mstream.active);
  1017. logger.log("id: ", mstream.id);
  1018. }
  1019. if(mtrack) {
  1020. logger.log("MediaStreamTrack:");
  1021. errorContent.MediaStreamTrack = {
  1022. enabled: mtrack.enabled,
  1023. id: mtrack.id,
  1024. label: mtrack.label,
  1025. muted: mtrack.muted
  1026. }
  1027. logger.log("enabled: ", mtrack.enabled);
  1028. logger.log("id: ", mtrack.id);
  1029. logger.log("label: ", mtrack.label);
  1030. logger.log("muted: ", mtrack.muted);
  1031. }
  1032. if(track.containers) {
  1033. errorContent.containers = [];
  1034. logger.log("Containers:");
  1035. track.containers.forEach(function (container) {
  1036. logger.log("Container:");
  1037. errorContent.containers.push({
  1038. autoplay: container.autoplay,
  1039. muted: container.muted,
  1040. src: container.src,
  1041. volume: container.volume,
  1042. id: container.id,
  1043. ended: container.ended,
  1044. paused: container.paused,
  1045. readyState: container.readyState
  1046. });
  1047. logger.log("autoplay: ", container.autoplay);
  1048. logger.log("muted: ", container.muted);
  1049. logger.log("src: ", container.src);
  1050. logger.log("volume: ", container.volume);
  1051. logger.log("id: ", container.id);
  1052. logger.log("ended: ", container.ended);
  1053. logger.log("paused: ", container.paused);
  1054. logger.log("readyState: ", container.readyState);
  1055. });
  1056. }
  1057. // Prints JSON.stringify(errorContent) to be able to see all properties of
  1058. // errorContent from torture
  1059. logger.error("Audio problem detected. The audio is received but not played",
  1060. errorContent);
  1061. delete errorContent.displayName;
  1062. this.statistics.sendDetectedAudioProblem(
  1063. new Error(JSON.stringify(errorContent)));
  1064. };
  1065. /**
  1066. * Logs an "application log" message.
  1067. * @param message {string} The message to log. Note that while this can be a
  1068. * generic string, the convention used by lib-jitsi-meet and jitsi-meet is to
  1069. * log valid JSON strings, with an "id" field used for distinguishing between
  1070. * message types. E.g.: {id: "recorder_status", status: "off"}
  1071. */
  1072. JitsiConference.prototype.sendApplicationLog = function(message) {
  1073. Statistics.sendLog(message);
  1074. };
  1075. /**
  1076. * Checks if the user identified by given <tt>mucJid</tt> is the conference
  1077. * focus.
  1078. * @param mucJid the full MUC address of the user to be checked.
  1079. * @returns {boolean} <tt>true</tt> if MUC user is the conference focus.
  1080. */
  1081. JitsiConference.prototype._isFocus = function (mucJid) {
  1082. return this.room.isFocus(mucJid);
  1083. };
  1084. /**
  1085. * Fires CONFERENCE_FAILED event with INCOMPATIBLE_SERVER_VERSIONS parameter
  1086. */
  1087. JitsiConference.prototype._fireIncompatibleVersionsEvent = function () {
  1088. this.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED,
  1089. JitsiConferenceErrors.INCOMPATIBLE_SERVER_VERSIONS);
  1090. };
  1091. /**
  1092. * Sends message via the datachannels.
  1093. * @param to {string} the id of the endpoint that should receive the message.
  1094. * If "" the message will be sent to all participants.
  1095. * @param payload {object} the payload of the message.
  1096. * @throws NetworkError or InvalidStateError or Error if the operation fails.
  1097. */
  1098. JitsiConference.prototype.sendEndpointMessage = function (to, payload) {
  1099. this.rtc.sendDataChannelMessage(to, payload);
  1100. }
  1101. /**
  1102. * Sends broadcast message via the datachannels.
  1103. * @param payload {object} the payload of the message.
  1104. * @throws NetworkError or InvalidStateError or Error if the operation fails.
  1105. */
  1106. JitsiConference.prototype.broadcastEndpointMessage = function (payload) {
  1107. this.sendEndpointMessage("", payload);
  1108. }
  1109. module.exports = JitsiConference;