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.

RTPStatsCollector.js 43KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308
  1. import browser from '../browser';
  2. import { browsers } from 'js-utils';
  3. import * as StatisticsEvents from '../../service/statistics/Events';
  4. import * as MediaType from '../../service/RTC/MediaType';
  5. const GlobalOnErrorHandler = require('../util/GlobalOnErrorHandler');
  6. const logger = require('jitsi-meet-logger').getLogger(__filename);
  7. /* Whether we support the browser we are running into for logging statistics */
  8. const browserSupported = browser.isChrome()
  9. || browser.isOpera() || browser.isFirefox()
  10. || browser.isNWJS() || browser.isElectron()
  11. || browser.isTemasysPluginUsed() || browser.isEdge()
  12. || browser.isSafariWithWebrtc();
  13. /**
  14. * The lib-jitsi-meet browser-agnostic names of the browser-specific keys
  15. * reported by RTCPeerConnection#getStats mapped by browser.
  16. */
  17. const KEYS_BY_BROWSER_TYPE = {};
  18. KEYS_BY_BROWSER_TYPE[browsers.FIREFOX] = {
  19. 'ssrc': 'ssrc',
  20. 'packetsReceived': 'packetsReceived',
  21. 'packetsLost': 'packetsLost',
  22. 'packetsSent': 'packetsSent',
  23. 'bytesReceived': 'bytesReceived',
  24. 'bytesSent': 'bytesSent',
  25. 'framerateMean': 'framerateMean',
  26. 'ip': 'ipAddress',
  27. 'port': 'portNumber',
  28. 'protocol': 'transport'
  29. };
  30. KEYS_BY_BROWSER_TYPE[browsers.CHROME] = {
  31. 'receiveBandwidth': 'googAvailableReceiveBandwidth',
  32. 'sendBandwidth': 'googAvailableSendBandwidth',
  33. 'remoteAddress': 'googRemoteAddress',
  34. 'transportType': 'googTransportType',
  35. 'localAddress': 'googLocalAddress',
  36. 'activeConnection': 'googActiveConnection',
  37. 'ssrc': 'ssrc',
  38. 'packetsReceived': 'packetsReceived',
  39. 'packetsSent': 'packetsSent',
  40. 'packetsLost': 'packetsLost',
  41. 'bytesReceived': 'bytesReceived',
  42. 'bytesSent': 'bytesSent',
  43. 'googFrameHeightReceived': 'googFrameHeightReceived',
  44. 'googFrameWidthReceived': 'googFrameWidthReceived',
  45. 'googFrameHeightSent': 'googFrameHeightSent',
  46. 'googFrameWidthSent': 'googFrameWidthSent',
  47. 'googFrameRateReceived': 'googFrameRateReceived',
  48. 'googFrameRateSent': 'googFrameRateSent',
  49. 'audioInputLevel': 'audioInputLevel',
  50. 'audioOutputLevel': 'audioOutputLevel',
  51. 'currentRoundTripTime': 'googRtt',
  52. 'remoteCandidateType': 'googRemoteCandidateType',
  53. 'localCandidateType': 'googLocalCandidateType',
  54. 'ip': 'ip',
  55. 'port': 'port',
  56. 'protocol': 'protocol'
  57. };
  58. KEYS_BY_BROWSER_TYPE[browsers.EDGE] = {
  59. 'sendBandwidth': 'googAvailableSendBandwidth',
  60. 'remoteAddress': 'remoteAddress',
  61. 'transportType': 'protocol',
  62. 'localAddress': 'localAddress',
  63. 'activeConnection': 'activeConnection',
  64. 'ssrc': 'ssrc',
  65. 'packetsReceived': 'packetsReceived',
  66. 'packetsSent': 'packetsSent',
  67. 'packetsLost': 'packetsLost',
  68. 'bytesReceived': 'bytesReceived',
  69. 'bytesSent': 'bytesSent',
  70. 'googFrameHeightReceived': 'frameHeight',
  71. 'googFrameWidthReceived': 'frameWidth',
  72. 'googFrameHeightSent': 'frameHeight',
  73. 'googFrameWidthSent': 'frameWidth',
  74. 'googFrameRateReceived': 'framesPerSecond',
  75. 'googFrameRateSent': 'framesPerSecond',
  76. 'audioInputLevel': 'audioLevel',
  77. 'audioOutputLevel': 'audioLevel',
  78. 'currentRoundTripTime': 'roundTripTime'
  79. };
  80. KEYS_BY_BROWSER_TYPE[browsers.OPERA]
  81. = KEYS_BY_BROWSER_TYPE[browsers.CHROME];
  82. KEYS_BY_BROWSER_TYPE[browsers.NWJS]
  83. = KEYS_BY_BROWSER_TYPE[browsers.CHROME];
  84. KEYS_BY_BROWSER_TYPE[browsers.ELECTRON]
  85. = KEYS_BY_BROWSER_TYPE[browsers.CHROME];
  86. KEYS_BY_BROWSER_TYPE[browsers.IEXPLORER]
  87. = KEYS_BY_BROWSER_TYPE[browsers.CHROME];
  88. KEYS_BY_BROWSER_TYPE[browsers.SAFARI]
  89. = KEYS_BY_BROWSER_TYPE[browsers.CHROME];
  90. KEYS_BY_BROWSER_TYPE[browsers.REACT_NATIVE]
  91. = KEYS_BY_BROWSER_TYPE[browsers.CHROME];
  92. /**
  93. * Calculates packet lost percent using the number of lost packets and the
  94. * number of all packet.
  95. * @param lostPackets the number of lost packets
  96. * @param totalPackets the number of all packets.
  97. * @returns {number} packet loss percent
  98. */
  99. function calculatePacketLoss(lostPackets, totalPackets) {
  100. if (!totalPackets || totalPackets <= 0
  101. || !lostPackets || lostPackets <= 0) {
  102. return 0;
  103. }
  104. return Math.round((lostPackets / totalPackets) * 100);
  105. }
  106. /**
  107. * Holds "statistics" for a single SSRC.
  108. * @constructor
  109. */
  110. function SsrcStats() {
  111. this.loss = {};
  112. this.bitrate = {
  113. download: 0,
  114. upload: 0
  115. };
  116. this.resolution = {};
  117. this.framerate = 0;
  118. }
  119. /**
  120. * Sets the "loss" object.
  121. * @param loss the value to set.
  122. */
  123. SsrcStats.prototype.setLoss = function(loss) {
  124. this.loss = loss || {};
  125. };
  126. /**
  127. * Sets resolution that belong to the ssrc represented by this instance.
  128. * @param resolution new resolution value to be set.
  129. */
  130. SsrcStats.prototype.setResolution = function(resolution) {
  131. this.resolution = resolution || {};
  132. };
  133. /**
  134. * Adds the "download" and "upload" fields from the "bitrate" parameter to
  135. * the respective fields of the "bitrate" field of this object.
  136. * @param bitrate an object holding the values to add.
  137. */
  138. SsrcStats.prototype.addBitrate = function(bitrate) {
  139. this.bitrate.download += bitrate.download;
  140. this.bitrate.upload += bitrate.upload;
  141. };
  142. /**
  143. * Resets the bit rate for given <tt>ssrc</tt> that belong to the peer
  144. * represented by this instance.
  145. */
  146. SsrcStats.prototype.resetBitrate = function() {
  147. this.bitrate.download = 0;
  148. this.bitrate.upload = 0;
  149. };
  150. /**
  151. * Sets the "framerate".
  152. * @param framerate the value to set.
  153. */
  154. SsrcStats.prototype.setFramerate = function(framerate) {
  155. this.framerate = framerate || 0;
  156. };
  157. /**
  158. *
  159. */
  160. function ConferenceStats() {
  161. /**
  162. * The bandwidth
  163. * @type {{}}
  164. */
  165. this.bandwidth = {};
  166. /**
  167. * The bit rate
  168. * @type {{}}
  169. */
  170. this.bitrate = {};
  171. /**
  172. * The packet loss rate
  173. * @type {{}}
  174. */
  175. this.packetLoss = null;
  176. /**
  177. * Array with the transport information.
  178. * @type {Array}
  179. */
  180. this.transport = [];
  181. }
  182. /* eslint-disable max-params */
  183. /**
  184. * <tt>StatsCollector</tt> registers for stats updates of given
  185. * <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
  186. * stats are extracted and put in {@link SsrcStats} objects. Once the processing
  187. * is done <tt>audioLevelsUpdateCallback</tt> is called with <tt>this</tt>
  188. * instance as an event source.
  189. *
  190. * @param peerconnection WebRTC PeerConnection object.
  191. * @param audioLevelsInterval
  192. * @param statsInterval stats refresh interval given in ms.
  193. * @param eventEmitter
  194. * @constructor
  195. */
  196. export default function StatsCollector(
  197. peerconnection,
  198. audioLevelsInterval,
  199. statsInterval,
  200. eventEmitter) {
  201. // StatsCollector depends entirely on the format of the reports returned by
  202. // RTCPeerConnection#getStats. Given that the value of
  203. // browser#getName() is very unlikely to change at runtime, it
  204. // makes sense to discover whether StatsCollector supports the executing
  205. // browser as soon as possible. Otherwise, (1) getStatValue would have to
  206. // needlessly check a "static" condition multiple times very very often and
  207. // (2) the lack of support for the executing browser would be discovered and
  208. // reported multiple times very very often too late in the execution in some
  209. // totally unrelated callback.
  210. /**
  211. * The browser type supported by this StatsCollector. In other words, the
  212. * type of the browser which initialized this StatsCollector
  213. * instance.
  214. * @private
  215. */
  216. this._browserType = browser.getName();
  217. const keys = KEYS_BY_BROWSER_TYPE[this._browserType];
  218. if (!keys) {
  219. // eslint-disable-next-line no-throw-literal
  220. throw `The browser type '${this._browserType}' isn't supported!`;
  221. }
  222. /**
  223. * The function which is to be used to retrieve the value associated in a
  224. * report returned by RTCPeerConnection#getStats with a lib-jitsi-meet
  225. * browser-agnostic name/key.
  226. *
  227. * @function
  228. * @private
  229. */
  230. this._getStatValue = this._defineGetStatValueMethod(keys);
  231. this._getNewStatValue = this._defineNewGetStatValueMethod(keys);
  232. this.peerconnection = peerconnection;
  233. this.baselineAudioLevelsReport = null;
  234. this.currentAudioLevelsReport = null;
  235. this.currentStatsReport = null;
  236. this.previousStatsReport = null;
  237. this.audioLevelsIntervalId = null;
  238. this.eventEmitter = eventEmitter;
  239. this.conferenceStats = new ConferenceStats();
  240. // Updates stats interval
  241. this.audioLevelsIntervalMilis = audioLevelsInterval;
  242. this.statsIntervalId = null;
  243. this.statsIntervalMilis = statsInterval;
  244. /**
  245. * Maps SSRC numbers to {@link SsrcStats}.
  246. * @type {Map<number,SsrcStats}
  247. */
  248. this.ssrc2stats = new Map();
  249. }
  250. /* eslint-enable max-params */
  251. /**
  252. * Stops stats updates.
  253. */
  254. StatsCollector.prototype.stop = function() {
  255. if (this.audioLevelsIntervalId) {
  256. clearInterval(this.audioLevelsIntervalId);
  257. this.audioLevelsIntervalId = null;
  258. }
  259. if (this.statsIntervalId) {
  260. clearInterval(this.statsIntervalId);
  261. this.statsIntervalId = null;
  262. }
  263. };
  264. /**
  265. * Callback passed to <tt>getStats</tt> method.
  266. * @param error an error that occurred on <tt>getStats</tt> call.
  267. */
  268. StatsCollector.prototype.errorCallback = function(error) {
  269. GlobalOnErrorHandler.callErrorHandler(error);
  270. logger.error('Get stats error', error);
  271. this.stop();
  272. };
  273. /**
  274. * Starts stats updates.
  275. */
  276. StatsCollector.prototype.start = function(startAudioLevelStats) {
  277. const self = this;
  278. if (startAudioLevelStats) {
  279. this.audioLevelsIntervalId = setInterval(
  280. () => {
  281. // Interval updates
  282. self.peerconnection.getStats(
  283. report => {
  284. let results = null;
  285. if (!report || !report.result
  286. || typeof report.result !== 'function') {
  287. results = report;
  288. } else {
  289. results = report.result();
  290. }
  291. self.currentAudioLevelsReport = results;
  292. if (browser.isSafariWithWebrtc()
  293. || browser.isFirefox()) {
  294. self.processNewAudioLevelReport();
  295. } else {
  296. self.processAudioLevelReport();
  297. }
  298. self.baselineAudioLevelsReport
  299. = self.currentAudioLevelsReport;
  300. },
  301. self.errorCallback
  302. );
  303. },
  304. self.audioLevelsIntervalMilis
  305. );
  306. }
  307. if (browserSupported) {
  308. this.statsIntervalId = setInterval(
  309. () => {
  310. // Interval updates
  311. self.peerconnection.getStats(
  312. report => {
  313. let results = null;
  314. if (!report || !report.result
  315. || typeof report.result !== 'function') {
  316. // firefox
  317. results = report;
  318. } else {
  319. // chrome
  320. results = report.result();
  321. }
  322. self.currentStatsReport = results;
  323. try {
  324. if (browser.isSafariWithWebrtc()
  325. || browser.isFirefox()) {
  326. self.processNewStatsReport();
  327. } else {
  328. self.processStatsReport();
  329. }
  330. } catch (e) {
  331. GlobalOnErrorHandler.callErrorHandler(e);
  332. logger.error(`Unsupported key:${e}`, e);
  333. }
  334. self.previousStatsReport = self.currentStatsReport;
  335. },
  336. self.errorCallback
  337. );
  338. },
  339. self.statsIntervalMilis
  340. );
  341. }
  342. };
  343. /**
  344. * Defines a function which (1) is to be used as a StatsCollector method and (2)
  345. * gets the value from a specific report returned by RTCPeerConnection#getStats
  346. * associated with a lib-jitsi-meet browser-agnostic name.
  347. *
  348. * @param {Object.<string,string>} keys the map of LibJitsi browser-agnostic
  349. * names to RTCPeerConnection#getStats browser-specific keys
  350. */
  351. StatsCollector.prototype._defineGetStatValueMethod = function(keys) {
  352. // Define the function which converts a lib-jitsi-meet browser-asnostic name
  353. // to a browser-specific key of a report returned by
  354. // RTCPeerConnection#getStats.
  355. const keyFromName = function(name) {
  356. const key = keys[name];
  357. if (key) {
  358. return key;
  359. }
  360. // eslint-disable-next-line no-throw-literal
  361. throw `The property '${name}' isn't supported!`;
  362. };
  363. // Define the function which retrieves the value from a specific report
  364. // returned by RTCPeerConnection#getStats associated with a given
  365. // browser-specific key.
  366. let itemStatByKey;
  367. switch (this._browserType) {
  368. case browsers.CHROME:
  369. case browsers.OPERA:
  370. case browsers.NWJS:
  371. case browsers.ELECTRON:
  372. // TODO What about other types of browser which are based on Chrome such
  373. // as NW.js? Every time we want to support a new type browser we have to
  374. // go and add more conditions (here and in multiple other places).
  375. // Cannot we do a feature detection instead of a browser type check? For
  376. // example, if item has a stat property of type function, then it's very
  377. // likely that whoever defined it wanted you to call it in order to
  378. // retrieve the value associated with a specific key.
  379. itemStatByKey = (item, key) => item.stat(key);
  380. break;
  381. case browsers.REACT_NATIVE:
  382. // The implementation provided by react-native-webrtc follows the
  383. // Objective-C WebRTC API: RTCStatsReport has a values property of type
  384. // Array in which each element is a key-value pair.
  385. itemStatByKey = function(item, key) {
  386. let value;
  387. item.values.some(pair => {
  388. if (pair.hasOwnProperty(key)) {
  389. value = pair[key];
  390. return true;
  391. }
  392. return false;
  393. });
  394. return value;
  395. };
  396. break;
  397. case browsers.EDGE:
  398. itemStatByKey = (item, key) => item[key];
  399. break;
  400. default:
  401. itemStatByKey = (item, key) => item[key];
  402. }
  403. // Compose the 2 functions defined above to get a function which retrieves
  404. // the value from a specific report returned by RTCPeerConnection#getStats
  405. // associated with a specific lib-jitsi-meet browser-agnostic name.
  406. return (item, name) => itemStatByKey(item, keyFromName(name));
  407. };
  408. /**
  409. * Obtains a stat value from given stat and converts it to a non-negative
  410. * number. If the value is either invalid or negative then 0 will be returned.
  411. * @param report
  412. * @param {string} name
  413. * @return {number}
  414. * @private
  415. */
  416. StatsCollector.prototype.getNonNegativeStat = function(report, name) {
  417. let value = this._getStatValue(report, name);
  418. if (typeof value !== 'number') {
  419. value = Number(value);
  420. }
  421. if (isNaN(value)) {
  422. return 0;
  423. }
  424. return Math.max(0, value);
  425. };
  426. /* eslint-disable no-continue */
  427. /**
  428. * Stats processing logic.
  429. */
  430. StatsCollector.prototype.processStatsReport = function() {
  431. if (!this.previousStatsReport) {
  432. return;
  433. }
  434. const getStatValue = this._getStatValue;
  435. const byteSentStats = {};
  436. for (const idx in this.currentStatsReport) {
  437. if (!this.currentStatsReport.hasOwnProperty(idx)) {
  438. continue;
  439. }
  440. const now = this.currentStatsReport[idx];
  441. // The browser API may return "undefined" values in the array
  442. if (!now) {
  443. continue;
  444. }
  445. try {
  446. const receiveBandwidth = getStatValue(now, 'receiveBandwidth');
  447. const sendBandwidth = getStatValue(now, 'sendBandwidth');
  448. if (receiveBandwidth || sendBandwidth) {
  449. this.conferenceStats.bandwidth = {
  450. 'download': Math.round(receiveBandwidth / 1000),
  451. 'upload': Math.round(sendBandwidth / 1000)
  452. };
  453. }
  454. } catch (e) { /* not supported*/ }
  455. if (now.type === 'googCandidatePair') {
  456. let active, ip, localCandidateType, localip,
  457. remoteCandidateType, rtt, type;
  458. try {
  459. active = getStatValue(now, 'activeConnection');
  460. if (!active) {
  461. continue;
  462. }
  463. ip = getStatValue(now, 'remoteAddress');
  464. type = getStatValue(now, 'transportType');
  465. localip = getStatValue(now, 'localAddress');
  466. localCandidateType = getStatValue(now, 'localCandidateType');
  467. remoteCandidateType = getStatValue(now, 'remoteCandidateType');
  468. rtt = this.getNonNegativeStat(now, 'currentRoundTripTime');
  469. } catch (e) { /* not supported*/ }
  470. if (!ip || !type || !localip || active !== 'true') {
  471. continue;
  472. }
  473. // Save the address unless it has been saved already.
  474. const conferenceStatsTransport = this.conferenceStats.transport;
  475. if (!conferenceStatsTransport.some(
  476. t =>
  477. t.ip === ip
  478. && t.type === type
  479. && t.localip === localip)) {
  480. conferenceStatsTransport.push({
  481. ip,
  482. type,
  483. localip,
  484. p2p: this.peerconnection.isP2P,
  485. localCandidateType,
  486. remoteCandidateType,
  487. rtt
  488. });
  489. }
  490. continue;
  491. }
  492. if (now.type === 'candidatepair') {
  493. // we need succeeded and selected pairs only
  494. if (now.state !== 'succeeded' || !now.selected) {
  495. continue;
  496. }
  497. const local = this.currentStatsReport[now.localCandidateId];
  498. const remote = this.currentStatsReport[now.remoteCandidateId];
  499. this.conferenceStats.transport.push({
  500. ip: `${remote.ipAddress}:${remote.portNumber}`,
  501. type: local.transport,
  502. localip: `${local.ipAddress}:${local.portNumber}`,
  503. p2p: this.peerconnection.isP2P,
  504. localCandidateType: local.candidateType,
  505. remoteCandidateType: remote.candidateType
  506. });
  507. }
  508. // NOTE: Edge's proprietary stats via RTCIceTransport.msGetStats().
  509. if (now.msType === 'transportdiagnostics') {
  510. this.conferenceStats.transport.push({
  511. ip: now.remoteAddress,
  512. type: now.protocol,
  513. localip: now.localAddress,
  514. p2p: this.peerconnection.isP2P
  515. });
  516. }
  517. if (now.type !== 'ssrc' && now.type !== 'outboundrtp'
  518. && now.type !== 'inboundrtp' && now.type !== 'track') {
  519. continue;
  520. }
  521. // NOTE: In Edge, stats with type "inboundrtp" and "outboundrtp" are
  522. // completely useless, so ignore them.
  523. if (browser.isEdge()
  524. && (now.type === 'inboundrtp' || now.type === 'outboundrtp')) {
  525. continue;
  526. }
  527. const before = this.previousStatsReport[idx];
  528. let ssrc = this.getNonNegativeStat(now, 'ssrc');
  529. // If type="track", take the first SSRC from ssrcIds.
  530. if (now.type === 'track' && Array.isArray(now.ssrcIds)) {
  531. ssrc = Number(now.ssrcIds[0]);
  532. }
  533. if (!before || !ssrc) {
  534. continue;
  535. }
  536. // isRemote is available only in FF and is ignored in case of chrome
  537. // according to the spec
  538. // https://www.w3.org/TR/webrtc-stats/#dom-rtcrtpstreamstats-isremote
  539. // when isRemote is true indicates that the measurements were done at
  540. // the remote endpoint and reported in an RTCP RR/XR.
  541. // Fixes a problem where we are calculating local stats wrong adding
  542. // the sent bytes to the local download bitrate.
  543. // In new W3 stats spec, type="track" has a remoteSource boolean
  544. // property.
  545. // Edge uses the new format, so skip this check.
  546. if (!browser.isEdge()
  547. && (now.isRemote === true || now.remoteSource === true)) {
  548. continue;
  549. }
  550. let ssrcStats = this.ssrc2stats.get(ssrc);
  551. if (!ssrcStats) {
  552. ssrcStats = new SsrcStats();
  553. this.ssrc2stats.set(ssrc, ssrcStats);
  554. }
  555. let isDownloadStream = true;
  556. let key = 'packetsReceived';
  557. let packetsNow = getStatValue(now, key);
  558. if (typeof packetsNow === 'undefined'
  559. || packetsNow === null || packetsNow === '') {
  560. isDownloadStream = false;
  561. key = 'packetsSent';
  562. packetsNow = getStatValue(now, key);
  563. if (typeof packetsNow === 'undefined' || packetsNow === null) {
  564. logger.warn('No packetsReceived nor packetsSent stat found');
  565. }
  566. }
  567. if (!packetsNow || packetsNow < 0) {
  568. packetsNow = 0;
  569. }
  570. const packetsBefore = this.getNonNegativeStat(before, key);
  571. const packetsDiff = Math.max(0, packetsNow - packetsBefore);
  572. const packetsLostNow
  573. = this.getNonNegativeStat(now, 'packetsLost');
  574. const packetsLostBefore
  575. = this.getNonNegativeStat(before, 'packetsLost');
  576. const packetsLostDiff = Math.max(0, packetsLostNow - packetsLostBefore);
  577. ssrcStats.setLoss({
  578. packetsTotal: packetsDiff + packetsLostDiff,
  579. packetsLost: packetsLostDiff,
  580. isDownloadStream
  581. });
  582. const bytesReceivedNow
  583. = this.getNonNegativeStat(now, 'bytesReceived');
  584. const bytesReceivedBefore
  585. = this.getNonNegativeStat(before, 'bytesReceived');
  586. const bytesReceived
  587. = Math.max(0, bytesReceivedNow - bytesReceivedBefore);
  588. let bytesSent = 0;
  589. // TODO: clean this mess up!
  590. let nowBytesTransmitted = getStatValue(now, 'bytesSent');
  591. if (typeof nowBytesTransmitted === 'number'
  592. || typeof nowBytesTransmitted === 'string') {
  593. nowBytesTransmitted = Number(nowBytesTransmitted);
  594. if (!isNaN(nowBytesTransmitted)) {
  595. byteSentStats[ssrc] = nowBytesTransmitted;
  596. if (nowBytesTransmitted > 0) {
  597. bytesSent = nowBytesTransmitted
  598. - getStatValue(before, 'bytesSent');
  599. }
  600. }
  601. }
  602. bytesSent = Math.max(0, bytesSent);
  603. const timeMs = now.timestamp - before.timestamp;
  604. let bitrateReceivedKbps = 0, bitrateSentKbps = 0;
  605. if (timeMs > 0) {
  606. // TODO is there any reason to round here?
  607. bitrateReceivedKbps = Math.round((bytesReceived * 8) / timeMs);
  608. bitrateSentKbps = Math.round((bytesSent * 8) / timeMs);
  609. }
  610. ssrcStats.addBitrate({
  611. 'download': bitrateReceivedKbps,
  612. 'upload': bitrateSentKbps
  613. });
  614. const resolution = {
  615. height: null,
  616. width: null
  617. };
  618. try {
  619. let height, width;
  620. if ((height = getStatValue(now, 'googFrameHeightReceived'))
  621. && (width = getStatValue(now, 'googFrameWidthReceived'))) {
  622. resolution.height = height;
  623. resolution.width = width;
  624. } else if ((height = getStatValue(now, 'googFrameHeightSent'))
  625. && (width = getStatValue(now, 'googFrameWidthSent'))) {
  626. resolution.height = height;
  627. resolution.width = width;
  628. }
  629. } catch (e) { /* not supported*/ }
  630. // Tries to get frame rate
  631. let frameRate;
  632. try {
  633. frameRate = getStatValue(now, 'googFrameRateReceived')
  634. || getStatValue(now, 'googFrameRateSent') || 0;
  635. } catch (e) {
  636. // if it fails with previous properties(chrome),
  637. // let's try with another one (FF)
  638. try {
  639. frameRate = this.getNonNegativeStat(now, 'framerateMean');
  640. } catch (err) { /* not supported*/ }
  641. }
  642. ssrcStats.setFramerate(Math.round(frameRate || 0));
  643. if (resolution.height && resolution.width) {
  644. ssrcStats.setResolution(resolution);
  645. } else {
  646. ssrcStats.setResolution(null);
  647. }
  648. }
  649. this.eventEmitter.emit(
  650. StatisticsEvents.BYTE_SENT_STATS, this.peerconnection, byteSentStats);
  651. this._processAndEmitReport();
  652. };
  653. /**
  654. *
  655. */
  656. StatsCollector.prototype._processAndEmitReport = function() {
  657. // process stats
  658. const totalPackets = {
  659. download: 0,
  660. upload: 0
  661. };
  662. const lostPackets = {
  663. download: 0,
  664. upload: 0
  665. };
  666. let bitrateDownload = 0;
  667. let bitrateUpload = 0;
  668. const resolutions = {};
  669. const framerates = {};
  670. let audioBitrateDownload = 0;
  671. let audioBitrateUpload = 0;
  672. let videoBitrateDownload = 0;
  673. let videoBitrateUpload = 0;
  674. for (const [ ssrc, ssrcStats ] of this.ssrc2stats) {
  675. // process packet loss stats
  676. const loss = ssrcStats.loss;
  677. const type = loss.isDownloadStream ? 'download' : 'upload';
  678. totalPackets[type] += loss.packetsTotal;
  679. lostPackets[type] += loss.packetsLost;
  680. // process bitrate stats
  681. bitrateDownload += ssrcStats.bitrate.download;
  682. bitrateUpload += ssrcStats.bitrate.upload;
  683. // collect resolutions and framerates
  684. const track = this.peerconnection.getTrackBySSRC(ssrc);
  685. if (track) {
  686. if (track.isAudioTrack()) {
  687. audioBitrateDownload += ssrcStats.bitrate.download;
  688. audioBitrateUpload += ssrcStats.bitrate.upload;
  689. } else {
  690. videoBitrateDownload += ssrcStats.bitrate.download;
  691. videoBitrateUpload += ssrcStats.bitrate.upload;
  692. }
  693. const participantId = track.getParticipantId();
  694. if (participantId) {
  695. const resolution = ssrcStats.resolution;
  696. if (resolution.width
  697. && resolution.height
  698. && resolution.width !== -1
  699. && resolution.height !== -1) {
  700. const userResolutions = resolutions[participantId] || {};
  701. userResolutions[ssrc] = resolution;
  702. resolutions[participantId] = userResolutions;
  703. }
  704. if (ssrcStats.framerate !== 0) {
  705. const userFramerates = framerates[participantId] || {};
  706. userFramerates[ssrc] = ssrcStats.framerate;
  707. framerates[participantId] = userFramerates;
  708. }
  709. } else {
  710. logger.error(`No participant ID returned by ${track}`);
  711. }
  712. } else if (this.peerconnection.isP2P) {
  713. // NOTE For JVB connection there are JVB tracks reported in
  714. // the stats, but they do not have corresponding JitsiRemoteTrack
  715. // instances stored in TPC. It is not trivial to figure out that
  716. // a SSRC belongs to JVB, so we print this error ony for the P2P
  717. // connection for the time being.
  718. //
  719. // Also there will be reports for tracks removed from the session,
  720. // for the users who have left the conference.
  721. logger.error(
  722. `JitsiTrack not found for SSRC ${ssrc}`
  723. + ` in ${this.peerconnection}`);
  724. }
  725. ssrcStats.resetBitrate();
  726. }
  727. this.conferenceStats.bitrate = {
  728. 'upload': bitrateUpload,
  729. 'download': bitrateDownload
  730. };
  731. this.conferenceStats.bitrate.audio = {
  732. 'upload': audioBitrateUpload,
  733. 'download': audioBitrateDownload
  734. };
  735. this.conferenceStats.bitrate.video = {
  736. 'upload': videoBitrateUpload,
  737. 'download': videoBitrateDownload
  738. };
  739. this.conferenceStats.packetLoss = {
  740. total:
  741. calculatePacketLoss(
  742. lostPackets.download + lostPackets.upload,
  743. totalPackets.download + totalPackets.upload),
  744. download:
  745. calculatePacketLoss(lostPackets.download, totalPackets.download),
  746. upload:
  747. calculatePacketLoss(lostPackets.upload, totalPackets.upload)
  748. };
  749. this.eventEmitter.emit(
  750. StatisticsEvents.CONNECTION_STATS,
  751. this.peerconnection,
  752. {
  753. 'bandwidth': this.conferenceStats.bandwidth,
  754. 'bitrate': this.conferenceStats.bitrate,
  755. 'packetLoss': this.conferenceStats.packetLoss,
  756. 'resolution': resolutions,
  757. 'framerate': framerates,
  758. 'transport': this.conferenceStats.transport
  759. });
  760. this.conferenceStats.transport = [];
  761. };
  762. /**
  763. * Stats processing logic.
  764. */
  765. StatsCollector.prototype.processAudioLevelReport = function() {
  766. if (!this.baselineAudioLevelsReport) {
  767. return;
  768. }
  769. const getStatValue = this._getStatValue;
  770. for (const idx in this.currentAudioLevelsReport) {
  771. if (!this.currentAudioLevelsReport.hasOwnProperty(idx)) {
  772. continue;
  773. }
  774. const now = this.currentAudioLevelsReport[idx];
  775. if (now.type !== 'ssrc' && now.type !== 'track') {
  776. continue;
  777. }
  778. const before = this.baselineAudioLevelsReport[idx];
  779. let ssrc = this.getNonNegativeStat(now, 'ssrc');
  780. if (!ssrc && Array.isArray(now.ssrcIds)) {
  781. ssrc = Number(now.ssrcIds[0]);
  782. }
  783. if (!before) {
  784. logger.warn(`${ssrc} not enough data`);
  785. continue;
  786. }
  787. if (!ssrc) {
  788. if ((Date.now() - now.timestamp) < 3000) {
  789. logger.warn('No ssrc: ');
  790. }
  791. continue;
  792. }
  793. // Audio level
  794. let audioLevel;
  795. try {
  796. audioLevel
  797. = getStatValue(now, 'audioInputLevel')
  798. || getStatValue(now, 'audioOutputLevel');
  799. } catch (e) { /* not supported*/
  800. logger.warn('Audio Levels are not available in the statistics.');
  801. clearInterval(this.audioLevelsIntervalId);
  802. return;
  803. }
  804. if (audioLevel) {
  805. let isLocal;
  806. // If type="ssrc" (legacy) check whether they are received packets.
  807. if (now.type === 'ssrc') {
  808. isLocal = !getStatValue(now, 'packetsReceived');
  809. // If type="track", check remoteSource boolean property.
  810. } else {
  811. isLocal = !now.remoteSource;
  812. }
  813. // According to the W3C WebRTC Stats spec, audioLevel should be in
  814. // 0..1 range (0 == silence). However browsers don't behave that
  815. // way so we must convert it to 0..1.
  816. //
  817. // In Edge the range is -100..0 (-100 == silence) measured in dB,
  818. // so convert to linear. The levels are set to 0 for remote tracks,
  819. // so don't convert those, since 0 means "the maximum" in Edge.
  820. if (browser.isEdge()) {
  821. audioLevel = audioLevel < 0 ? Math.pow(10, audioLevel / 20) : 0;
  822. // TODO: Can't find specs about what this value really is, but it
  823. // seems to vary between 0 and around 32k.
  824. } else {
  825. audioLevel = audioLevel / 32767;
  826. }
  827. this.eventEmitter.emit(
  828. StatisticsEvents.AUDIO_LEVEL,
  829. this.peerconnection,
  830. ssrc,
  831. audioLevel,
  832. isLocal);
  833. }
  834. }
  835. };
  836. /* eslint-enable no-continue */
  837. /**
  838. * New promised based getStats report processing.
  839. * Tested with chrome, firefox and safari. Not switching it on for for chrome as
  840. * frameRate stat is missing and calculating it using framesSent,
  841. * gives values double the values seen in webrtc-internals.
  842. * https://w3c.github.io/webrtc-stats/
  843. */
  844. /**
  845. * Defines a function which (1) is to be used as a StatsCollector method and (2)
  846. * gets the value from a specific report returned by RTCPeerConnection#getStats
  847. * associated with a lib-jitsi-meet browser-agnostic name in case of using
  848. * Promised based getStats.
  849. *
  850. * @param {Object.<string,string>} keys the map of LibJitsi browser-agnostic
  851. * names to RTCPeerConnection#getStats browser-specific keys
  852. */
  853. StatsCollector.prototype._defineNewGetStatValueMethod = function(keys) {
  854. // Define the function which converts a lib-jitsi-meet browser-asnostic name
  855. // to a browser-specific key of a report returned by
  856. // RTCPeerConnection#getStats.
  857. const keyFromName = function(name) {
  858. const key = keys[name];
  859. if (key) {
  860. return key;
  861. }
  862. // eslint-disable-next-line no-throw-literal
  863. throw `The property '${name}' isn't supported!`;
  864. };
  865. // Compose the 2 functions defined above to get a function which retrieves
  866. // the value from a specific report returned by RTCPeerConnection#getStats
  867. // associated with a specific lib-jitsi-meet browser-agnostic name.
  868. return (item, name) => item[keyFromName(name)];
  869. };
  870. /**
  871. * Converts the value to a non-negative number.
  872. * If the value is either invalid or negative then 0 will be returned.
  873. * @param {*} v
  874. * @return {number}
  875. * @private
  876. */
  877. StatsCollector.prototype.getNonNegativeValue = function(v) {
  878. let value = v;
  879. if (typeof value !== 'number') {
  880. value = Number(value);
  881. }
  882. if (isNaN(value)) {
  883. return 0;
  884. }
  885. return Math.max(0, value);
  886. };
  887. /**
  888. * Calculates bitrate between before and now using a supplied field name and its
  889. * value in the stats.
  890. * @param {RTCInboundRtpStreamStats|RTCSentRtpStreamStats} now the current stats
  891. * @param {RTCInboundRtpStreamStats|RTCSentRtpStreamStats} before the
  892. * previous stats.
  893. * @param fieldName the field to use for calculations.
  894. * @return {number} the calculated bitrate between now and before.
  895. * @private
  896. */
  897. StatsCollector.prototype._calculateBitrate = function(now, before, fieldName) {
  898. const bytesNow = this.getNonNegativeValue(now[fieldName]);
  899. const bytesBefore = this.getNonNegativeValue(before[fieldName]);
  900. const bytesProcessed = Math.max(0, bytesNow - bytesBefore);
  901. const timeMs = now.timestamp - before.timestamp;
  902. let bitrateKbps = 0;
  903. if (timeMs > 0) {
  904. // TODO is there any reason to round here?
  905. bitrateKbps = Math.round((bytesProcessed * 8) / timeMs);
  906. }
  907. return bitrateKbps;
  908. };
  909. /**
  910. * Stats processing new getStats logic.
  911. */
  912. StatsCollector.prototype.processNewStatsReport = function() {
  913. if (!this.previousStatsReport) {
  914. return;
  915. }
  916. const getStatValue = this._getNewStatValue;
  917. const byteSentStats = {};
  918. this.currentStatsReport.forEach(now => {
  919. // RTCIceCandidatePairStats
  920. // https://w3c.github.io/webrtc-stats/#candidatepair-dict*
  921. if (now.type === 'candidate-pair'
  922. && now.nominated
  923. && now.state === 'succeeded') {
  924. const availableIncomingBitrate = now.availableIncomingBitrate;
  925. const availableOutgoingBitrate = now.availableOutgoingBitrate;
  926. if (availableIncomingBitrate || availableOutgoingBitrate) {
  927. this.conferenceStats.bandwidth = {
  928. 'download': Math.round(availableIncomingBitrate / 1000),
  929. 'upload': Math.round(availableOutgoingBitrate / 1000)
  930. };
  931. }
  932. const remoteUsedCandidate
  933. = this.currentStatsReport.get(now.remoteCandidateId);
  934. const localUsedCandidate
  935. = this.currentStatsReport.get(now.localCandidateId);
  936. // RTCIceCandidateStats
  937. // https://w3c.github.io/webrtc-stats/#icecandidate-dict*
  938. // safari currently does not provide ice candidates in stats
  939. if (remoteUsedCandidate && localUsedCandidate) {
  940. // FF uses non-standard ipAddress, portNumber, transport
  941. // instead of ip, port, protocol
  942. const remoteIpAddress = getStatValue(remoteUsedCandidate, 'ip');
  943. const remotePort = getStatValue(remoteUsedCandidate, 'port');
  944. const ip = `${remoteIpAddress}:${remotePort}`;
  945. const localIpAddress = getStatValue(localUsedCandidate, 'ip');
  946. const localPort = getStatValue(localUsedCandidate, 'port');
  947. const localIp = `${localIpAddress}:${localPort}`;
  948. const type = getStatValue(remoteUsedCandidate, 'protocol');
  949. // Save the address unless it has been saved already.
  950. const conferenceStatsTransport = this.conferenceStats.transport;
  951. if (!conferenceStatsTransport.some(
  952. t =>
  953. t.ip === ip
  954. && t.type === type
  955. && t.localip === localIp)) {
  956. conferenceStatsTransport.push({
  957. ip,
  958. type,
  959. localIp,
  960. p2p: this.peerconnection.isP2P,
  961. localCandidateType: localUsedCandidate.candidateType,
  962. remoteCandidateType: remoteUsedCandidate.candidateType,
  963. networkType: localUsedCandidate.networkType,
  964. rtt: now.currentRoundTripTime * 1000
  965. });
  966. }
  967. }
  968. // RTCReceivedRtpStreamStats
  969. // https://w3c.github.io/webrtc-stats/#receivedrtpstats-dict*
  970. // RTCSentRtpStreamStats
  971. // https://w3c.github.io/webrtc-stats/#sentrtpstats-dict*
  972. } else if (now.type === 'inbound-rtp' || now.type === 'outbound-rtp') {
  973. const before = this.previousStatsReport.get(now.id);
  974. const ssrc = this.getNonNegativeValue(now.ssrc);
  975. if (!before || !ssrc) {
  976. return;
  977. }
  978. let ssrcStats = this.ssrc2stats.get(ssrc);
  979. if (!ssrcStats) {
  980. ssrcStats = new SsrcStats();
  981. this.ssrc2stats.set(ssrc, ssrcStats);
  982. }
  983. let isDownloadStream = true;
  984. let key = 'packetsReceived';
  985. if (now.type === 'outbound-rtp') {
  986. isDownloadStream = false;
  987. key = 'packetsSent';
  988. }
  989. let packetsNow = now[key];
  990. if (!packetsNow || packetsNow < 0) {
  991. packetsNow = 0;
  992. }
  993. const packetsBefore = this.getNonNegativeValue(before[key]);
  994. const packetsDiff = Math.max(0, packetsNow - packetsBefore);
  995. const packetsLostNow
  996. = this.getNonNegativeValue(now.packetsLost);
  997. const packetsLostBefore
  998. = this.getNonNegativeValue(before.packetsLost);
  999. const packetsLostDiff
  1000. = Math.max(0, packetsLostNow - packetsLostBefore);
  1001. ssrcStats.setLoss({
  1002. packetsTotal: packetsDiff + packetsLostDiff,
  1003. packetsLost: packetsLostDiff,
  1004. isDownloadStream
  1005. });
  1006. if (now.type === 'inbound-rtp') {
  1007. ssrcStats.addBitrate({
  1008. 'download': this._calculateBitrate(
  1009. now, before, 'bytesReceived'),
  1010. 'upload': 0
  1011. });
  1012. // RTCInboundRtpStreamStats
  1013. // https://w3c.github.io/webrtc-stats/#inboundrtpstats-dict*
  1014. // TODO: can we use framesDecoded for frame rate, available
  1015. // in chrome
  1016. } else {
  1017. byteSentStats[ssrc] = this.getNonNegativeValue(now.bytesSent);
  1018. ssrcStats.addBitrate({
  1019. 'download': 0,
  1020. 'upload': this._calculateBitrate(
  1021. now, before, 'bytesSent')
  1022. });
  1023. // RTCOutboundRtpStreamStats
  1024. // https://w3c.github.io/webrtc-stats/#outboundrtpstats-dict*
  1025. // TODO: can we use framesEncoded for frame rate, available
  1026. // in chrome
  1027. }
  1028. // FF has framerateMean out of spec
  1029. const framerateMean = now.framerateMean;
  1030. if (framerateMean) {
  1031. ssrcStats.setFramerate(Math.round(framerateMean || 0));
  1032. }
  1033. // track for resolution
  1034. // RTCVideoHandlerStats
  1035. // https://w3c.github.io/webrtc-stats/#vststats-dict*
  1036. // RTCMediaHandlerStats
  1037. // https://w3c.github.io/webrtc-stats/#mststats-dict*
  1038. } else if (now.type === 'track') {
  1039. const resolution = {
  1040. height: now.frameHeight,
  1041. width: now.frameWidth
  1042. };
  1043. // Tries to get frame rate
  1044. let frameRate = now.framesPerSecond;
  1045. if (!frameRate) {
  1046. // we need to calculate it
  1047. const before = this.previousStatsReport.get(now.id);
  1048. if (before) {
  1049. const timeMs = now.timestamp - before.timestamp;
  1050. if (timeMs > 0 && now.framesSent) {
  1051. const numberOfFramesSinceBefore
  1052. = now.framesSent - before.framesSent;
  1053. frameRate = (numberOfFramesSinceBefore / timeMs) * 1000;
  1054. }
  1055. }
  1056. if (!frameRate) {
  1057. return;
  1058. }
  1059. }
  1060. const trackIdentifier = now.trackIdentifier;
  1061. const ssrc = this.peerconnection.getSsrcByTrackId(trackIdentifier);
  1062. let ssrcStats = this.ssrc2stats.get(ssrc);
  1063. if (!ssrcStats) {
  1064. ssrcStats = new SsrcStats();
  1065. this.ssrc2stats.set(ssrc, ssrcStats);
  1066. }
  1067. ssrcStats.setFramerate(Math.round(frameRate || 0));
  1068. if (resolution.height && resolution.width) {
  1069. ssrcStats.setResolution(resolution);
  1070. } else {
  1071. ssrcStats.setResolution(null);
  1072. }
  1073. }
  1074. });
  1075. this.eventEmitter.emit(
  1076. StatisticsEvents.BYTE_SENT_STATS, this.peerconnection, byteSentStats);
  1077. this._processAndEmitReport();
  1078. };
  1079. /**
  1080. * Stats processing logic.
  1081. */
  1082. StatsCollector.prototype.processNewAudioLevelReport = function() {
  1083. if (!this.baselineAudioLevelsReport) {
  1084. return;
  1085. }
  1086. this.currentAudioLevelsReport.forEach(now => {
  1087. if (now.type !== 'track') {
  1088. return;
  1089. }
  1090. // Audio level
  1091. const audioLevel = now.audioLevel;
  1092. if (!audioLevel) {
  1093. return;
  1094. }
  1095. const trackIdentifier = now.trackIdentifier;
  1096. const ssrc = this.peerconnection.getSsrcByTrackId(trackIdentifier);
  1097. if (ssrc) {
  1098. const isLocal
  1099. = ssrc === this.peerconnection.getLocalSSRC(
  1100. this.peerconnection.getLocalTracks(MediaType.AUDIO));
  1101. this.eventEmitter.emit(
  1102. StatisticsEvents.AUDIO_LEVEL,
  1103. this.peerconnection,
  1104. ssrc,
  1105. audioLevel,
  1106. isLocal);
  1107. }
  1108. });
  1109. };
  1110. /**
  1111. * End new promised based getStats processing methods.
  1112. */