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

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