modified lib-jitsi-meet dev repo
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

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