|
@@ -275,61 +275,86 @@ StatsCollector.prototype._processAndEmitReport = function() {
|
275
|
275
|
bitrateDownload += ssrcStats.bitrate.download;
|
276
|
276
|
bitrateUpload += ssrcStats.bitrate.upload;
|
277
|
277
|
|
|
278
|
+ ssrcStats.resetBitrate();
|
|
279
|
+
|
278
|
280
|
// collect resolutions and framerates
|
279
|
281
|
const track = this.peerconnection.getTrackBySSRC(ssrc);
|
280
|
282
|
|
281
|
|
- if (track) {
|
282
|
|
- let audioCodec;
|
283
|
|
- let videoCodec;
|
284
|
|
-
|
285
|
|
- if (track.isAudioTrack()) {
|
286
|
|
- audioBitrateDownload += ssrcStats.bitrate.download;
|
287
|
|
- audioBitrateUpload += ssrcStats.bitrate.upload;
|
288
|
|
- audioCodec = ssrcStats.codec;
|
289
|
|
- } else {
|
290
|
|
- videoBitrateDownload += ssrcStats.bitrate.download;
|
291
|
|
- videoBitrateUpload += ssrcStats.bitrate.upload;
|
292
|
|
- videoCodec = ssrcStats.codec;
|
293
|
|
- }
|
294
|
|
-
|
295
|
|
- const participantId = track.getParticipantId();
|
296
|
|
-
|
297
|
|
- if (participantId) {
|
298
|
|
- const resolution = ssrcStats.resolution;
|
|
283
|
+ if (!track) {
|
|
284
|
+ continue; // eslint-disable-line no-continue
|
|
285
|
+ }
|
299
|
286
|
|
300
|
|
- if (resolution.width
|
301
|
|
- && resolution.height
|
302
|
|
- && resolution.width !== -1
|
303
|
|
- && resolution.height !== -1) {
|
304
|
|
- const userResolutions = resolutions[participantId] || {};
|
|
287
|
+ let audioCodec;
|
|
288
|
+ let videoCodec;
|
305
|
289
|
|
306
|
|
- userResolutions[ssrc] = resolution;
|
307
|
|
- resolutions[participantId] = userResolutions;
|
308
|
|
- }
|
|
290
|
+ if (track.isAudioTrack()) {
|
|
291
|
+ audioBitrateDownload += ssrcStats.bitrate.download;
|
|
292
|
+ audioBitrateUpload += ssrcStats.bitrate.upload;
|
|
293
|
+ audioCodec = ssrcStats.codec;
|
|
294
|
+ } else {
|
|
295
|
+ videoBitrateDownload += ssrcStats.bitrate.download;
|
|
296
|
+ videoBitrateUpload += ssrcStats.bitrate.upload;
|
|
297
|
+ videoCodec = ssrcStats.codec;
|
|
298
|
+ }
|
309
|
299
|
|
310
|
|
- if (ssrcStats.framerate > 0) {
|
311
|
|
- const userFramerates = framerates[participantId] || {};
|
|
300
|
+ const participantId = track.getParticipantId();
|
312
|
301
|
|
313
|
|
- userFramerates[ssrc] = ssrcStats.framerate;
|
314
|
|
- framerates[participantId] = userFramerates;
|
315
|
|
- }
|
|
302
|
+ if (!participantId) {
|
|
303
|
+ // All tracks in ssrc-rewriting mode need not have a participant associated with it.
|
|
304
|
+ if (!FeatureFlags.isSsrcRewritingSupported()) {
|
|
305
|
+ logger.error(`No participant ID returned by ${track}`);
|
|
306
|
+ }
|
|
307
|
+ continue; // eslint-disable-line no-continue
|
|
308
|
+ }
|
316
|
309
|
|
317
|
|
- const userCodecs = codecs[participantId] ?? { };
|
|
310
|
+ const userCodecs = codecs[participantId] ?? { };
|
318
|
311
|
|
319
|
|
- userCodecs[ssrc] = {
|
320
|
|
- audio: audioCodec,
|
321
|
|
- video: videoCodec
|
322
|
|
- };
|
|
312
|
+ userCodecs[ssrc] = {
|
|
313
|
+ audio: audioCodec,
|
|
314
|
+ video: videoCodec
|
|
315
|
+ };
|
323
|
316
|
|
324
|
|
- codecs[participantId] = userCodecs;
|
|
317
|
+ codecs[participantId] = userCodecs;
|
|
318
|
+ const { resolution } = ssrcStats;
|
325
|
319
|
|
326
|
|
- // All tracks in ssrc-rewriting mode need not have a participant associated with it.
|
327
|
|
- } else if (!FeatureFlags.isSsrcRewritingSupported()) {
|
328
|
|
- logger.error(`No participant ID returned by ${track}`);
|
|
320
|
+ if (!track.isVideoTrack()
|
|
321
|
+ || isNaN(resolution?.height)
|
|
322
|
+ || isNaN(resolution?.width)
|
|
323
|
+ || resolution.height === -1
|
|
324
|
+ || resolution.width === -1) {
|
|
325
|
+ continue; // eslint-disable-line no-continue
|
|
326
|
+ }
|
|
327
|
+ const userResolutions = resolutions[participantId] || {};
|
|
328
|
+
|
|
329
|
+ // If simulcast (VP8) is used, there will be 3 "outbound-rtp" streams with different resolutions and 3
|
|
330
|
+ // different SSRCs. Based on the requested resolution and the current cpu and available bandwidth
|
|
331
|
+ // values, some of the streams might get suspended. Therefore the actual send resolution needs to be
|
|
332
|
+ // calculated based on the outbound-rtp streams that are currently active for the simulcast case.
|
|
333
|
+ // However for the SVC case, there will be only 1 "outbound-rtp" stream which will have the correct
|
|
334
|
+ // send resolution width and height.
|
|
335
|
+ if (track.isLocal() && !browser.supportsTrackBasedStats() && this.peerconnection.doesTrueSimulcast()) {
|
|
336
|
+ const localSsrcs = this.peerconnection.getLocalVideoSSRCs(track);
|
|
337
|
+
|
|
338
|
+ for (const localSsrc of localSsrcs) {
|
|
339
|
+ const ssrcResolution = this.ssrc2stats.get(localSsrc)?.resolution;
|
|
340
|
+
|
|
341
|
+ // The code processes resolution stats only for 'outbound-rtp' streams that are currently active.
|
|
342
|
+ if (ssrcResolution?.height && ssrcResolution?.width) {
|
|
343
|
+ resolution.height = Math.max(resolution.height, ssrcResolution.height);
|
|
344
|
+ resolution.width = Math.max(resolution.width, ssrcResolution.width);
|
|
345
|
+ }
|
329
|
346
|
}
|
330
|
347
|
}
|
331
|
348
|
|
332
|
|
- ssrcStats.resetBitrate();
|
|
349
|
+ userResolutions[ssrc] = resolution;
|
|
350
|
+ resolutions[participantId] = userResolutions;
|
|
351
|
+
|
|
352
|
+ if (ssrcStats.framerate > 0) {
|
|
353
|
+ const userFramerates = framerates[participantId] || {};
|
|
354
|
+
|
|
355
|
+ userFramerates[ssrc] = ssrcStats.framerate;
|
|
356
|
+ framerates[participantId] = userFramerates;
|
|
357
|
+ }
|
333
|
358
|
}
|
334
|
359
|
|
335
|
360
|
this.conferenceStats.bitrate = {
|
|
@@ -445,6 +470,26 @@ StatsCollector.prototype._calculateBitrate = function(now, before, fieldName) {
|
445
|
470
|
return bitrateKbps;
|
446
|
471
|
};
|
447
|
472
|
|
|
473
|
+/**
|
|
474
|
+ * Calculates the frames per second rate between before and now using a supplied field name and its value in stats.
|
|
475
|
+ * @param {RTCOutboundRtpStreamStats|RTCSentRtpStreamStats} now the current stats
|
|
476
|
+ * @param {RTCOutboundRtpStreamStats|RTCSentRtpStreamStats} before the previous stats
|
|
477
|
+ * @param {string} fieldName the field to use for calculations.
|
|
478
|
+ * @returns {number} the calculated frame rate between now and before.
|
|
479
|
+ */
|
|
480
|
+StatsCollector.prototype._calculateFps = function(now, before, fieldName) {
|
|
481
|
+ const timeMs = now.timestamp - before.timestamp;
|
|
482
|
+ let frameRate = 0;
|
|
483
|
+
|
|
484
|
+ if (timeMs > 0 && now[fieldName]) {
|
|
485
|
+ const numberOfFramesSinceBefore = now[fieldName] - before[fieldName];
|
|
486
|
+
|
|
487
|
+ frameRate = (numberOfFramesSinceBefore / timeMs) * 1000;
|
|
488
|
+ }
|
|
489
|
+
|
|
490
|
+ return frameRate;
|
|
491
|
+};
|
|
492
|
+
|
448
|
493
|
/**
|
449
|
494
|
* Stats processing for spec-compliant RTCPeerConnection#getStats.
|
450
|
495
|
*/
|
|
@@ -552,29 +597,37 @@ StatsCollector.prototype.processStatsReport = function() {
|
552
|
597
|
});
|
553
|
598
|
}
|
554
|
599
|
|
555
|
|
- // Get the resolution and framerate for only remote video sources here. For the local video sources,
|
556
|
|
- // 'track' stats will be used since they have the updated resolution based on the simulcast streams
|
557
|
|
- // currently being sent. Promise based getStats reports three 'outbound-rtp' streams and there will be
|
558
|
|
- // more calculations needed to determine what is the highest resolution stream sent by the client if the
|
559
|
|
- // 'outbound-rtp' stats are used.
|
560
|
|
- if (now.type === 'inbound-rtp') {
|
561
|
|
- const resolution = {
|
562
|
|
- height: now.frameHeight,
|
563
|
|
- width: now.frameWidth
|
564
|
|
- };
|
565
|
|
- const frameRate = now.framesPerSecond;
|
|
600
|
+ let resolution;
|
566
|
601
|
|
567
|
|
- if (resolution.height && resolution.width) {
|
568
|
|
- ssrcStats.setResolution(resolution);
|
569
|
|
- }
|
570
|
|
- ssrcStats.setFramerate(Math.round(frameRate || 0));
|
|
602
|
+ // Process the stats for 'inbound-rtp' streams always and 'outbound-rtp' only if the browser is
|
|
603
|
+ // Chromium based and version 112 and later since 'track' based stats are no longer available there
|
|
604
|
+ // for calculating send resolution and frame rate.
|
|
605
|
+ if (typeof now.frameHeight !== 'undefined' && typeof now.frameWidth !== 'undefined') {
|
|
606
|
+ // Assume the stream is active if the field is missing in the stats(Firefox)
|
|
607
|
+ const isStreamActive = now.active ?? true;
|
571
|
608
|
|
572
|
|
- if (before) {
|
573
|
|
- ssrcStats.addBitrate({
|
574
|
|
- 'download': this._calculateBitrate(now, before, 'bytesReceived'),
|
575
|
|
- 'upload': 0
|
576
|
|
- });
|
|
609
|
+ if (now.type === 'inbound-rtp' || (!browser.supportsTrackBasedStats() && isStreamActive)) {
|
|
610
|
+ resolution = {
|
|
611
|
+ height: now.frameHeight,
|
|
612
|
+ width: now.frameWidth
|
|
613
|
+ };
|
577
|
614
|
}
|
|
615
|
+ }
|
|
616
|
+ ssrcStats.setResolution(resolution);
|
|
617
|
+
|
|
618
|
+ let frameRate = now.framesPerSecond;
|
|
619
|
+
|
|
620
|
+ if (!frameRate && before) {
|
|
621
|
+ frameRate = this._calculateFps(now, before, 'framesSent');
|
|
622
|
+ }
|
|
623
|
+
|
|
624
|
+ ssrcStats.setFramerate(Math.round(frameRate || 0));
|
|
625
|
+
|
|
626
|
+ if (now.type === 'inbound-rtp' && before) {
|
|
627
|
+ ssrcStats.addBitrate({
|
|
628
|
+ 'download': this._calculateBitrate(now, before, 'bytesReceived'),
|
|
629
|
+ 'upload': 0
|
|
630
|
+ });
|
578
|
631
|
} else if (before) {
|
579
|
632
|
byteSentStats[ssrc] = this.getNonNegativeValue(now.bytesSent);
|
580
|
633
|
ssrcStats.addBitrate({
|
|
@@ -596,10 +649,11 @@ StatsCollector.prototype.processStatsReport = function() {
|
596
|
649
|
codecShortType && ssrcStats.setCodec(codecShortType);
|
597
|
650
|
}
|
598
|
651
|
|
599
|
|
- // Use track stats for resolution and framerate of the local video source.
|
600
|
|
- // RTCVideoHandlerStats - https://w3c.github.io/webrtc-stats/#vststats-dict*
|
601
|
|
- // RTCMediaHandlerStats - https://w3c.github.io/webrtc-stats/#mststats-dict*
|
602
|
|
- } else if (now.type === 'track' && now.kind === MediaType.VIDEO && !now.remoteSource) {
|
|
652
|
+ // Continue to use the 'track' based stats for Firefox and Safari and older versions of Chromium.
|
|
653
|
+ } else if (browser.supportsTrackBasedStats()
|
|
654
|
+ && now.type === 'track'
|
|
655
|
+ && now.kind === MediaType.VIDEO
|
|
656
|
+ && !now.remoteSource) {
|
603
|
657
|
const resolution = {
|
604
|
658
|
height: now.frameHeight,
|
605
|
659
|
width: now.frameWidth
|
|
@@ -629,27 +683,9 @@ StatsCollector.prototype.processStatsReport = function() {
|
629
|
683
|
// Therefore, it needs to be divided by the total number of active simulcast streams.
|
630
|
684
|
let frameRate = now.framesPerSecond;
|
631
|
685
|
|
632
|
|
- if (!frameRate) {
|
633
|
|
- if (before) {
|
634
|
|
- const timeMs = now.timestamp - before.timestamp;
|
635
|
|
-
|
636
|
|
- if (timeMs > 0 && now.framesSent) {
|
637
|
|
- const numberOfFramesSinceBefore = now.framesSent - before.framesSent;
|
638
|
|
-
|
639
|
|
- frameRate = (numberOfFramesSinceBefore / timeMs) * 1000;
|
640
|
|
- }
|
641
|
|
- }
|
642
|
|
-
|
643
|
|
- if (!frameRate) {
|
644
|
|
- return;
|
645
|
|
- }
|
|
686
|
+ if (!frameRate && before) {
|
|
687
|
+ frameRate = this._calculateFps(now, before, 'framesSent');
|
646
|
688
|
}
|
647
|
|
-
|
648
|
|
- // Get the number of simulcast streams currently enabled from TPC.
|
649
|
|
- const numberOfActiveStreams = this.peerconnection.getActiveSimulcastStreams();
|
650
|
|
-
|
651
|
|
- // Reset frame rate to 0 when video is suspended as a result of endpoint falling out of last-n.
|
652
|
|
- frameRate = numberOfActiveStreams ? Math.round(frameRate / numberOfActiveStreams) : 0;
|
653
|
689
|
ssrcStats.setFramerate(frameRate);
|
654
|
690
|
}
|
655
|
691
|
});
|