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

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