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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014
  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 pixels for remote screen streaming videos (reported only if
  455. * not a <tt>NaN</tt>).
  456. * @type {AverageStatReport}
  457. * @private
  458. */
  459. this._avgRemoteCameraPixels
  460. = new AverageStatReport('stat_avg_pixels_remote');
  461. /**
  462. * Average pixels for remote screen streaming videos (reported only if
  463. * not a <tt>NaN</tt>).
  464. * @type {AverageStatReport}
  465. * @private
  466. */
  467. this._avgRemoteScreenPixels
  468. = new AverageStatReport('stat_avg_pixels_screen_remote');
  469. /**
  470. * Average pixels for local video (camera)
  471. * @type {AverageStatReport}
  472. * @private
  473. */
  474. this._avgLocalCameraPixels
  475. = new AverageStatReport('stat_avg_pixels_local');
  476. /**
  477. * Average pixels for local screen streaming video (reported only if not
  478. * a <tt>NaN</tt>).
  479. * @type {AverageStatReport}
  480. * @private
  481. */
  482. this._avgLocalScreenPixels
  483. = new AverageStatReport('stat_avg_pixels_screen_local');
  484. /**
  485. * Average connection quality as defined by
  486. * the {@link ConnectionQuality} module.
  487. * @type {AverageStatReport}
  488. * @private
  489. */
  490. this._avgCQ = new AverageStatReport('stat_avg_cq');
  491. this._onLocalStatsUpdated = data => this._calculateAvgStats(data);
  492. conference.on(
  493. ConnectionQualityEvents.LOCAL_STATS_UPDATED,
  494. this._onLocalStatsUpdated);
  495. this._onP2PStatusChanged = () => {
  496. logger.debug('Resetting average stats calculation');
  497. this._resetAvgStats();
  498. this.jvbStatsMonitor._resetAvgStats();
  499. this.p2pStatsMonitor._resetAvgStats();
  500. };
  501. conference.on(
  502. ConferenceEvents.P2P_STATUS,
  503. this._onP2PStatusChanged);
  504. this._onJvb121StatusChanged = (oldStatus, newStatus) => {
  505. // We want to reset only on the transition from false => true,
  506. // because otherwise those stats are resetted on JVB <=> P2P
  507. // transition.
  508. if (newStatus === true) {
  509. logger.info('Resetting JVB avg RTP stats');
  510. this._resetAvgJvbStats();
  511. }
  512. };
  513. conference.on(
  514. ConferenceEvents.JVB121_STATUS,
  515. this._onJvb121StatusChanged);
  516. this.jvbStatsMonitor
  517. = new ConnectionAvgStats(this, false /* JVB */, n);
  518. this.p2pStatsMonitor
  519. = new ConnectionAvgStats(this, true /* P2P */, n);
  520. }
  521. /**
  522. * Processes next batch of stats reported on
  523. * {@link ConnectionQualityEvents.LOCAL_STATS_UPDATED}.
  524. * @param {go figure} data
  525. * @private
  526. */
  527. _calculateAvgStats(data) {
  528. if (!data) {
  529. logger.error('No stats');
  530. return;
  531. }
  532. const isP2P = this._conference.isP2PActive();
  533. const confSize = this._conference.getParticipantCount();
  534. if (!isP2P && confSize < 2) {
  535. // There's no point in collecting stats for a JVB conference of 1.
  536. // That happens for short period of time after everyone leaves
  537. // the room, until Jicofo terminates the session.
  538. return;
  539. }
  540. /* Uncomment to figure out stats structure
  541. for (const key in data) {
  542. if (data.hasOwnProperty(key)) {
  543. logger.info(`local stat ${key}: `, data[key]);
  544. }
  545. } */
  546. const bitrate = data.bitrate;
  547. const bandwidth = data.bandwidth;
  548. const packetLoss = data.packetLoss;
  549. const frameRate = data.framerate;
  550. const resolution = data.resolution;
  551. if (!bitrate) {
  552. logger.error('No "bitrate"');
  553. return;
  554. } else if (!bandwidth) {
  555. logger.error('No "bandwidth"');
  556. return;
  557. } else if (!packetLoss) {
  558. logger.error('No "packetloss"');
  559. return;
  560. } else if (!frameRate) {
  561. logger.error('No "framerate"');
  562. return;
  563. } else if (!resolution) {
  564. logger.error('No resolution');
  565. return;
  566. }
  567. this._avgAudioBitrateUp.addNext(bitrate.audio.upload);
  568. this._avgAudioBitrateDown.addNext(bitrate.audio.download);
  569. this._avgVideoBitrateUp.addNext(bitrate.video.upload);
  570. this._avgVideoBitrateDown.addNext(bitrate.video.download);
  571. if (RTCBrowserType.supportsBandwidthStatistics()) {
  572. this._avgBandwidthUp.addNext(bandwidth.upload);
  573. this._avgBandwidthDown.addNext(bandwidth.download);
  574. }
  575. this._avgPacketLossUp.addNext(packetLoss.upload);
  576. this._avgPacketLossDown.addNext(packetLoss.download);
  577. this._avgPacketLossTotal.addNext(packetLoss.total);
  578. this._avgCQ.addNext(data.connectionQuality);
  579. if (frameRate) {
  580. this._avgRemoteFPS.addNext(
  581. this._calculateAvgVideoFps(
  582. frameRate, false /* remote */, VideoType.CAMERA));
  583. this._avgRemoteScreenFPS.addNext(
  584. this._calculateAvgVideoFps(
  585. frameRate, false /* remote */, VideoType.DESKTOP));
  586. this._avgLocalFPS.addNext(
  587. this._calculateAvgVideoFps(
  588. frameRate, true /* local */, VideoType.CAMERA));
  589. this._avgLocalScreenFPS.addNext(
  590. this._calculateAvgVideoFps(
  591. frameRate, true /* local */, VideoType.DESKTOP));
  592. }
  593. if (resolution) {
  594. this._avgRemoteCameraPixels.addNext(
  595. this._calculateAvgVideoPixels(
  596. resolution, false /* remote */, VideoType.CAMERA));
  597. this._avgRemoteScreenPixels.addNext(
  598. this._calculateAvgVideoPixels(
  599. resolution, false /* remote */, VideoType.DESKTOP));
  600. this._avgLocalCameraPixels.addNext(
  601. this._calculateAvgVideoPixels(
  602. resolution, true /* local */, VideoType.CAMERA));
  603. this._avgLocalScreenPixels.addNext(
  604. this._calculateAvgVideoPixels(
  605. resolution, true /* local */, VideoType.DESKTOP));
  606. }
  607. this._sampleIdx += 1;
  608. if (this._sampleIdx >= this._n) {
  609. const batchReport = {
  610. p2p: isP2P,
  611. size: confSize
  612. };
  613. if (data.transport && data.transport.length) {
  614. Object.assign(batchReport, {
  615. localCandidateType: data.transport[0].localCandidateType,
  616. remoteCandidateType: data.transport[0].remoteCandidateType,
  617. transportType: data.transport[0].type
  618. });
  619. }
  620. this._avgAudioBitrateUp.appendReport(batchReport);
  621. this._avgAudioBitrateDown.appendReport(batchReport);
  622. this._avgVideoBitrateUp.appendReport(batchReport);
  623. this._avgVideoBitrateDown.appendReport(batchReport);
  624. if (RTCBrowserType.supportsBandwidthStatistics()) {
  625. this._avgBandwidthUp.appendReport(batchReport);
  626. this._avgBandwidthDown.appendReport(batchReport);
  627. }
  628. this._avgPacketLossUp.appendReport(batchReport);
  629. this._avgPacketLossDown.appendReport(batchReport);
  630. this._avgPacketLossTotal.appendReport(batchReport);
  631. this._avgRemoteFPS.appendReport(batchReport);
  632. if (!isNaN(this._avgRemoteScreenFPS.calculate())) {
  633. this._avgRemoteScreenFPS.appendReport(batchReport);
  634. }
  635. this._avgLocalFPS.appendReport(batchReport);
  636. if (!isNaN(this._avgLocalScreenFPS.calculate())) {
  637. this._avgLocalScreenFPS.appendReport(batchReport);
  638. }
  639. this._avgRemoteCameraPixels.appendReport(batchReport);
  640. if (!isNaN(this._avgRemoteScreenPixels.calculate())) {
  641. this._avgRemoteScreenPixels.appendReport(batchReport);
  642. }
  643. this._avgLocalCameraPixels.appendReport(batchReport);
  644. if (!isNaN(this._avgLocalScreenPixels.calculate())) {
  645. this._avgLocalScreenPixels.appendReport(batchReport);
  646. }
  647. this._avgCQ.appendReport(batchReport);
  648. Statistics.analytics.sendEvent(AVG_RTP_STATS_EVENT, batchReport);
  649. this._resetAvgStats();
  650. }
  651. }
  652. /**
  653. * Calculates average number of pixels for the report
  654. *
  655. * @param {map} peerResolutions a map of peer resolutions
  656. * @param {boolean} isLocal if the average is to be calculated for the local
  657. * video or <tt>false</tt> if for remote videos.
  658. * @param {VideoType} videoType
  659. * @return {number|NaN} average number of pixels or <tt>NaN</tt> if there
  660. * are no samples.
  661. * @private
  662. */
  663. _calculateAvgVideoPixels(peerResolutions, isLocal, videoType) {
  664. let peerPixelsSum = 0;
  665. let peerCount = 0;
  666. const myID = this._conference.myUserId();
  667. for (const peerID of Object.keys(peerResolutions)) {
  668. if (isLocal ? peerID === myID : peerID !== myID) {
  669. const participant
  670. = isLocal
  671. ? null : this._conference.getParticipantById(peerID);
  672. const videosResolution = peerResolutions[peerID];
  673. // Do not continue without participant for non local peerID
  674. if ((isLocal || participant) && videosResolution) {
  675. const peerAvgPixels = this._calculatePeerAvgVideoPixels(
  676. videosResolution, participant, videoType);
  677. if (!isNaN(peerAvgPixels)) {
  678. peerPixelsSum += peerAvgPixels;
  679. peerCount += 1;
  680. }
  681. }
  682. }
  683. }
  684. return peerPixelsSum / peerCount;
  685. }
  686. /**
  687. * Calculate average pixels for either remote or local participant
  688. * @param {object} videos maps resolution per video SSRC
  689. * @param {JitsiParticipant|null} participant remote participant or
  690. * <tt>null</tt> for local video pixels calculation.
  691. * @param {VideoType} videoType the type of the video for which an average
  692. * will be calculated.
  693. * @return {number|NaN} average video pixels of all participant's videos or
  694. * <tt>NaN</tt> if currently not available
  695. * @private
  696. */
  697. _calculatePeerAvgVideoPixels(videos, participant, videoType) {
  698. let ssrcs = Object.keys(videos).map(ssrc => Number(ssrc));
  699. let videoTracks = null;
  700. // NOTE that this method is supposed to be called for the stats
  701. // received from the current peerconnection.
  702. const tpc = this._conference.getActivePeerConnection();
  703. if (participant) {
  704. videoTracks = participant.getTracksByMediaType(MediaType.VIDEO);
  705. if (videoTracks) {
  706. ssrcs
  707. = ssrcs.filter(
  708. ssrc => videoTracks.find(
  709. track => !track.isMuted()
  710. && track.getSSRC() === ssrc
  711. && track.videoType === videoType));
  712. }
  713. } else {
  714. videoTracks = this._conference.getLocalTracks(MediaType.VIDEO);
  715. ssrcs
  716. = ssrcs.filter(
  717. ssrc => videoTracks.find(
  718. track => !track.isMuted()
  719. && tpc.getLocalSSRC(track) === ssrc
  720. && track.videoType === videoType));
  721. }
  722. let peerPixelsSum = 0;
  723. let peerSsrcCount = 0;
  724. for (const ssrc of ssrcs) {
  725. const peerSsrcPixels
  726. = Number(videos[ssrc].height) * Number(videos[ssrc].width);
  727. // FPS is reported as 0 for users with no video
  728. if (!isNaN(peerSsrcPixels) && peerSsrcPixels > 0) {
  729. peerPixelsSum += peerSsrcPixels;
  730. peerSsrcCount += 1;
  731. }
  732. }
  733. return peerPixelsSum / peerSsrcCount;
  734. }
  735. /**
  736. * Calculates average FPS for the report
  737. * @param {go figure} frameRate
  738. * @param {boolean} isLocal if the average is to be calculated for the local
  739. * video or <tt>false</tt> if for remote videos.
  740. * @param {VideoType} videoType
  741. * @return {number|NaN} average FPS or <tt>NaN</tt> if there are no samples.
  742. * @private
  743. */
  744. _calculateAvgVideoFps(frameRate, isLocal, videoType) {
  745. let peerFpsSum = 0;
  746. let peerCount = 0;
  747. const myID = this._conference.myUserId();
  748. for (const peerID of Object.keys(frameRate)) {
  749. if (isLocal ? peerID === myID : peerID !== myID) {
  750. const participant
  751. = isLocal
  752. ? null : this._conference.getParticipantById(peerID);
  753. const videosFps = frameRate[peerID];
  754. // Do not continue without participant for non local peerID
  755. if ((isLocal || participant) && videosFps) {
  756. const peerAvgFPS
  757. = this._calculatePeerAvgVideoFps(
  758. videosFps, participant, videoType);
  759. if (!isNaN(peerAvgFPS)) {
  760. peerFpsSum += peerAvgFPS;
  761. peerCount += 1;
  762. }
  763. }
  764. }
  765. }
  766. return peerFpsSum / peerCount;
  767. }
  768. /**
  769. * Calculate average FPS for either remote or local participant
  770. * @param {object} videos maps FPS per video SSRC
  771. * @param {JitsiParticipant|null} participant remote participant or
  772. * <tt>null</tt> for local FPS calculation.
  773. * @param {VideoType} videoType the type of the video for which an average
  774. * will be calculated.
  775. * @return {number|NaN} average FPS of all participant's videos or
  776. * <tt>NaN</tt> if currently not available
  777. * @private
  778. */
  779. _calculatePeerAvgVideoFps(videos, participant, videoType) {
  780. let ssrcs = Object.keys(videos).map(ssrc => Number(ssrc));
  781. let videoTracks = null;
  782. // NOTE that this method is supposed to be called for the stats
  783. // received from the current peerconnection.
  784. const tpc = this._conference.getActivePeerConnection();
  785. if (participant) {
  786. videoTracks = participant.getTracksByMediaType(MediaType.VIDEO);
  787. if (videoTracks) {
  788. ssrcs
  789. = ssrcs.filter(
  790. ssrc => videoTracks.find(
  791. track => !track.isMuted()
  792. && track.getSSRC() === ssrc
  793. && track.videoType === videoType));
  794. }
  795. } else {
  796. videoTracks = this._conference.getLocalTracks(MediaType.VIDEO);
  797. ssrcs
  798. = ssrcs.filter(
  799. ssrc => videoTracks.find(
  800. track => !track.isMuted()
  801. && tpc.getLocalSSRC(track) === ssrc
  802. && track.videoType === videoType));
  803. }
  804. let peerFpsSum = 0;
  805. let peerSsrcCount = 0;
  806. for (const ssrc of ssrcs) {
  807. const peerSsrcFps = Number(videos[ssrc]);
  808. // FPS is reported as 0 for users with no video
  809. if (!isNaN(peerSsrcFps) && peerSsrcFps > 0) {
  810. peerFpsSum += peerSsrcFps;
  811. peerSsrcCount += 1;
  812. }
  813. }
  814. return peerFpsSum / peerSsrcCount;
  815. }
  816. /**
  817. * Resets the stats related to JVB connection. Must not be called when in
  818. * P2P mode, because then the {@link AverageStatReport} instances are
  819. * tracking P2P stats. Note that this should never happen unless something
  820. * is wrong with the P2P and JVB121 events.
  821. * @private
  822. */
  823. _resetAvgJvbStats() {
  824. this._resetAvgStats();
  825. this.jvbStatsMonitor._resetAvgStats();
  826. }
  827. /**
  828. * Reset cache of all averages and {@link _sampleIdx}.
  829. * @private
  830. */
  831. _resetAvgStats() {
  832. this._avgAudioBitrateUp.reset();
  833. this._avgAudioBitrateDown.reset();
  834. this._avgVideoBitrateUp.reset();
  835. this._avgVideoBitrateDown.reset();
  836. this._avgBandwidthUp.reset();
  837. this._avgBandwidthDown.reset();
  838. this._avgPacketLossUp.reset();
  839. this._avgPacketLossDown.reset();
  840. this._avgPacketLossTotal.reset();
  841. this._avgRemoteFPS.reset();
  842. this._avgRemoteScreenFPS.reset();
  843. this._avgLocalFPS.reset();
  844. this._avgLocalScreenFPS.reset();
  845. this._avgRemoteCameraPixels.reset();
  846. this._avgRemoteScreenPixels.reset();
  847. this._avgLocalCameraPixels.reset();
  848. this._avgLocalScreenPixels.reset();
  849. this._avgCQ.reset();
  850. this._sampleIdx = 0;
  851. }
  852. /**
  853. * Unregisters all event listeners and stops working.
  854. */
  855. dispose() {
  856. this._conference.off(
  857. ConferenceEvents.P2P_STATUS,
  858. this._onP2PStatusChanged);
  859. this._conference.off(
  860. ConnectionQualityEvents.LOCAL_STATS_UPDATED,
  861. this._onLocalStatsUpdated);
  862. this._conference.off(
  863. ConferenceEvents.JVB121_STATUS,
  864. this._onJvb121StatusChanged);
  865. this.jvbStatsMonitor.dispose();
  866. this.p2pStatsMonitor.dispose();
  867. }
  868. }