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

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