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.

AvgRTPStatsReporter.js 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. /* global __filename */
  2. import { getLogger } from 'jitsi-meet-logger';
  3. import * as ConnectionQualityEvents
  4. from '../../service/connectivity/ConnectionQualityEvents';
  5. import * as ConferenceEvents from '../../JitsiConferenceEvents';
  6. import * as MediaType from '../../service/RTC/MediaType';
  7. import RTCBrowserType from '../RTC/RTCBrowserType';
  8. import Statistics from './statistics';
  9. import * as VideoType from '../../service/RTC/VideoType';
  10. const logger = getLogger(__filename);
  11. /**
  12. * This will calculate an average for one, named stat and submit it to
  13. * the analytics module when requested. It automatically counts the samples.
  14. */
  15. class AverageStatReport {
  16. /**
  17. * Creates new <tt>AverageStatReport</tt> for given name.
  18. * @param {string} name that's the name of the event that will be reported
  19. * to the analytics module.
  20. */
  21. constructor(name) {
  22. this.name = name;
  23. this.count = 0;
  24. this.sum = 0;
  25. }
  26. /**
  27. * Adds the next value that will be included in the average when
  28. * {@link calculate} is called.
  29. * @param {number} nextValue
  30. */
  31. addNext(nextValue) {
  32. if (typeof nextValue !== 'number') {
  33. logger.error(
  34. `${this.name} - invalid value for idx: ${this.count}`,
  35. nextValue);
  36. } else if (!isNaN(nextValue)) {
  37. this.sum += nextValue;
  38. this.count += 1;
  39. }
  40. }
  41. /**
  42. * Calculates an average for the samples collected using {@link addNext}.
  43. * @return {number|NaN} an average of all collected samples or <tt>NaN</tt>
  44. * if no samples were collected.
  45. */
  46. calculate() {
  47. return this.sum / this.count;
  48. }
  49. /**
  50. * Calculates an average and submit the report to the analytics module.
  51. * @param {boolean} isP2P indicates if the report is to be submitted for
  52. * the P2P connection (when conference is currently in the P2P mode). This
  53. * will add 'p2p.' prefix to the name of the event. All averages should be
  54. * cleared when the conference switches, between P2P and JVB modes.
  55. */
  56. report(isP2P) {
  57. Statistics.analytics.sendEvent(
  58. `${isP2P ? 'p2p.' : ''}${this.name}`,
  59. { value: this.calculate() });
  60. }
  61. /**
  62. * Clears all memory of any samples collected, so that new average can be
  63. * calculated using this instance.
  64. */
  65. reset() {
  66. this.sum = 0;
  67. this.count = 0;
  68. }
  69. }
  70. /**
  71. * Class gathers the stats that are calculated and reported for a
  72. * {@link TraceablePeerConnection} even if it's not currently active. For
  73. * example we want to monitor RTT for the JVB connection while in P2P mode.
  74. */
  75. class ConnectionAvgStats {
  76. /**
  77. * Creates new <tt>ConnectionAvgStats</tt>
  78. * @param {JitsiConference} conference
  79. * @param {boolean} isP2P
  80. * @param {number} n the number of samples, before arithmetic mean is to be
  81. * calculated and values submitted to the analytics module.
  82. */
  83. constructor(conference, isP2P, n) {
  84. /**
  85. * Is this instance for JVB or P2P connection ?
  86. * @type {boolean}
  87. */
  88. this.isP2P = isP2P;
  89. /**
  90. * How many samples are to be included in arithmetic mean calculation.
  91. * @type {number}
  92. * @private
  93. */
  94. this._n = n;
  95. /**
  96. * The current sample index. Starts from 0 and goes up to {@link _n})
  97. * when analytics report will be submitted.
  98. * @type {number}
  99. * @private
  100. */
  101. this._sampleIdx = 0;
  102. /**
  103. * Average round trip time reported by the ICE candidate pair.
  104. * @type {AverageStatReport}
  105. */
  106. this._avgRTT = new AverageStatReport('stat.avg.rtt');
  107. /**
  108. * Map stores average RTT to the JVB reported by remote participants.
  109. * Mapped per participant id {@link JitsiParticipant.getId}.
  110. *
  111. * This is used only when {@link ConnectionAvgStats.isP2P} equals to
  112. * <tt>false</tt>.
  113. *
  114. * @type {Map<string,AverageStatReport>}
  115. * @private
  116. */
  117. this._avgRemoteRTTMap = new Map();
  118. /**
  119. * The conference for which stats will be collected and reported.
  120. * @type {JitsiConference}
  121. * @private
  122. */
  123. this._conference = conference;
  124. this._onConnectionStats = (tpc, stats) => {
  125. if (this.isP2P === tpc.isP2P) {
  126. this._calculateAvgStats(stats);
  127. }
  128. };
  129. conference.statistics.addConnectionStatsListener(
  130. this._onConnectionStats);
  131. if (!this.isP2P) {
  132. this._onUserLeft = id => this._avgRemoteRTTMap.delete(id);
  133. conference.on(ConferenceEvents.USER_LEFT, this._onUserLeft);
  134. this._onRemoteStatsUpdated
  135. = (id, data) => this._processRemoteStats(id, data);
  136. conference.on(
  137. ConnectionQualityEvents.REMOTE_STATS_UPDATED,
  138. this._onRemoteStatsUpdated);
  139. }
  140. }
  141. /**
  142. * Processes next batch of stats.
  143. * @param {go figure} data
  144. * @private
  145. */
  146. _calculateAvgStats(data) {
  147. if (!data) {
  148. logger.error('No stats');
  149. return;
  150. }
  151. if (RTCBrowserType.supportsRTTStatistics()) {
  152. if (data.transport && data.transport.length) {
  153. this._avgRTT.addNext(data.transport[0].rtt);
  154. }
  155. }
  156. this._sampleIdx += 1;
  157. if (this._sampleIdx >= this._n) {
  158. if (RTCBrowserType.supportsRTTStatistics()) {
  159. this._avgRTT.report(this.isP2P);
  160. // Report end to end RTT only for JVB
  161. if (!this.isP2P) {
  162. const avgRemoteRTT = this._calculateAvgRemoteRTT();
  163. const avgLocalRTT = this._avgRTT.calculate();
  164. if (!isNaN(avgLocalRTT) && !isNaN(avgRemoteRTT)) {
  165. Statistics.analytics.sendEvent(
  166. 'stat.avg.end2endrtt',
  167. { value: avgLocalRTT + avgRemoteRTT });
  168. }
  169. }
  170. }
  171. this._resetAvgStats();
  172. }
  173. }
  174. /**
  175. * Calculates arithmetic mean of all RTTs towards the JVB reported by
  176. * participants.
  177. * @return {number|NaN} NaN if not available (not enough data)
  178. * @private
  179. */
  180. _calculateAvgRemoteRTT() {
  181. let count = 0, sum = 0;
  182. // FIXME should we ignore RTT for participant
  183. // who "is having connectivity issues" ?
  184. for (const remoteAvg of this._avgRemoteRTTMap.values()) {
  185. const avg = remoteAvg.calculate();
  186. if (!isNaN(avg)) {
  187. sum += avg;
  188. count += 1;
  189. remoteAvg.reset();
  190. }
  191. }
  192. return sum / count;
  193. }
  194. /**
  195. * Processes {@link ConnectionQualityEvents.REMOTE_STATS_UPDATED} to analyse
  196. * RTT towards the JVB reported by each participant.
  197. * @param {string} id {@link JitsiParticipant.getId}
  198. * @param {go figure in ConnectionQuality.js} data
  199. * @private
  200. */
  201. _processRemoteStats(id, data) {
  202. const validData = typeof data.jvbRTT === 'number';
  203. let rttAvg = this._avgRemoteRTTMap.get(id);
  204. if (!rttAvg && validData) {
  205. rttAvg = new AverageStatReport(`${id}.stat.rtt`);
  206. this._avgRemoteRTTMap.set(id, rttAvg);
  207. }
  208. if (validData) {
  209. rttAvg.addNext(data.jvbRTT);
  210. } else if (rttAvg) {
  211. this._avgRemoteRTTMap.delete(id);
  212. }
  213. }
  214. /**
  215. * Reset cache of all averages and {@link _sampleIdx}.
  216. * @private
  217. */
  218. _resetAvgStats() {
  219. this._avgRTT.reset();
  220. if (this._avgRemoteRTTMap) {
  221. this._avgRemoteRTTMap.clear();
  222. }
  223. this._sampleIdx = 0;
  224. }
  225. /**
  226. *
  227. */
  228. dispose() {
  229. this._conference.statistics.removeConnectionStatsListener(
  230. this._onConnectionStats);
  231. if (!this.isP2P) {
  232. this._conference.off(
  233. ConnectionQualityEvents.REMOTE_STATS_UPDATED,
  234. this._onRemoteStatsUpdated);
  235. this._conference.off(
  236. ConferenceEvents.USER_LEFT,
  237. this._onUserLeft);
  238. }
  239. }
  240. }
  241. /**
  242. * Reports average RTP statistics values (arithmetic mean) to the analytics
  243. * module for things like bit rate, bandwidth, packet loss etc. It keeps track
  244. * of the P2P vs JVB conference modes and submits the values under different
  245. * namespaces (the events for P2P mode have 'p2p.' prefix). Every switch between
  246. * P2P mode resets the data collected so far and averages are calculated from
  247. * scratch.
  248. */
  249. export default class AvgRTPStatsReporter {
  250. /**
  251. * Creates new instance of <tt>AvgRTPStatsReporter</tt>
  252. * @param {JitsiConference} conference
  253. * @param {number} n the number of samples, before arithmetic mean is to be
  254. * calculated and values submitted to the analytics module.
  255. */
  256. constructor(conference, n) {
  257. /**
  258. * How many {@link ConnectionQualityEvents.LOCAL_STATS_UPDATED} samples
  259. * are to be included in arithmetic mean calculation.
  260. * @type {number}
  261. * @private
  262. */
  263. this._n = n;
  264. if (n > 0) {
  265. logger.info(`Avg RTP stats will be calculated every ${n} samples`);
  266. } else {
  267. logger.info('Avg RTP stats reports are disabled.');
  268. // Do not initialize
  269. return;
  270. }
  271. /**
  272. * The current sample index. Starts from 0 and goes up to {@link _n})
  273. * when analytics report will be submitted.
  274. * @type {number}
  275. * @private
  276. */
  277. this._sampleIdx = 0;
  278. /**
  279. * The conference for which stats will be collected and reported.
  280. * @type {JitsiConference}
  281. * @private
  282. */
  283. this._conference = conference;
  284. /**
  285. * Average audio upload bitrate
  286. * @type {AverageStatReport}
  287. * @private
  288. */
  289. this._avgAudioBitrateUp
  290. = new AverageStatReport('stat.avg.bitrate.audio.upload');
  291. /**
  292. * Average audio download bitrate
  293. * @type {AverageStatReport}
  294. * @private
  295. */
  296. this._avgAudioBitrateDown
  297. = new AverageStatReport('stat.avg.bitrate.audio.download');
  298. /**
  299. * Average video upload bitrate
  300. * @type {AverageStatReport}
  301. * @private
  302. */
  303. this._avgVideoBitrateUp
  304. = new AverageStatReport('stat.avg.bitrate.video.upload');
  305. /**
  306. * Average video download bitrate
  307. * @type {AverageStatReport}
  308. * @private
  309. */
  310. this._avgVideoBitrateDown
  311. = new AverageStatReport('stat.avg.bitrate.video.download');
  312. /**
  313. * Average upload bandwidth
  314. * @type {AverageStatReport}
  315. * @private
  316. */
  317. this._avgBandwidthUp
  318. = new AverageStatReport('stat.avg.bandwidth.upload');
  319. /**
  320. * Average download bandwidth
  321. * @type {AverageStatReport}
  322. * @private
  323. */
  324. this._avgBandwidthDown
  325. = new AverageStatReport('stat.avg.bandwidth.download');
  326. /**
  327. * Average total packet loss
  328. * @type {AverageStatReport}
  329. * @private
  330. */
  331. this._avgPacketLossTotal
  332. = new AverageStatReport('stat.avg.packetloss.total');
  333. /**
  334. * Average upload packet loss
  335. * @type {AverageStatReport}
  336. * @private
  337. */
  338. this._avgPacketLossUp
  339. = new AverageStatReport('stat.avg.packetloss.upload');
  340. /**
  341. * Average download packet loss
  342. * @type {AverageStatReport}
  343. * @private
  344. */
  345. this._avgPacketLossDown
  346. = new AverageStatReport('stat.avg.packetloss.download');
  347. /**
  348. * Average FPS for remote videos
  349. * @type {AverageStatReport}
  350. * @private
  351. */
  352. this._avgRemoteFPS = new AverageStatReport('stat.avg.framerate.remote');
  353. /**
  354. * Average FPS for remote screen streaming videos (reported only if not
  355. * a <tt>NaN</tt>).
  356. * @type {AverageStatReport}
  357. * @private
  358. */
  359. this._avgRemoteScreenFPS
  360. = new AverageStatReport('stat.avg.framerate.screen.remote');
  361. /**
  362. * Average FPS for local video (camera)
  363. * @type {AverageStatReport}
  364. * @private
  365. */
  366. this._avgLocalFPS = new AverageStatReport('stat.avg.framerate.local');
  367. /**
  368. * Average FPS for local screen streaming video (reported only if not
  369. * a <tt>NaN</tt>).
  370. * @type {AverageStatReport}
  371. * @private
  372. */
  373. this._avgLocalScreenFPS
  374. = new AverageStatReport('stat.avg.framerate.screen.local');
  375. /**
  376. * Average connection quality as defined by
  377. * the {@link ConnectionQuality} module.
  378. * @type {AverageStatReport}
  379. * @private
  380. */
  381. this._avgCQ = new AverageStatReport('stat.avg.cq');
  382. this._onLocalStatsUpdated = data => this._calculateAvgStats(data);
  383. conference.on(
  384. ConnectionQualityEvents.LOCAL_STATS_UPDATED,
  385. this._onLocalStatsUpdated);
  386. this._onP2PStatusChanged = () => {
  387. logger.debug('Resetting average stats calculation');
  388. this._resetAvgStats();
  389. this.jvbStatsMonitor._resetAvgStats();
  390. this.p2pStatsMonitor._resetAvgStats();
  391. };
  392. conference.on(
  393. ConferenceEvents.P2P_STATUS,
  394. this._onP2PStatusChanged);
  395. this.jvbStatsMonitor
  396. = new ConnectionAvgStats(conference, false /* JVB */, n);
  397. this.p2pStatsMonitor
  398. = new ConnectionAvgStats(conference, true /* P2P */, n);
  399. }
  400. /**
  401. * Processes next batch of stats reported on
  402. * {@link ConnectionQualityEvents.LOCAL_STATS_UPDATED}.
  403. * @param {go figure} data
  404. * @private
  405. */
  406. _calculateAvgStats(data) {
  407. const isP2P = this._conference.isP2PActive();
  408. const peerCount = this._conference.getParticipants().length;
  409. if (!isP2P && peerCount < 1) {
  410. // There's no point in collecting stats for a JVB conference of 1.
  411. // That happens for short period of time after everyone leaves
  412. // the room, until Jicofo terminates the session.
  413. return;
  414. }
  415. /* Uncomment to figure out stats structure
  416. for (const key in data) {
  417. if (data.hasOwnProperty(key)) {
  418. logger.info(`local stat ${key}: `, data[key]);
  419. }
  420. } */
  421. if (!data) {
  422. logger.error('No stats');
  423. return;
  424. }
  425. const bitrate = data.bitrate;
  426. const bandwidth = data.bandwidth;
  427. const packetLoss = data.packetLoss;
  428. const frameRate = data.framerate;
  429. if (!bitrate) {
  430. logger.error('No "bitrate"');
  431. return;
  432. } else if (!bandwidth) {
  433. logger.error('No "bandwidth"');
  434. return;
  435. } else if (!packetLoss) {
  436. logger.error('No "packetloss"');
  437. return;
  438. } else if (!frameRate) {
  439. logger.error('No "framerate"');
  440. return;
  441. }
  442. this._avgAudioBitrateUp.addNext(bitrate.audio.upload);
  443. this._avgAudioBitrateDown.addNext(bitrate.audio.download);
  444. this._avgVideoBitrateUp.addNext(bitrate.video.upload);
  445. this._avgVideoBitrateDown.addNext(bitrate.video.download);
  446. if (RTCBrowserType.supportsBandwidthStatistics()) {
  447. this._avgBandwidthUp.addNext(bandwidth.upload);
  448. this._avgBandwidthDown.addNext(bandwidth.download);
  449. }
  450. this._avgPacketLossUp.addNext(packetLoss.upload);
  451. this._avgPacketLossDown.addNext(packetLoss.download);
  452. this._avgPacketLossTotal.addNext(packetLoss.total);
  453. this._avgCQ.addNext(data.connectionQuality);
  454. if (frameRate) {
  455. this._avgRemoteFPS.addNext(
  456. this._calculateAvgVideoFps(
  457. frameRate, false /* remote */, VideoType.CAMERA));
  458. this._avgRemoteScreenFPS.addNext(
  459. this._calculateAvgVideoFps(
  460. frameRate, false /* remote */, VideoType.DESKTOP));
  461. this._avgLocalFPS.addNext(
  462. this._calculateAvgVideoFps(
  463. frameRate, true /* local */, VideoType.CAMERA));
  464. this._avgLocalScreenFPS.addNext(
  465. this._calculateAvgVideoFps(
  466. frameRate, true /* local */, VideoType.DESKTOP));
  467. }
  468. this._sampleIdx += 1;
  469. if (this._sampleIdx >= this._n) {
  470. this._avgAudioBitrateUp.report(isP2P);
  471. this._avgAudioBitrateDown.report(isP2P);
  472. this._avgVideoBitrateUp.report(isP2P);
  473. this._avgVideoBitrateDown.report(isP2P);
  474. if (RTCBrowserType.supportsBandwidthStatistics()) {
  475. this._avgBandwidthUp.report(isP2P);
  476. this._avgBandwidthDown.report(isP2P);
  477. }
  478. this._avgPacketLossUp.report(isP2P);
  479. this._avgPacketLossDown.report(isP2P);
  480. this._avgPacketLossTotal.report(isP2P);
  481. this._avgRemoteFPS.report(isP2P);
  482. if (!isNaN(this._avgRemoteScreenFPS.calculate())) {
  483. this._avgRemoteScreenFPS.report(isP2P);
  484. }
  485. this._avgLocalFPS.report(isP2P);
  486. if (!isNaN(this._avgLocalScreenFPS.calculate())) {
  487. this._avgLocalScreenFPS.report(isP2P);
  488. }
  489. this._avgCQ.report(isP2P);
  490. this._resetAvgStats();
  491. }
  492. }
  493. /**
  494. * Calculates average FPS for the report
  495. * @param {go figure} frameRate
  496. * @param {boolean} isLocal if the average is to be calculated for the local
  497. * video or <tt>false</tt> if for remote videos.
  498. * @param {VideoType} videoType
  499. * @return {number|NaN} average FPS or <tt>NaN</tt> if there are no samples.
  500. * @private
  501. */
  502. _calculateAvgVideoFps(frameRate, isLocal, videoType) {
  503. let peerFpsSum = 0;
  504. let peerCount = 0;
  505. const myID = this._conference.myUserId();
  506. for (const peerID of Object.keys(frameRate)) {
  507. if (isLocal ? peerID === myID : peerID !== myID) {
  508. const participant
  509. = isLocal
  510. ? null : this._conference.getParticipantById(peerID);
  511. const videosFps = frameRate[peerID];
  512. // Do not continue without participant for non local peerID
  513. if ((isLocal || participant) && videosFps) {
  514. const peerAvgFPS
  515. = this._calculatePeerAvgVideoFps(
  516. videosFps, participant, videoType);
  517. if (!isNaN(peerAvgFPS)) {
  518. peerFpsSum += peerAvgFPS;
  519. peerCount += 1;
  520. }
  521. }
  522. }
  523. }
  524. return peerFpsSum / peerCount;
  525. }
  526. /**
  527. * Calculate average FPS for either remote or local participant
  528. * @param {object} videos maps FPS per video SSRC
  529. * @param {JitsiParticipant|null} participant remote participant or
  530. * <tt>null</tt> for local FPS calculation.
  531. * @param {VideoType} videoType the type of the video for which an average
  532. * will be calculated.
  533. * @return {number|NaN} average FPS of all participant's videos or
  534. * <tt>NaN</tt> if currently not available
  535. * @private
  536. */
  537. _calculatePeerAvgVideoFps(videos, participant, videoType) {
  538. let ssrcs = Object.keys(videos).map(ssrc => Number(ssrc));
  539. let videoTracks = null;
  540. // NOTE that this method is supposed to be called for the stats
  541. // received from the current peerconnection.
  542. const tpc = this._conference.getActivePeerConnection();
  543. if (participant) {
  544. videoTracks = participant.getTracksByMediaType(MediaType.VIDEO);
  545. if (videoTracks) {
  546. ssrcs
  547. = ssrcs.filter(
  548. ssrc => videoTracks.find(
  549. track => !track.isMuted()
  550. && track.getSSRC() === ssrc
  551. && track.videoType === videoType));
  552. }
  553. } else {
  554. videoTracks = this._conference.getLocalTracks(MediaType.VIDEO);
  555. ssrcs
  556. = ssrcs.filter(
  557. ssrc => videoTracks.find(
  558. track => !track.isMuted()
  559. && tpc.getLocalSSRC(track) === ssrc
  560. && track.videoType === videoType));
  561. }
  562. let peerFpsSum = 0;
  563. let peerSsrcCount = 0;
  564. for (const ssrc of ssrcs) {
  565. const peerSsrcFps = Number(videos[ssrc]);
  566. // FPS is reported as 0 for users with no video
  567. if (!isNaN(peerSsrcFps) && peerSsrcFps > 0) {
  568. peerFpsSum += peerSsrcFps;
  569. peerSsrcCount += 1;
  570. }
  571. }
  572. return peerFpsSum / peerSsrcCount;
  573. }
  574. /**
  575. * Reset cache of all averages and {@link _sampleIdx}.
  576. * @private
  577. */
  578. _resetAvgStats() {
  579. this._avgAudioBitrateUp.reset();
  580. this._avgAudioBitrateDown.reset();
  581. this._avgVideoBitrateUp.reset();
  582. this._avgVideoBitrateDown.reset();
  583. this._avgBandwidthUp.reset();
  584. this._avgBandwidthDown.reset();
  585. this._avgPacketLossUp.reset();
  586. this._avgPacketLossDown.reset();
  587. this._avgPacketLossTotal.reset();
  588. this._avgRemoteFPS.reset();
  589. this._avgRemoteScreenFPS.reset();
  590. this._avgLocalFPS.reset();
  591. this._avgLocalScreenFPS.reset();
  592. this._avgCQ.reset();
  593. this._sampleIdx = 0;
  594. }
  595. /**
  596. * Unregisters all event listeners and stops working.
  597. */
  598. dispose() {
  599. this._conference.off(
  600. ConferenceEvents.P2P_STATUS,
  601. this._onP2PStatusChanged);
  602. this._conference.off(
  603. ConnectionQualityEvents.LOCAL_STATS_UPDATED,
  604. this._onLocalStatsUpdated);
  605. this.jvbStatsMonitor.dispose();
  606. this.p2pStatsMonitor.dispose();
  607. }
  608. }