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

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