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 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. import { getLogger } from '@jitsi/logger';
  2. import { MediaType } from '../../service/RTC/MediaType';
  3. import * as StatisticsEvents from '../../service/statistics/Events';
  4. import browser from '../browser';
  5. import FeatureFlags from '../flags/FeatureFlags';
  6. const logger = getLogger(__filename);
  7. /**
  8. * Calculates packet lost percent using the number of lost packets and the
  9. * number of all packet.
  10. * @param lostPackets the number of lost packets
  11. * @param totalPackets the number of all packets.
  12. * @returns {number} packet loss percent
  13. */
  14. function calculatePacketLoss(lostPackets, totalPackets) {
  15. if (!totalPackets || totalPackets <= 0
  16. || !lostPackets || lostPackets <= 0) {
  17. return 0;
  18. }
  19. return Math.round((lostPackets / totalPackets) * 100);
  20. }
  21. /**
  22. * Holds "statistics" for a single SSRC.
  23. * @constructor
  24. */
  25. function SsrcStats() {
  26. this.loss = {};
  27. this.bitrate = {
  28. download: 0,
  29. upload: 0
  30. };
  31. this.resolution = {};
  32. this.framerate = 0;
  33. this.codec = '';
  34. }
  35. /**
  36. * Sets the "loss" object.
  37. * @param loss the value to set.
  38. */
  39. SsrcStats.prototype.setLoss = function(loss) {
  40. this.loss = loss || {};
  41. };
  42. /**
  43. * Sets resolution that belong to the ssrc represented by this instance.
  44. * @param resolution new resolution value to be set.
  45. */
  46. SsrcStats.prototype.setResolution = function(resolution) {
  47. this.resolution = resolution || {};
  48. };
  49. /**
  50. * Adds the "download" and "upload" fields from the "bitrate" parameter to
  51. * the respective fields of the "bitrate" field of this object.
  52. * @param bitrate an object holding the values to add.
  53. */
  54. SsrcStats.prototype.addBitrate = function(bitrate) {
  55. this.bitrate.download += bitrate.download;
  56. this.bitrate.upload += bitrate.upload;
  57. };
  58. /**
  59. * Resets the bit rate for given <tt>ssrc</tt> that belong to the peer
  60. * represented by this instance.
  61. */
  62. SsrcStats.prototype.resetBitrate = function() {
  63. this.bitrate.download = 0;
  64. this.bitrate.upload = 0;
  65. };
  66. /**
  67. * Sets the "framerate".
  68. * @param framerate the value to set.
  69. */
  70. SsrcStats.prototype.setFramerate = function(framerate) {
  71. this.framerate = framerate || 0;
  72. };
  73. SsrcStats.prototype.setCodec = function(codec) {
  74. this.codec = codec || '';
  75. };
  76. /**
  77. *
  78. */
  79. function ConferenceStats() {
  80. /**
  81. * The bandwidth
  82. * @type {{}}
  83. */
  84. this.bandwidth = {};
  85. /**
  86. * The bit rate
  87. * @type {{}}
  88. */
  89. this.bitrate = {};
  90. /**
  91. * The packet loss rate
  92. * @type {{}}
  93. */
  94. this.packetLoss = null;
  95. /**
  96. * Array with the transport information.
  97. * @type {Array}
  98. */
  99. this.transport = [];
  100. }
  101. /* eslint-disable max-params */
  102. /**
  103. * <tt>StatsCollector</tt> registers for stats updates of given
  104. * <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
  105. * stats are extracted and put in {@link SsrcStats} objects. Once the processing
  106. * is done <tt>audioLevelsUpdateCallback</tt> is called with <tt>this</tt>
  107. * instance as an event source.
  108. *
  109. * @param peerconnection WebRTC PeerConnection object.
  110. * @param audioLevelsInterval
  111. * @param statsInterval stats refresh interval given in ms.
  112. * @param eventEmitter
  113. * @constructor
  114. */
  115. export default function StatsCollector(peerconnection, audioLevelsInterval, statsInterval, eventEmitter) {
  116. this.peerconnection = peerconnection;
  117. this.currentStatsReport = null;
  118. this.previousStatsReport = null;
  119. this.audioLevelReportHistory = {};
  120. this.audioLevelsIntervalId = null;
  121. this.eventEmitter = eventEmitter;
  122. this.conferenceStats = new ConferenceStats();
  123. // Updates stats interval
  124. this.audioLevelsIntervalMilis = audioLevelsInterval;
  125. this.speakerList = [];
  126. this.statsIntervalId = null;
  127. this.statsIntervalMilis = statsInterval;
  128. /**
  129. * Maps SSRC numbers to {@link SsrcStats}.
  130. * @type {Map<number,SsrcStats}
  131. */
  132. this.ssrc2stats = new Map();
  133. }
  134. /**
  135. * Set the list of the remote speakers for which audio levels are to be calculated.
  136. *
  137. * @param {Array<string>} speakerList - Endpoint ids.
  138. * @returns {void}
  139. */
  140. StatsCollector.prototype.setSpeakerList = function(speakerList) {
  141. this.speakerList = speakerList;
  142. };
  143. /**
  144. * Stops stats updates.
  145. */
  146. StatsCollector.prototype.stop = function() {
  147. if (this.audioLevelsIntervalId) {
  148. clearInterval(this.audioLevelsIntervalId);
  149. this.audioLevelsIntervalId = null;
  150. }
  151. if (this.statsIntervalId) {
  152. clearInterval(this.statsIntervalId);
  153. this.statsIntervalId = null;
  154. }
  155. };
  156. /**
  157. * Callback passed to <tt>getStats</tt> method.
  158. * @param error an error that occurred on <tt>getStats</tt> call.
  159. */
  160. StatsCollector.prototype.errorCallback = function(error) {
  161. logger.error('Get stats error', error);
  162. this.stop();
  163. };
  164. /**
  165. * Starts stats updates.
  166. */
  167. StatsCollector.prototype.start = function(startAudioLevelStats) {
  168. if (startAudioLevelStats && browser.supportsReceiverStats()) {
  169. this.audioLevelsIntervalId = setInterval(
  170. () => {
  171. const audioLevels = this.peerconnection.getAudioLevels(this.speakerList);
  172. for (const ssrc in audioLevels) {
  173. if (audioLevels.hasOwnProperty(ssrc)) {
  174. // Use a scaling factor of 2.5 to report the same audio levels that getStats reports.
  175. const audioLevel = audioLevels[ssrc] * 2.5;
  176. this.eventEmitter.emit(
  177. StatisticsEvents.AUDIO_LEVEL,
  178. this.peerconnection,
  179. Number.parseInt(ssrc, 10),
  180. audioLevel,
  181. false /* isLocal */);
  182. }
  183. }
  184. },
  185. this.audioLevelsIntervalMilis
  186. );
  187. }
  188. const processStats = () => {
  189. // Interval updates
  190. this.peerconnection.getStats()
  191. .then(report => {
  192. this.currentStatsReport = typeof report?.result === 'function'
  193. ? report.result()
  194. : report;
  195. try {
  196. this.processStatsReport();
  197. } catch (error) {
  198. logger.error('Processing of RTP stats failed:', error);
  199. }
  200. this.previousStatsReport = this.currentStatsReport;
  201. })
  202. .catch(error => this.errorCallback(error));
  203. };
  204. processStats();
  205. this.statsIntervalId = setInterval(processStats, this.statsIntervalMilis);
  206. };
  207. /**
  208. *
  209. */
  210. StatsCollector.prototype._processAndEmitReport = function() {
  211. // process stats
  212. const totalPackets = {
  213. download: 0,
  214. upload: 0
  215. };
  216. const lostPackets = {
  217. download: 0,
  218. upload: 0
  219. };
  220. let bitrateDownload = 0;
  221. let bitrateUpload = 0;
  222. const resolutions = {};
  223. const framerates = {};
  224. const codecs = {};
  225. let audioBitrateDownload = 0;
  226. let audioBitrateUpload = 0;
  227. let videoBitrateDownload = 0;
  228. let videoBitrateUpload = 0;
  229. for (const [ ssrc, ssrcStats ] of this.ssrc2stats) {
  230. // process packet loss stats
  231. const loss = ssrcStats.loss;
  232. const type = loss.isDownloadStream ? 'download' : 'upload';
  233. totalPackets[type] += loss.packetsTotal;
  234. lostPackets[type] += loss.packetsLost;
  235. // process bitrate stats
  236. bitrateDownload += ssrcStats.bitrate.download;
  237. bitrateUpload += ssrcStats.bitrate.upload;
  238. ssrcStats.resetBitrate();
  239. // collect resolutions and framerates
  240. const track = this.peerconnection.getTrackBySSRC(ssrc);
  241. if (!track) {
  242. continue; // eslint-disable-line no-continue
  243. }
  244. let audioCodec;
  245. let videoCodec;
  246. if (track.isAudioTrack()) {
  247. audioBitrateDownload += ssrcStats.bitrate.download;
  248. audioBitrateUpload += ssrcStats.bitrate.upload;
  249. audioCodec = ssrcStats.codec;
  250. } else {
  251. videoBitrateDownload += ssrcStats.bitrate.download;
  252. videoBitrateUpload += ssrcStats.bitrate.upload;
  253. videoCodec = ssrcStats.codec;
  254. }
  255. const participantId = track.getParticipantId();
  256. if (!participantId) {
  257. // All tracks in ssrc-rewriting mode need not have a participant associated with it.
  258. if (!FeatureFlags.isSsrcRewritingSupported()) {
  259. logger.error(`No participant ID returned by ${track}`);
  260. }
  261. continue; // eslint-disable-line no-continue
  262. }
  263. const userCodecs = codecs[participantId] ?? { };
  264. userCodecs[ssrc] = {
  265. audio: audioCodec,
  266. video: videoCodec
  267. };
  268. codecs[participantId] = userCodecs;
  269. const { resolution } = ssrcStats;
  270. if (!track.isVideoTrack()
  271. || isNaN(resolution?.height)
  272. || isNaN(resolution?.width)
  273. || resolution.height === -1
  274. || resolution.width === -1) {
  275. continue; // eslint-disable-line no-continue
  276. }
  277. const userResolutions = resolutions[participantId] || {};
  278. // If simulcast (VP8) is used, there will be 3 "outbound-rtp" streams with different resolutions and 3
  279. // different SSRCs. Based on the requested resolution and the current cpu and available bandwidth
  280. // values, some of the streams might get suspended. Therefore the actual send resolution needs to be
  281. // calculated based on the outbound-rtp streams that are currently active for the simulcast case.
  282. // However for the SVC case, there will be only 1 "outbound-rtp" stream which will have the correct
  283. // send resolution width and height.
  284. if (track.isLocal() && !browser.supportsTrackBasedStats() && this.peerconnection.doesTrueSimulcast()) {
  285. const localSsrcs = this.peerconnection.getLocalVideoSSRCs(track);
  286. for (const localSsrc of localSsrcs) {
  287. const ssrcResolution = this.ssrc2stats.get(localSsrc)?.resolution;
  288. // The code processes resolution stats only for 'outbound-rtp' streams that are currently active.
  289. if (ssrcResolution?.height && ssrcResolution?.width) {
  290. resolution.height = Math.max(resolution.height, ssrcResolution.height);
  291. resolution.width = Math.max(resolution.width, ssrcResolution.width);
  292. }
  293. }
  294. }
  295. userResolutions[ssrc] = resolution;
  296. resolutions[participantId] = userResolutions;
  297. if (ssrcStats.framerate > 0) {
  298. const userFramerates = framerates[participantId] || {};
  299. userFramerates[ssrc] = ssrcStats.framerate;
  300. framerates[participantId] = userFramerates;
  301. }
  302. }
  303. this.conferenceStats.bitrate = {
  304. 'upload': bitrateUpload,
  305. 'download': bitrateDownload
  306. };
  307. this.conferenceStats.bitrate.audio = {
  308. 'upload': audioBitrateUpload,
  309. 'download': audioBitrateDownload
  310. };
  311. this.conferenceStats.bitrate.video = {
  312. 'upload': videoBitrateUpload,
  313. 'download': videoBitrateDownload
  314. };
  315. this.conferenceStats.packetLoss = {
  316. total:
  317. calculatePacketLoss(
  318. lostPackets.download + lostPackets.upload,
  319. totalPackets.download + totalPackets.upload),
  320. download:
  321. calculatePacketLoss(lostPackets.download, totalPackets.download),
  322. upload:
  323. calculatePacketLoss(lostPackets.upload, totalPackets.upload)
  324. };
  325. const avgAudioLevels = {};
  326. let localAvgAudioLevels;
  327. Object.keys(this.audioLevelReportHistory).forEach(ssrc => {
  328. const { data, isLocal } = this.audioLevelReportHistory[ssrc];
  329. const avgAudioLevel = data.reduce((sum, currentValue) => sum + currentValue) / data.length;
  330. if (isLocal) {
  331. localAvgAudioLevels = avgAudioLevel;
  332. } else {
  333. const track = this.peerconnection.getTrackBySSRC(Number(ssrc));
  334. if (track) {
  335. const participantId = track.getParticipantId();
  336. if (participantId) {
  337. avgAudioLevels[participantId] = avgAudioLevel;
  338. }
  339. }
  340. }
  341. });
  342. this.audioLevelReportHistory = {};
  343. this.eventEmitter.emit(
  344. StatisticsEvents.CONNECTION_STATS,
  345. this.peerconnection,
  346. {
  347. 'bandwidth': this.conferenceStats.bandwidth,
  348. 'bitrate': this.conferenceStats.bitrate,
  349. 'packetLoss': this.conferenceStats.packetLoss,
  350. 'resolution': resolutions,
  351. 'framerate': framerates,
  352. 'codec': codecs,
  353. 'transport': this.conferenceStats.transport,
  354. localAvgAudioLevels,
  355. avgAudioLevels
  356. });
  357. this.conferenceStats.transport = [];
  358. };
  359. /**
  360. * Converts the value to a non-negative number.
  361. * If the value is either invalid or negative then 0 will be returned.
  362. * @param {*} v
  363. * @return {number}
  364. * @private
  365. */
  366. StatsCollector.prototype.getNonNegativeValue = function(v) {
  367. let value = v;
  368. if (typeof value !== 'number') {
  369. value = Number(value);
  370. }
  371. if (isNaN(value)) {
  372. return 0;
  373. }
  374. return Math.max(0, value);
  375. };
  376. /**
  377. * Calculates bitrate between before and now using a supplied field name and its
  378. * value in the stats.
  379. * @param {RTCInboundRtpStreamStats|RTCSentRtpStreamStats} now the current stats
  380. * @param {RTCInboundRtpStreamStats|RTCSentRtpStreamStats} before the
  381. * previous stats.
  382. * @param fieldName the field to use for calculations.
  383. * @return {number} the calculated bitrate between now and before.
  384. * @private
  385. */
  386. StatsCollector.prototype._calculateBitrate = function(now, before, fieldName) {
  387. const bytesNow = this.getNonNegativeValue(now[fieldName]);
  388. const bytesBefore = this.getNonNegativeValue(before[fieldName]);
  389. const bytesProcessed = Math.max(0, bytesNow - bytesBefore);
  390. const timeMs = now.timestamp - before.timestamp;
  391. let bitrateKbps = 0;
  392. if (timeMs > 0) {
  393. // TODO is there any reason to round here?
  394. bitrateKbps = Math.round((bytesProcessed * 8) / timeMs);
  395. }
  396. return bitrateKbps;
  397. };
  398. /**
  399. * Calculates the frames per second rate between before and now using a supplied field name and its value in stats.
  400. * @param {RTCOutboundRtpStreamStats|RTCSentRtpStreamStats} now the current stats
  401. * @param {RTCOutboundRtpStreamStats|RTCSentRtpStreamStats} before the previous stats
  402. * @param {string} fieldName the field to use for calculations.
  403. * @returns {number} the calculated frame rate between now and before.
  404. */
  405. StatsCollector.prototype._calculateFps = function(now, before, fieldName) {
  406. const timeMs = now.timestamp - before.timestamp;
  407. let frameRate = 0;
  408. if (timeMs > 0 && now[fieldName]) {
  409. const numberOfFramesSinceBefore = now[fieldName] - before[fieldName];
  410. frameRate = (numberOfFramesSinceBefore / timeMs) * 1000;
  411. }
  412. return frameRate;
  413. };
  414. /**
  415. * Stats processing for spec-compliant RTCPeerConnection#getStats.
  416. */
  417. StatsCollector.prototype.processStatsReport = function() {
  418. const byteSentStats = {};
  419. this.currentStatsReport.forEach(now => {
  420. const before = this.previousStatsReport ? this.previousStatsReport.get(now.id) : null;
  421. // RTCIceCandidatePairStats - https://w3c.github.io/webrtc-stats/#candidatepair-dict*
  422. if (now.type === 'candidate-pair' && now.nominated && now.state === 'succeeded') {
  423. const availableIncomingBitrate = now.availableIncomingBitrate;
  424. const availableOutgoingBitrate = now.availableOutgoingBitrate;
  425. if (availableIncomingBitrate || availableOutgoingBitrate) {
  426. this.conferenceStats.bandwidth = {
  427. 'download': Math.round(availableIncomingBitrate / 1000),
  428. 'upload': Math.round(availableOutgoingBitrate / 1000)
  429. };
  430. }
  431. const remoteUsedCandidate = this.currentStatsReport.get(now.remoteCandidateId);
  432. const localUsedCandidate = this.currentStatsReport.get(now.localCandidateId);
  433. // RTCIceCandidateStats
  434. // https://w3c.github.io/webrtc-stats/#icecandidate-dict*
  435. if (remoteUsedCandidate && localUsedCandidate) {
  436. const remoteIpAddress = browser.isChromiumBased()
  437. ? remoteUsedCandidate.ip
  438. : remoteUsedCandidate.address;
  439. const remotePort = remoteUsedCandidate.port;
  440. const ip = `${remoteIpAddress}:${remotePort}`;
  441. const localIpAddress = browser.isChromiumBased()
  442. ? localUsedCandidate.ip
  443. : localUsedCandidate.address;
  444. const localPort = localUsedCandidate.port;
  445. const localip = `${localIpAddress}:${localPort}`;
  446. const type = remoteUsedCandidate.protocol;
  447. // Save the address unless it has been saved already.
  448. const conferenceStatsTransport = this.conferenceStats.transport;
  449. if (!conferenceStatsTransport.some(t =>
  450. t.ip === ip
  451. && t.type === type
  452. && t.localip === localip)) {
  453. conferenceStatsTransport.push({
  454. ip,
  455. type,
  456. localip,
  457. p2p: this.peerconnection.isP2P,
  458. localCandidateType: localUsedCandidate.candidateType,
  459. remoteCandidateType: remoteUsedCandidate.candidateType,
  460. networkType: localUsedCandidate.networkType,
  461. rtt: now.currentRoundTripTime * 1000
  462. });
  463. }
  464. }
  465. // RTCReceivedRtpStreamStats
  466. // https://w3c.github.io/webrtc-stats/#receivedrtpstats-dict*
  467. // RTCSentRtpStreamStats
  468. // https://w3c.github.io/webrtc-stats/#sentrtpstats-dict*
  469. } else if (now.type === 'inbound-rtp' || now.type === 'outbound-rtp') {
  470. const ssrc = this.getNonNegativeValue(now.ssrc);
  471. if (!ssrc) {
  472. return;
  473. }
  474. let ssrcStats = this.ssrc2stats.get(ssrc);
  475. if (!ssrcStats) {
  476. ssrcStats = new SsrcStats();
  477. this.ssrc2stats.set(ssrc, ssrcStats);
  478. }
  479. let isDownloadStream = true;
  480. let key = 'packetsReceived';
  481. if (now.type === 'outbound-rtp') {
  482. isDownloadStream = false;
  483. key = 'packetsSent';
  484. }
  485. let packetsNow = now[key];
  486. if (!packetsNow || packetsNow < 0) {
  487. packetsNow = 0;
  488. }
  489. if (before) {
  490. const packetsBefore = this.getNonNegativeValue(before[key]);
  491. const packetsDiff = Math.max(0, packetsNow - packetsBefore);
  492. const packetsLostNow = this.getNonNegativeValue(now.packetsLost);
  493. const packetsLostBefore = this.getNonNegativeValue(before.packetsLost);
  494. const packetsLostDiff = Math.max(0, packetsLostNow - packetsLostBefore);
  495. ssrcStats.setLoss({
  496. packetsTotal: packetsDiff + packetsLostDiff,
  497. packetsLost: packetsLostDiff,
  498. isDownloadStream
  499. });
  500. }
  501. let resolution;
  502. // Process the stats for 'inbound-rtp' streams always and 'outbound-rtp' only if the browser is
  503. // Chromium based and version 112 and later since 'track' based stats are no longer available there
  504. // for calculating send resolution and frame rate.
  505. if (typeof now.frameHeight !== 'undefined' && typeof now.frameWidth !== 'undefined') {
  506. // Assume the stream is active if the field is missing in the stats(Firefox)
  507. const isStreamActive = now.active ?? true;
  508. if (now.type === 'inbound-rtp' || (!browser.supportsTrackBasedStats() && isStreamActive)) {
  509. resolution = {
  510. height: now.frameHeight,
  511. width: now.frameWidth
  512. };
  513. }
  514. }
  515. ssrcStats.setResolution(resolution);
  516. let frameRate = now.framesPerSecond;
  517. if (!frameRate && before) {
  518. frameRate = this._calculateFps(now, before, 'framesSent');
  519. }
  520. ssrcStats.setFramerate(Math.round(frameRate || 0));
  521. if (now.type === 'inbound-rtp' && before) {
  522. ssrcStats.addBitrate({
  523. 'download': this._calculateBitrate(now, before, 'bytesReceived'),
  524. 'upload': 0
  525. });
  526. } else if (before) {
  527. byteSentStats[ssrc] = this.getNonNegativeValue(now.bytesSent);
  528. ssrcStats.addBitrate({
  529. 'download': 0,
  530. 'upload': this._calculateBitrate(now, before, 'bytesSent')
  531. });
  532. }
  533. const codec = this.currentStatsReport.get(now.codecId);
  534. if (codec) {
  535. /**
  536. * The mime type has the following form: video/VP8 or audio/ISAC,
  537. * so we what to keep just the type after the '/', audio and video
  538. * keys will be added on the processing side.
  539. */
  540. const codecShortType = codec.mimeType.split('/')[1];
  541. codecShortType && ssrcStats.setCodec(codecShortType);
  542. }
  543. // Continue to use the 'track' based stats for Firefox and Safari and older versions of Chromium.
  544. } else if (browser.supportsTrackBasedStats()
  545. && now.type === 'track'
  546. && now.kind === MediaType.VIDEO
  547. && !now.remoteSource) {
  548. const resolution = {
  549. height: now.frameHeight,
  550. width: now.frameWidth
  551. };
  552. const localVideoTracks = this.peerconnection.getLocalTracks(MediaType.VIDEO);
  553. if (!localVideoTracks?.length) {
  554. return;
  555. }
  556. const ssrc = this.peerconnection.getSsrcByTrackId(now.trackIdentifier);
  557. if (!ssrc) {
  558. return;
  559. }
  560. let ssrcStats = this.ssrc2stats.get(ssrc);
  561. if (!ssrcStats) {
  562. ssrcStats = new SsrcStats();
  563. this.ssrc2stats.set(ssrc, ssrcStats);
  564. }
  565. if (resolution.height && resolution.width) {
  566. ssrcStats.setResolution(resolution);
  567. }
  568. // Calculate the frame rate. 'framesSent' is the total aggregate value for all the simulcast streams.
  569. // Therefore, it needs to be divided by the total number of active simulcast streams.
  570. let frameRate = now.framesPerSecond;
  571. if (!frameRate && before) {
  572. frameRate = this._calculateFps(now, before, 'framesSent');
  573. }
  574. ssrcStats.setFramerate(frameRate);
  575. }
  576. });
  577. if (Object.keys(byteSentStats).length) {
  578. this.eventEmitter.emit(StatisticsEvents.BYTE_SENT_STATS, this.peerconnection, byteSentStats);
  579. }
  580. this._processAndEmitReport();
  581. };