Quellcode durchsuchen

Implements the promised based getStats. Enables them for Safari and FF.

Adds stats and audio levels for Safari. Enables the new getStats API for Firefox, that will get rid of the following warning:
'non-maplike pc.getStats access is deprecated, and will be removed in the near future! See http://w3c.github.io/webrtc-pc/#getstats-example for usage.'
dev1
damencho vor 7 Jahren
Ursprung
Commit
e3adbca608

+ 33
- 7
modules/RTC/TraceablePeerConnection.js Datei anzeigen

@@ -531,6 +531,28 @@ TraceablePeerConnection.prototype.getTrackBySSRC = function(ssrc) {
531 531
     return null;
532 532
 };
533 533
 
534
+/**
535
+ * Tries to find SSRC number for given {@link JitsiTrack} id. It will search
536
+ * both local and remote tracks bound to this instance.
537
+ * @param {string} id
538
+ * @return {number|null}
539
+ */
540
+TraceablePeerConnection.prototype.getSsrcByTrackId = function(id) {
541
+
542
+    for (const localTrack of this.localTracks.values()) {
543
+        if (localTrack.getTrack().id === id) {
544
+            return this.getLocalSSRC(localTrack);
545
+        }
546
+    }
547
+    for (const remoteTrack of this.getRemoteTracks()) {
548
+        if (remoteTrack.getTrack().id === id) {
549
+            return remoteTrack.getSSRC();
550
+        }
551
+    }
552
+
553
+    return null;
554
+};
555
+
534 556
 /**
535 557
  * Called when new remote MediaStream is added to the PeerConnection.
536 558
  * @param {MediaStream} stream the WebRTC MediaStream for remote participant
@@ -2303,9 +2325,8 @@ TraceablePeerConnection.prototype.getStats = function(callback, errback) {
2303 2325
     // TODO (brian): After moving all browsers to adapter, check if adapter is
2304 2326
     // accounting for different getStats apis, making the browser-checking-if
2305 2327
     // unnecessary.
2306
-    if (browser.isFirefox()
2307
-            || browser.isTemasysPluginUsed()
2308
-            || browser.isReactNative()) {
2328
+    if (browser.isTemasysPluginUsed()
2329
+        || browser.isReactNative()) {
2309 2330
         this.peerconnection.getStats(
2310 2331
             null,
2311 2332
             callback,
@@ -2314,10 +2335,15 @@ TraceablePeerConnection.prototype.getStats = function(callback, errback) {
2314 2335
                 // Making sure that getStats won't fail if error callback is
2315 2336
                 // not passed.
2316 2337
             }));
2317
-    } else if (browser.isSafariWithWebrtc()) {
2318
-        // FIXME: Safari's native stats implementation is not compatibile with
2319
-        // existing stats processing logic. Skip implementing stats for now to
2320
-        // at least get native webrtc Safari available for use.
2338
+    } else if (browser.isSafariWithWebrtc() || browser.isFirefox()) {
2339
+        // uses the new Promise based getStats
2340
+        this.peerconnection.getStats()
2341
+            .then(callback)
2342
+            .catch(errback || (() => {
2343
+
2344
+                // Making sure that getStats won't fail if error callback is
2345
+                // not passed.
2346
+            }));
2321 2347
     } else {
2322 2348
         this.peerconnection.getStats(callback);
2323 2349
     }

+ 2
- 1
modules/browser/BrowserCapabilities.js Datei anzeigen

@@ -91,7 +91,8 @@ export default class BrowserCapabilities extends BrowserDetection {
91 91
     supportsBandwidthStatistics() {
92 92
         // FIXME bandwidth stats are currently not implemented for FF on our
93 93
         // side, but not sure if not possible ?
94
-        return !this.isFirefox() && !this.isEdge();
94
+        return !this.isFirefox() && !this.isEdge()
95
+            && !this.isSafariWithWebrtc();
95 96
     }
96 97
 
97 98
     /**

+ 374
- 8
modules/statistics/RTPStatsCollector.js Datei anzeigen

@@ -2,6 +2,7 @@ import browser from '../browser';
2 2
 import { browsers } from 'js-utils';
3 3
 
4 4
 import * as StatisticsEvents from '../../service/statistics/Events';
5
+import * as MediaType from '../../service/RTC/MediaType';
5 6
 
6 7
 const GlobalOnErrorHandler = require('../util/GlobalOnErrorHandler');
7 8
 const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -10,7 +11,8 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
10 11
 const browserSupported = browser.isChrome()
11 12
         || browser.isOpera() || browser.isFirefox()
12 13
         || browser.isNWJS() || browser.isElectron()
13
-        || browser.isTemasysPluginUsed() || browser.isEdge();
14
+        || browser.isTemasysPluginUsed() || browser.isEdge()
15
+        || browser.isSafariWithWebrtc();
14 16
 
15 17
 /**
16 18
  * The lib-jitsi-meet browser-agnostic names of the browser-specific keys
@@ -25,7 +27,10 @@ KEYS_BY_BROWSER_TYPE[browsers.FIREFOX] = {
25 27
     'packetsSent': 'packetsSent',
26 28
     'bytesReceived': 'bytesReceived',
27 29
     'bytesSent': 'bytesSent',
28
-    'framerateMean': 'framerateMean'
30
+    'framerateMean': 'framerateMean',
31
+    'ip': 'ipAddress',
32
+    'port': 'portNumber',
33
+    'protocol': 'transport'
29 34
 };
30 35
 KEYS_BY_BROWSER_TYPE[browsers.CHROME] = {
31 36
     'receiveBandwidth': 'googAvailableReceiveBandwidth',
@@ -50,7 +55,10 @@ KEYS_BY_BROWSER_TYPE[browsers.CHROME] = {
50 55
     'audioOutputLevel': 'audioOutputLevel',
51 56
     'currentRoundTripTime': 'googRtt',
52 57
     'remoteCandidateType': 'googRemoteCandidateType',
53
-    'localCandidateType': 'googLocalCandidateType'
58
+    'localCandidateType': 'googLocalCandidateType',
59
+    'ip': 'ip',
60
+    'port': 'port',
61
+    'protocol': 'protocol'
54 62
 };
55 63
 KEYS_BY_BROWSER_TYPE[browsers.EDGE] = {
56 64
     'sendBandwidth': 'googAvailableSendBandwidth',
@@ -242,6 +250,7 @@ export default function StatsCollector(
242 250
      * @private
243 251
      */
244 252
     this._getStatValue = this._defineGetStatValueMethod(keys);
253
+    this._getNewStatValue = this._defineNewGetStatValueMethod(keys);
245 254
 
246 255
     this.peerconnection = peerconnection;
247 256
     this.baselineAudioLevelsReport = null;
@@ -313,7 +322,13 @@ StatsCollector.prototype.start = function(startAudioLevelStats) {
313 322
                             results = report.result();
314 323
                         }
315 324
                         self.currentAudioLevelsReport = results;
316
-                        self.processAudioLevelReport();
325
+                        if (browser.isSafariWithWebrtc()
326
+                            || browser.isFirefox()) {
327
+                            self.processNewAudioLevelReport();
328
+                        } else {
329
+                            self.processAudioLevelReport();
330
+                        }
331
+
317 332
                         self.baselineAudioLevelsReport
318 333
                             = self.currentAudioLevelsReport;
319 334
                     },
@@ -343,7 +358,12 @@ StatsCollector.prototype.start = function(startAudioLevelStats) {
343 358
 
344 359
                         self.currentStatsReport = results;
345 360
                         try {
346
-                            self.processStatsReport();
361
+                            if (browser.isSafariWithWebrtc()
362
+                                || browser.isFirefox()) {
363
+                                self.processNewStatsReport();
364
+                            } else {
365
+                                self.processStatsReport();
366
+                            }
347 367
                         } catch (e) {
348 368
                             GlobalOnErrorHandler.callErrorHandler(e);
349 369
                             logger.error(`Unsupported key:${e}`, e);
@@ -722,6 +742,16 @@ StatsCollector.prototype.processStatsReport = function() {
722 742
         }
723 743
     }
724 744
 
745
+    this.eventEmitter.emit(
746
+        StatisticsEvents.BYTE_SENT_STATS, this.peerconnection, byteSentStats);
747
+
748
+    this._processAndEmitReport();
749
+};
750
+
751
+/**
752
+ *
753
+ */
754
+StatsCollector.prototype._processAndEmitReport = function() {
725 755
     // process stats
726 756
     const totalPackets = {
727 757
         download: 0,
@@ -804,9 +834,6 @@ StatsCollector.prototype.processStatsReport = function() {
804 834
         ssrcStats.resetBitrate();
805 835
     }
806 836
 
807
-    this.eventEmitter.emit(
808
-        StatisticsEvents.BYTE_SENT_STATS, this.peerconnection, byteSentStats);
809
-
810 837
     this.conferenceStats.bitrate = {
811 838
         'upload': bitrateUpload,
812 839
         'download': bitrateDownload
@@ -940,3 +967,342 @@ StatsCollector.prototype.processAudioLevelReport = function() {
940 967
 };
941 968
 
942 969
 /* eslint-enable no-continue */
970
+
971
+/**
972
+ * New promised based getStats report processing.
973
+ * Tested with chrome, firefox and safari. Not switching it on for for chrome as
974
+ * frameRate stat is missing and calculating it using framesSent,
975
+ * gives values double the values seen in webrtc-internals.
976
+ * https://w3c.github.io/webrtc-stats/
977
+ */
978
+
979
+/**
980
+ * Defines a function which (1) is to be used as a StatsCollector method and (2)
981
+ * gets the value from a specific report returned by RTCPeerConnection#getStats
982
+ * associated with a lib-jitsi-meet browser-agnostic name in case of using
983
+ * Promised based getStats.
984
+ *
985
+ * @param {Object.<string,string>} keys the map of LibJitsi browser-agnostic
986
+ * names to RTCPeerConnection#getStats browser-specific keys
987
+ */
988
+StatsCollector.prototype._defineNewGetStatValueMethod = function(keys) {
989
+    // Define the function which converts a lib-jitsi-meet browser-asnostic name
990
+    // to a browser-specific key of a report returned by
991
+    // RTCPeerConnection#getStats.
992
+    const keyFromName = function(name) {
993
+        const key = keys[name];
994
+
995
+        if (key) {
996
+            return key;
997
+        }
998
+
999
+        // eslint-disable-next-line no-throw-literal
1000
+        throw `The property '${name}' isn't supported!`;
1001
+    };
1002
+
1003
+    // Compose the 2 functions defined above to get a function which retrieves
1004
+    // the value from a specific report returned by RTCPeerConnection#getStats
1005
+    // associated with a specific lib-jitsi-meet browser-agnostic name.
1006
+    return (item, name) => item[keyFromName(name)];
1007
+};
1008
+
1009
+/**
1010
+ * Converts the value to a non-negative number.
1011
+ * If the value is either invalid or negative then 0 will be returned.
1012
+ * @param {*} v
1013
+ * @return {number}
1014
+ * @private
1015
+ */
1016
+StatsCollector.prototype.getNonNegativeValue = function(v) {
1017
+    let value = v;
1018
+
1019
+    if (typeof value !== 'number') {
1020
+        value = Number(value);
1021
+    }
1022
+
1023
+    if (isNaN(value)) {
1024
+        return 0;
1025
+    }
1026
+
1027
+    return Math.max(0, value);
1028
+};
1029
+
1030
+/**
1031
+ * Calculates bitrate between before and now using a supplied field name and its
1032
+ * value in the stats.
1033
+ * @param {RTCInboundRtpStreamStats|RTCSentRtpStreamStats} now the current stats
1034
+ * @param {RTCInboundRtpStreamStats|RTCSentRtpStreamStats} before the
1035
+ * previous stats.
1036
+ * @param fieldName the field to use for calculations.
1037
+ * @return {number} the calculated bitrate between now and before.
1038
+ * @private
1039
+ */
1040
+StatsCollector.prototype._calculateBitrate = function(now, before, fieldName) {
1041
+    const bytesNow = this.getNonNegativeValue(now[fieldName]);
1042
+    const bytesBefore = this.getNonNegativeValue(before[fieldName]);
1043
+    const bytesProcessed = Math.max(0, bytesNow - bytesBefore);
1044
+
1045
+    const timeMs = now.timestamp - before.timestamp;
1046
+    let bitrateKbps = 0;
1047
+
1048
+    if (timeMs > 0) {
1049
+        // TODO is there any reason to round here?
1050
+        bitrateKbps = Math.round((bytesProcessed * 8) / timeMs);
1051
+    }
1052
+
1053
+    return bitrateKbps;
1054
+};
1055
+
1056
+/**
1057
+ * Stats processing new getStats logic.
1058
+ */
1059
+StatsCollector.prototype.processNewStatsReport = function() {
1060
+    if (!this.previousStatsReport) {
1061
+        return;
1062
+    }
1063
+
1064
+    const getStatValue = this._getNewStatValue;
1065
+    const byteSentStats = {};
1066
+
1067
+    this.currentStatsReport.forEach(now => {
1068
+
1069
+        // RTCIceCandidatePairStats
1070
+        // https://w3c.github.io/webrtc-stats/#candidatepair-dict*
1071
+        if (now.type === 'candidate-pair'
1072
+            && now.nominated
1073
+            && now.state === 'succeeded') {
1074
+
1075
+            const availableIncomingBitrate = now.availableIncomingBitrate;
1076
+            const availableOutgoingBitrate = now.availableOutgoingBitrate;
1077
+
1078
+            if (availableIncomingBitrate || availableOutgoingBitrate) {
1079
+                this.conferenceStats.bandwidth = {
1080
+                    'download': Math.round(availableIncomingBitrate / 1000),
1081
+                    'upload': Math.round(availableOutgoingBitrate / 1000)
1082
+                };
1083
+            }
1084
+
1085
+            const remoteUsedCandidate
1086
+                = this.currentStatsReport.get(now.remoteCandidateId);
1087
+            const localUsedCandidate
1088
+                = this.currentStatsReport.get(now.localCandidateId);
1089
+
1090
+            // RTCIceCandidateStats
1091
+            // https://w3c.github.io/webrtc-stats/#icecandidate-dict*
1092
+            // safari currently does not provide ice candidates in stats
1093
+            if (remoteUsedCandidate && localUsedCandidate) {
1094
+                // FF uses non-standard ipAddress, portNumber, transport
1095
+                // instead of ip, port, protocol
1096
+                const remoteIpAddress = getStatValue(remoteUsedCandidate, 'ip');
1097
+                const remotePort = getStatValue(remoteUsedCandidate, 'port');
1098
+                const ip = `${remoteIpAddress}:${remotePort}`;
1099
+
1100
+                const localIpAddress = getStatValue(localUsedCandidate, 'ip');
1101
+                const localPort = getStatValue(localUsedCandidate, 'port');
1102
+
1103
+                const localIp = `${localIpAddress}:${localPort}`;
1104
+                const type = getStatValue(remoteUsedCandidate, 'protocol');
1105
+
1106
+                // Save the address unless it has been saved already.
1107
+                const conferenceStatsTransport = this.conferenceStats.transport;
1108
+
1109
+                if (!conferenceStatsTransport.some(
1110
+                        t =>
1111
+                            t.ip === ip
1112
+                            && t.type === type
1113
+                            && t.localip === localIp)) {
1114
+                    conferenceStatsTransport.push({
1115
+                        ip,
1116
+                        type,
1117
+                        localIp,
1118
+                        p2p: this.peerconnection.isP2P,
1119
+                        localCandidateType: localUsedCandidate.candidateType,
1120
+                        remoteCandidateType: remoteUsedCandidate.candidateType,
1121
+                        networkType: localUsedCandidate.networkType,
1122
+                        rtt: now.currentRoundTripTime * 1000
1123
+                    });
1124
+                }
1125
+            }
1126
+
1127
+        // RTCReceivedRtpStreamStats
1128
+        // https://w3c.github.io/webrtc-stats/#receivedrtpstats-dict*
1129
+        // RTCSentRtpStreamStats
1130
+        // https://w3c.github.io/webrtc-stats/#sentrtpstats-dict*
1131
+        } else if (now.type === 'inbound-rtp' || now.type === 'outbound-rtp') {
1132
+            const before = this.previousStatsReport.get(now.id);
1133
+            const ssrc = this.getNonNegativeValue(now.ssrc);
1134
+
1135
+            if (!before || !ssrc) {
1136
+                return;
1137
+            }
1138
+
1139
+            let ssrcStats = this.ssrc2stats.get(ssrc);
1140
+
1141
+            if (!ssrcStats) {
1142
+                ssrcStats = new SsrcStats();
1143
+                this.ssrc2stats.set(ssrc, ssrcStats);
1144
+            }
1145
+
1146
+            let isDownloadStream = true;
1147
+            let key = 'packetsReceived';
1148
+
1149
+            if (now.type === 'outbound-rtp') {
1150
+                isDownloadStream = false;
1151
+                key = 'packetsSent';
1152
+            }
1153
+
1154
+            let packetsNow = now[key];
1155
+
1156
+            if (!packetsNow || packetsNow < 0) {
1157
+                packetsNow = 0;
1158
+            }
1159
+
1160
+            const packetsBefore = this.getNonNegativeValue(before[key]);
1161
+            const packetsDiff = Math.max(0, packetsNow - packetsBefore);
1162
+
1163
+            const packetsLostNow
1164
+                = this.getNonNegativeValue(now.packetsLost);
1165
+            const packetsLostBefore
1166
+                = this.getNonNegativeValue(before.packetsLost);
1167
+            const packetsLostDiff
1168
+                = Math.max(0, packetsLostNow - packetsLostBefore);
1169
+
1170
+            ssrcStats.setLoss({
1171
+                packetsTotal: packetsDiff + packetsLostDiff,
1172
+                packetsLost: packetsLostDiff,
1173
+                isDownloadStream
1174
+            });
1175
+
1176
+            if (now.type === 'inbound-rtp') {
1177
+
1178
+                ssrcStats.addBitrate({
1179
+                    'download': this._calculateBitrate(
1180
+                                    now, before, 'bytesReceived'),
1181
+                    'upload': 0
1182
+                });
1183
+
1184
+                // RTCInboundRtpStreamStats
1185
+                // https://w3c.github.io/webrtc-stats/#inboundrtpstats-dict*
1186
+                // TODO: can we use framesDecoded for frame rate, available
1187
+                // in chrome
1188
+            } else {
1189
+                byteSentStats[ssrc] = this.getNonNegativeValue(now.bytesSent);
1190
+                ssrcStats.addBitrate({
1191
+                    'download': 0,
1192
+                    'upload': this._calculateBitrate(
1193
+                                now, before, 'bytesSent')
1194
+                });
1195
+
1196
+                // RTCOutboundRtpStreamStats
1197
+                // https://w3c.github.io/webrtc-stats/#outboundrtpstats-dict*
1198
+                // TODO: can we use framesEncoded for frame rate, available
1199
+                // in chrome
1200
+            }
1201
+
1202
+            // FF has framerateMean out of spec
1203
+            const framerateMean = now.framerateMean;
1204
+
1205
+            if (framerateMean) {
1206
+                ssrcStats.setFramerate(Math.round(framerateMean || 0));
1207
+            }
1208
+
1209
+        // track for resolution
1210
+        // RTCVideoHandlerStats
1211
+        // https://w3c.github.io/webrtc-stats/#vststats-dict*
1212
+        // RTCMediaHandlerStats
1213
+        // https://w3c.github.io/webrtc-stats/#mststats-dict*
1214
+        } else if (now.type === 'track') {
1215
+
1216
+            const resolution = {
1217
+                height: now.frameHeight,
1218
+                width: now.frameWidth
1219
+            };
1220
+
1221
+            // Tries to get frame rate
1222
+            let frameRate = now.framesPerSecond;
1223
+
1224
+            if (!frameRate) {
1225
+                // we need to calculate it
1226
+                const before = this.previousStatsReport.get(now.id);
1227
+
1228
+                if (before) {
1229
+                    const timeMs = now.timestamp - before.timestamp;
1230
+
1231
+                    if (timeMs > 0 && now.framesSent) {
1232
+                        const numberOfFramesSinceBefore
1233
+                            = now.framesSent - before.framesSent;
1234
+
1235
+                        frameRate = (numberOfFramesSinceBefore / timeMs) * 1000;
1236
+                    }
1237
+                }
1238
+
1239
+                if (!frameRate) {
1240
+                    return;
1241
+                }
1242
+            }
1243
+
1244
+            const trackIdentifier = now.trackIdentifier;
1245
+            const ssrc = this.peerconnection.getSsrcByTrackId(trackIdentifier);
1246
+            let ssrcStats = this.ssrc2stats.get(ssrc);
1247
+
1248
+            if (!ssrcStats) {
1249
+                ssrcStats = new SsrcStats();
1250
+                this.ssrc2stats.set(ssrc, ssrcStats);
1251
+            }
1252
+            ssrcStats.setFramerate(Math.round(frameRate || 0));
1253
+
1254
+            if (resolution.height && resolution.width) {
1255
+                ssrcStats.setResolution(resolution);
1256
+            } else {
1257
+                ssrcStats.setResolution(null);
1258
+            }
1259
+        }
1260
+    });
1261
+
1262
+    this.eventEmitter.emit(
1263
+        StatisticsEvents.BYTE_SENT_STATS, this.peerconnection, byteSentStats);
1264
+
1265
+    this._processAndEmitReport();
1266
+};
1267
+
1268
+/**
1269
+ * Stats processing logic.
1270
+ */
1271
+StatsCollector.prototype.processNewAudioLevelReport = function() {
1272
+    if (!this.baselineAudioLevelsReport) {
1273
+        return;
1274
+    }
1275
+
1276
+    this.currentAudioLevelsReport.forEach(now => {
1277
+        if (now.type !== 'track') {
1278
+            return;
1279
+        }
1280
+
1281
+        // Audio level
1282
+        const audioLevel = now.audioLevel;
1283
+
1284
+        if (!audioLevel) {
1285
+            return;
1286
+        }
1287
+
1288
+        const trackIdentifier = now.trackIdentifier;
1289
+        const ssrc = this.peerconnection.getSsrcByTrackId(trackIdentifier);
1290
+
1291
+        if (ssrc) {
1292
+            const isLocal
1293
+                = ssrc === this.peerconnection.getLocalSSRC(
1294
+                this.peerconnection.getLocalTracks(MediaType.AUDIO));
1295
+
1296
+            this.eventEmitter.emit(
1297
+                StatisticsEvents.AUDIO_LEVEL,
1298
+                this.peerconnection,
1299
+                ssrc,
1300
+                audioLevel,
1301
+                isLocal);
1302
+        }
1303
+    });
1304
+};
1305
+
1306
+/**
1307
+ * End new promised based getStats processing methods.
1308
+ */

Laden…
Abbrechen
Speichern