Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

AvgRTPStatsReporter.js 33KB

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