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

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