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.

JitsiConferenceEventManager.js 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. /* global __filename, Strophe */
  2. import AuthenticationEvents
  3. from './service/authentication/AuthenticationEvents';
  4. import EventEmitterForwarder from './modules/util/EventEmitterForwarder';
  5. import { getLogger } from 'jitsi-meet-logger';
  6. import * as JitsiConferenceErrors from './JitsiConferenceErrors';
  7. import * as JitsiConferenceEvents from './JitsiConferenceEvents';
  8. import * as MediaType from './service/RTC/MediaType';
  9. import RTCEvents from './service/RTC/RTCEvents';
  10. import Statistics from './modules/statistics/statistics';
  11. import XMPPEvents from './service/xmpp/XMPPEvents';
  12. const logger = getLogger(__filename);
  13. /**
  14. * Setups all event listeners related to conference
  15. * @param conference {JitsiConference} the conference
  16. */
  17. function JitsiConferenceEventManager(conference) {
  18. this.conference = conference;
  19. // Listeners related to the conference only
  20. conference.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED,
  21. function(track) {
  22. if(!track.isLocal() || !conference.statistics) {
  23. return;
  24. }
  25. conference.statistics.sendMuteEvent(track.isMuted(),
  26. track.getType());
  27. });
  28. }
  29. /**
  30. * Setups event listeners related to conference.chatRoom
  31. */
  32. JitsiConferenceEventManager.prototype.setupChatRoomListeners = function() {
  33. const conference = this.conference;
  34. const chatRoom = conference.room;
  35. this.chatRoomForwarder = new EventEmitterForwarder(chatRoom,
  36. this.conference.eventEmitter);
  37. chatRoom.addListener(XMPPEvents.ICE_RESTARTING, function() {
  38. // All data channels have to be closed, before ICE restart
  39. // otherwise Chrome will not trigger "opened" event for the channel
  40. // established with the new bridge
  41. conference.rtc.closeAllDataChannels();
  42. });
  43. chatRoom.addListener(XMPPEvents.AUDIO_MUTED_BY_FOCUS,
  44. function(value) {
  45. // set isMutedByFocus when setAudioMute Promise ends
  46. conference.rtc.setAudioMute(value).then(
  47. function() {
  48. conference.isMutedByFocus = true;
  49. },
  50. function() {
  51. logger.warn(
  52. 'Error while audio muting due to focus request');
  53. });
  54. }
  55. );
  56. this.chatRoomForwarder.forward(XMPPEvents.SUBJECT_CHANGED,
  57. JitsiConferenceEvents.SUBJECT_CHANGED);
  58. this.chatRoomForwarder.forward(XMPPEvents.MUC_JOINED,
  59. JitsiConferenceEvents.CONFERENCE_JOINED);
  60. // send some analytics events
  61. chatRoom.addListener(XMPPEvents.MUC_JOINED,
  62. () => {
  63. this.conference.connectionIsInterrupted = false;
  64. Object.keys(chatRoom.connectionTimes).forEach(key => {
  65. const value = chatRoom.connectionTimes[key];
  66. Statistics.analytics.sendEvent(`conference.${key}`, {value});
  67. });
  68. Object.keys(chatRoom.xmpp.connectionTimes).forEach(key => {
  69. const value = chatRoom.xmpp.connectionTimes[key];
  70. Statistics.analytics.sendEvent(`xmpp.${key}`, {value});
  71. });
  72. });
  73. this.chatRoomForwarder.forward(XMPPEvents.ROOM_JOIN_ERROR,
  74. JitsiConferenceEvents.CONFERENCE_FAILED,
  75. JitsiConferenceErrors.CONNECTION_ERROR);
  76. this.chatRoomForwarder.forward(XMPPEvents.ROOM_CONNECT_ERROR,
  77. JitsiConferenceEvents.CONFERENCE_FAILED,
  78. JitsiConferenceErrors.CONNECTION_ERROR);
  79. this.chatRoomForwarder.forward(XMPPEvents.ROOM_CONNECT_NOT_ALLOWED_ERROR,
  80. JitsiConferenceEvents.CONFERENCE_FAILED,
  81. JitsiConferenceErrors.NOT_ALLOWED_ERROR);
  82. this.chatRoomForwarder.forward(XMPPEvents.ROOM_MAX_USERS_ERROR,
  83. JitsiConferenceEvents.CONFERENCE_FAILED,
  84. JitsiConferenceErrors.CONFERENCE_MAX_USERS);
  85. this.chatRoomForwarder.forward(XMPPEvents.PASSWORD_REQUIRED,
  86. JitsiConferenceEvents.CONFERENCE_FAILED,
  87. JitsiConferenceErrors.PASSWORD_REQUIRED);
  88. this.chatRoomForwarder.forward(XMPPEvents.AUTHENTICATION_REQUIRED,
  89. JitsiConferenceEvents.CONFERENCE_FAILED,
  90. JitsiConferenceErrors.AUTHENTICATION_REQUIRED);
  91. this.chatRoomForwarder.forward(XMPPEvents.BRIDGE_DOWN,
  92. JitsiConferenceEvents.CONFERENCE_FAILED,
  93. JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE);
  94. chatRoom.addListener(
  95. XMPPEvents.BRIDGE_DOWN,
  96. () => Statistics.analytics.sendEvent('conference.bridgeDown'));
  97. this.chatRoomForwarder.forward(XMPPEvents.RESERVATION_ERROR,
  98. JitsiConferenceEvents.CONFERENCE_FAILED,
  99. JitsiConferenceErrors.RESERVATION_ERROR);
  100. this.chatRoomForwarder.forward(XMPPEvents.GRACEFUL_SHUTDOWN,
  101. JitsiConferenceEvents.CONFERENCE_FAILED,
  102. JitsiConferenceErrors.GRACEFUL_SHUTDOWN);
  103. chatRoom.addListener(XMPPEvents.JINGLE_FATAL_ERROR,
  104. function(session, error) {
  105. conference.eventEmitter.emit(
  106. JitsiConferenceEvents.CONFERENCE_FAILED,
  107. JitsiConferenceErrors.JINGLE_FATAL_ERROR, error);
  108. });
  109. chatRoom.addListener(XMPPEvents.CONNECTION_ICE_FAILED,
  110. function() {
  111. chatRoom.eventEmitter.emit(
  112. XMPPEvents.CONFERENCE_SETUP_FAILED,
  113. new Error('ICE fail'));
  114. });
  115. this.chatRoomForwarder.forward(XMPPEvents.MUC_DESTROYED,
  116. JitsiConferenceEvents.CONFERENCE_FAILED,
  117. JitsiConferenceErrors.CONFERENCE_DESTROYED);
  118. this.chatRoomForwarder.forward(XMPPEvents.CHAT_ERROR_RECEIVED,
  119. JitsiConferenceEvents.CONFERENCE_ERROR,
  120. JitsiConferenceErrors.CHAT_ERROR);
  121. this.chatRoomForwarder.forward(XMPPEvents.FOCUS_DISCONNECTED,
  122. JitsiConferenceEvents.CONFERENCE_FAILED,
  123. JitsiConferenceErrors.FOCUS_DISCONNECTED);
  124. chatRoom.addListener(XMPPEvents.FOCUS_LEFT,
  125. function() {
  126. Statistics.analytics.sendEvent('conference.focusLeft');
  127. conference.eventEmitter.emit(
  128. JitsiConferenceEvents.CONFERENCE_FAILED,
  129. JitsiConferenceErrors.FOCUS_LEFT);
  130. });
  131. const eventLogHandler
  132. = reason => Statistics.sendEventToAll(`conference.error.${reason}`);
  133. chatRoom.addListener(XMPPEvents.SESSION_ACCEPT_TIMEOUT,
  134. eventLogHandler.bind(null, 'sessionAcceptTimeout'));
  135. this.chatRoomForwarder.forward(XMPPEvents.CONNECTION_INTERRUPTED,
  136. JitsiConferenceEvents.CONNECTION_INTERRUPTED);
  137. chatRoom.addListener(XMPPEvents.CONNECTION_INTERRUPTED,
  138. () => {
  139. Statistics.sendEventToAll('connection.interrupted');
  140. this.conference.connectionIsInterrupted = true;
  141. });
  142. this.chatRoomForwarder.forward(XMPPEvents.RECORDER_STATE_CHANGED,
  143. JitsiConferenceEvents.RECORDER_STATE_CHANGED);
  144. this.chatRoomForwarder.forward(XMPPEvents.PHONE_NUMBER_CHANGED,
  145. JitsiConferenceEvents.PHONE_NUMBER_CHANGED);
  146. this.chatRoomForwarder.forward(XMPPEvents.CONNECTION_RESTORED,
  147. JitsiConferenceEvents.CONNECTION_RESTORED);
  148. chatRoom.addListener(XMPPEvents.CONNECTION_RESTORED,
  149. () => {
  150. Statistics.sendEventToAll('connection.restored');
  151. this.conference.connectionIsInterrupted = false;
  152. });
  153. this.chatRoomForwarder.forward(XMPPEvents.CONFERENCE_SETUP_FAILED,
  154. JitsiConferenceEvents.CONFERENCE_FAILED,
  155. JitsiConferenceErrors.SETUP_FAILED);
  156. chatRoom.setParticipantPropertyListener(function(node, from) {
  157. const participant = conference.getParticipantById(from);
  158. if (!participant) {
  159. return;
  160. }
  161. participant.setProperty(
  162. node.tagName.substring('jitsi_participant_'.length),
  163. node.value);
  164. });
  165. this.chatRoomForwarder.forward(XMPPEvents.KICKED,
  166. JitsiConferenceEvents.KICKED);
  167. chatRoom.addListener(XMPPEvents.KICKED,
  168. function() {
  169. conference.room = null;
  170. conference.leave();
  171. });
  172. chatRoom.addListener(XMPPEvents.SUSPEND_DETECTED,
  173. conference.onSuspendDetected.bind(conference));
  174. this.chatRoomForwarder.forward(XMPPEvents.MUC_LOCK_CHANGED,
  175. JitsiConferenceEvents.LOCK_STATE_CHANGED);
  176. chatRoom.addListener(XMPPEvents.MUC_MEMBER_JOINED,
  177. conference.onMemberJoined.bind(conference));
  178. chatRoom.addListener(XMPPEvents.MUC_MEMBER_LEFT,
  179. conference.onMemberLeft.bind(conference));
  180. this.chatRoomForwarder.forward(XMPPEvents.MUC_LEFT,
  181. JitsiConferenceEvents.CONFERENCE_LEFT);
  182. chatRoom.addListener(XMPPEvents.DISPLAY_NAME_CHANGED,
  183. conference.onDisplayNameChanged.bind(conference));
  184. chatRoom.addListener(XMPPEvents.LOCAL_ROLE_CHANGED, function(role) {
  185. conference.eventEmitter.emit(JitsiConferenceEvents.USER_ROLE_CHANGED,
  186. conference.myUserId(), role);
  187. // log all events for the recorder operated by the moderator
  188. if (conference.statistics && conference.isModerator()) {
  189. conference.on(JitsiConferenceEvents.RECORDER_STATE_CHANGED,
  190. function(status, error) {
  191. const logObject = {
  192. id: 'recorder_status',
  193. status
  194. };
  195. if (error) {
  196. logObject.error = error;
  197. }
  198. Statistics.sendLog(JSON.stringify(logObject));
  199. });
  200. }
  201. });
  202. chatRoom.addListener(XMPPEvents.MUC_ROLE_CHANGED,
  203. conference.onUserRoleChanged.bind(conference));
  204. chatRoom.addListener(AuthenticationEvents.IDENTITY_UPDATED,
  205. function(authEnabled, authIdentity) {
  206. conference.authEnabled = authEnabled;
  207. conference.authIdentity = authIdentity;
  208. conference.eventEmitter.emit(
  209. JitsiConferenceEvents.AUTH_STATUS_CHANGED, authEnabled,
  210. authIdentity);
  211. });
  212. chatRoom.addListener(XMPPEvents.MESSAGE_RECEIVED,
  213. function(jid, displayName, txt, myJid, ts) {
  214. const id = Strophe.getResourceFromJid(jid);
  215. conference.eventEmitter.emit(JitsiConferenceEvents.MESSAGE_RECEIVED,
  216. id, txt, ts);
  217. });
  218. chatRoom.addListener(XMPPEvents.PRESENCE_STATUS,
  219. function(jid, status) {
  220. const id = Strophe.getResourceFromJid(jid);
  221. const participant = conference.getParticipantById(id);
  222. if (!participant || participant._status === status) {
  223. return;
  224. }
  225. participant._status = status;
  226. conference.eventEmitter.emit(
  227. JitsiConferenceEvents.USER_STATUS_CHANGED, id, status);
  228. });
  229. conference.room.addListener(XMPPEvents.LOCAL_UFRAG_CHANGED,
  230. function(ufrag) {
  231. Statistics.sendLog(
  232. JSON.stringify({id: 'local_ufrag', value: ufrag}));
  233. });
  234. conference.room.addListener(XMPPEvents.REMOTE_UFRAG_CHANGED,
  235. function(ufrag) {
  236. Statistics.sendLog(
  237. JSON.stringify({id: 'remote_ufrag', value: ufrag}));
  238. });
  239. chatRoom.addPresenceListener('startmuted', function(data, from) {
  240. let isModerator = false;
  241. if (conference.myUserId() === from && conference.isModerator()) {
  242. isModerator = true;
  243. } else {
  244. const participant = conference.getParticipantById(from);
  245. if (participant && participant.isModerator()) {
  246. isModerator = true;
  247. }
  248. }
  249. if (!isModerator) {
  250. return;
  251. }
  252. const startAudioMuted = data.attributes.audio === 'true';
  253. const startVideoMuted = data.attributes.video === 'true';
  254. let updated = false;
  255. if (startAudioMuted !== conference.startMutedPolicy.audio) {
  256. conference.startMutedPolicy.audio = startAudioMuted;
  257. updated = true;
  258. }
  259. if (startVideoMuted !== conference.startMutedPolicy.video) {
  260. conference.startMutedPolicy.video = startVideoMuted;
  261. updated = true;
  262. }
  263. if (updated) {
  264. conference.eventEmitter.emit(
  265. JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
  266. conference.startMutedPolicy
  267. );
  268. }
  269. });
  270. chatRoom.addPresenceListener('videomuted', function(values, from) {
  271. conference.rtc.handleRemoteTrackMute(MediaType.VIDEO,
  272. values.value == 'true', from);
  273. });
  274. chatRoom.addPresenceListener('audiomuted', function(values, from) {
  275. conference.rtc.handleRemoteTrackMute(MediaType.AUDIO,
  276. values.value == 'true', from);
  277. });
  278. chatRoom.addPresenceListener('videoType', function(data, from) {
  279. conference.rtc.handleRemoteTrackVideoTypeChanged(data.value, from);
  280. });
  281. chatRoom.addPresenceListener('devices', function(data, from) {
  282. let isAudioAvailable = false;
  283. let isVideoAvailable = false;
  284. data.children.forEach(function(config) {
  285. if (config.tagName === 'audio') {
  286. isAudioAvailable = config.value === 'true';
  287. }
  288. if (config.tagName === 'video') {
  289. isVideoAvailable = config.value === 'true';
  290. }
  291. });
  292. let availableDevices;
  293. if (conference.myUserId() === from) {
  294. availableDevices = conference.availableDevices;
  295. } else {
  296. const participant = conference.getParticipantById(from);
  297. if (!participant) {
  298. return;
  299. }
  300. availableDevices = participant._availableDevices;
  301. }
  302. let updated = false;
  303. if (availableDevices.audio !== isAudioAvailable) {
  304. updated = true;
  305. availableDevices.audio = isAudioAvailable;
  306. }
  307. if (availableDevices.video !== isVideoAvailable) {
  308. updated = true;
  309. availableDevices.video = isVideoAvailable;
  310. }
  311. if (updated) {
  312. conference.eventEmitter.emit(
  313. JitsiConferenceEvents.AVAILABLE_DEVICES_CHANGED,
  314. from, availableDevices);
  315. }
  316. });
  317. if(conference.statistics) {
  318. // FIXME ICE related events should end up in RTCEvents eventually
  319. chatRoom.addListener(XMPPEvents.CONNECTION_ICE_FAILED,
  320. function(pc) {
  321. conference.statistics.sendIceConnectionFailedEvent(pc);
  322. });
  323. chatRoom.addListener(XMPPEvents.ADD_ICE_CANDIDATE_FAILED,
  324. function(e, pc) {
  325. conference.statistics.sendAddIceCandidateFailed(e, pc);
  326. });
  327. }
  328. };
  329. /**
  330. * Setups event listeners related to conference.rtc
  331. */
  332. JitsiConferenceEventManager.prototype.setupRTCListeners = function() {
  333. const conference = this.conference;
  334. const rtc = conference.rtc;
  335. this.rtcForwarder
  336. = new EventEmitterForwarder(rtc, this.conference.eventEmitter);
  337. rtc.addListener(
  338. RTCEvents.REMOTE_TRACK_ADDED,
  339. conference.onRemoteTrackAdded.bind(conference));
  340. rtc.addListener(
  341. RTCEvents.REMOTE_TRACK_REMOVED,
  342. conference.onRemoteTrackRemoved.bind(conference));
  343. rtc.addListener(RTCEvents.DOMINANT_SPEAKER_CHANGED,
  344. function(id) {
  345. if(conference.lastDominantSpeaker !== id && conference.room) {
  346. conference.lastDominantSpeaker = id;
  347. conference.eventEmitter.emit(
  348. JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, id);
  349. }
  350. if (conference.statistics && conference.myUserId() === id) {
  351. // We are the new dominant speaker.
  352. conference.statistics.sendDominantSpeakerEvent();
  353. }
  354. });
  355. rtc.addListener(RTCEvents.DATA_CHANNEL_OPEN, function() {
  356. const now = window.performance.now();
  357. logger.log('(TIME) data channel opened ', now);
  358. conference.room.connectionTimes['data.channel.opened'] = now;
  359. Statistics.analytics.sendEvent('conference.dataChannel.open',
  360. {value: now});
  361. });
  362. this.rtcForwarder.forward(RTCEvents.LASTN_CHANGED,
  363. JitsiConferenceEvents.IN_LAST_N_CHANGED);
  364. this.rtcForwarder.forward(RTCEvents.LASTN_ENDPOINT_CHANGED,
  365. JitsiConferenceEvents.LAST_N_ENDPOINTS_CHANGED);
  366. rtc.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED,
  367. function(devices) {
  368. conference.room.updateDeviceAvailability(devices);
  369. });
  370. rtc.addListener(RTCEvents.ENDPOINT_MESSAGE_RECEIVED,
  371. function(from, payload) {
  372. const participant = conference.getParticipantById(from);
  373. if (participant) {
  374. conference.eventEmitter.emit(
  375. JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
  376. participant, payload);
  377. } else {
  378. logger.warn(
  379. 'Ignored ENDPOINT_MESSAGE_RECEIVED for not existing '
  380. + `participant: ${from}`,
  381. payload);
  382. }
  383. });
  384. if (conference.statistics) {
  385. rtc.addListener(RTCEvents.CREATE_ANSWER_FAILED,
  386. (e, pc) => {
  387. conference.statistics.sendCreateAnswerFailed(e, pc);
  388. });
  389. rtc.addListener(RTCEvents.CREATE_OFFER_FAILED,
  390. (e, pc) => {
  391. conference.statistics.sendCreateOfferFailed(e, pc);
  392. });
  393. rtc.addListener(RTCEvents.SET_LOCAL_DESCRIPTION_FAILED,
  394. (e, pc) => {
  395. conference.statistics.sendSetLocalDescFailed(e, pc);
  396. });
  397. rtc.addListener(RTCEvents.SET_REMOTE_DESCRIPTION_FAILED,
  398. (e, pc) => {
  399. conference.statistics.sendSetRemoteDescFailed(e, pc);
  400. });
  401. }
  402. };
  403. /**
  404. * Setups event listeners related to conference.xmpp
  405. */
  406. JitsiConferenceEventManager.prototype.setupXMPPListeners = function() {
  407. const conference = this.conference;
  408. conference.xmpp.caps.addListener(XMPPEvents.PARTCIPANT_FEATURES_CHANGED,
  409. from => {
  410. const participant = conference.getParticipantId(
  411. Strophe.getResourceFromJid(from));
  412. if(participant) {
  413. conference.eventEmitter.emit(
  414. JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
  415. participant);
  416. }
  417. });
  418. conference.xmpp.addListener(
  419. XMPPEvents.CALL_INCOMING, conference.onIncomingCall.bind(conference));
  420. conference.xmpp.addListener(
  421. XMPPEvents.CALL_ENDED, conference.onCallEnded.bind(conference));
  422. conference.xmpp.addListener(XMPPEvents.START_MUTED_FROM_FOCUS,
  423. function(audioMuted, videoMuted) {
  424. conference.startAudioMuted = audioMuted;
  425. conference.startVideoMuted = videoMuted;
  426. // mute existing local tracks because this is initial mute from
  427. // Jicofo
  428. conference.getLocalTracks().forEach(function(track) {
  429. switch (track.getType()) {
  430. case MediaType.AUDIO:
  431. conference.startAudioMuted && track.mute();
  432. break;
  433. case MediaType.VIDEO:
  434. conference.startVideoMuted && track.mute();
  435. break;
  436. }
  437. });
  438. conference.eventEmitter.emit(JitsiConferenceEvents.STARTED_MUTED);
  439. });
  440. };
  441. /**
  442. * Setups event listeners related to conference.statistics
  443. */
  444. JitsiConferenceEventManager.prototype.setupStatisticsListeners = function() {
  445. const conference = this.conference;
  446. if(!conference.statistics) {
  447. return;
  448. }
  449. conference.statistics.addAudioLevelListener(function(ssrc, level) {
  450. const resource = conference.rtc.getResourceBySSRC(ssrc);
  451. if (!resource) {
  452. return;
  453. }
  454. conference.rtc.setAudioLevel(resource, level);
  455. });
  456. // Forward the "before stats disposed" event
  457. conference.statistics.addBeforeDisposedListener(function() {
  458. conference.eventEmitter.emit(
  459. JitsiConferenceEvents.BEFORE_STATISTICS_DISPOSED);
  460. });
  461. conference.statistics.addConnectionStatsListener(function(stats) {
  462. const ssrc2resolution = stats.resolution;
  463. const id2resolution = {};
  464. // preprocess resolutions: group by user id, skip incorrect
  465. // resolutions etc.
  466. Object.keys(ssrc2resolution).forEach(function(ssrc) {
  467. const resolution = ssrc2resolution[ssrc];
  468. if (!resolution.width || !resolution.height
  469. || resolution.width == -1 || resolution.height == -1) {
  470. return;
  471. }
  472. const id = conference.rtc.getResourceBySSRC(ssrc);
  473. if (!id) {
  474. return;
  475. }
  476. // ssrc to resolution map for user id
  477. const idResolutions = id2resolution[id] || {};
  478. idResolutions[ssrc] = resolution;
  479. id2resolution[id] = idResolutions;
  480. });
  481. stats.resolution = id2resolution;
  482. conference.eventEmitter.emit(
  483. JitsiConferenceEvents.CONNECTION_STATS, stats);
  484. });
  485. conference.statistics.addByteSentStatsListener(function(stats) {
  486. conference.getLocalTracks(MediaType.AUDIO).forEach(function(track) {
  487. const ssrc = track.getSSRC();
  488. if (!ssrc || !stats.hasOwnProperty(ssrc)) {
  489. return;
  490. }
  491. track._setByteSent(stats[ssrc]);
  492. });
  493. });
  494. };
  495. module.exports = JitsiConferenceEventManager;