瀏覽代碼

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 7 年之前
父節點
當前提交
e3adbca608
共有 3 個檔案被更改,包括 409 行新增16 行删除
  1. 33
    7
      modules/RTC/TraceablePeerConnection.js
  2. 2
    1
      modules/browser/BrowserCapabilities.js
  3. 374
    8
      modules/statistics/RTPStatsCollector.js

+ 33
- 7
modules/RTC/TraceablePeerConnection.js 查看文件

531
     return null;
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
  * Called when new remote MediaStream is added to the PeerConnection.
557
  * Called when new remote MediaStream is added to the PeerConnection.
536
  * @param {MediaStream} stream the WebRTC MediaStream for remote participant
558
  * @param {MediaStream} stream the WebRTC MediaStream for remote participant
2303
     // TODO (brian): After moving all browsers to adapter, check if adapter is
2325
     // TODO (brian): After moving all browsers to adapter, check if adapter is
2304
     // accounting for different getStats apis, making the browser-checking-if
2326
     // accounting for different getStats apis, making the browser-checking-if
2305
     // unnecessary.
2327
     // unnecessary.
2306
-    if (browser.isFirefox()
2307
-            || browser.isTemasysPluginUsed()
2308
-            || browser.isReactNative()) {
2328
+    if (browser.isTemasysPluginUsed()
2329
+        || browser.isReactNative()) {
2309
         this.peerconnection.getStats(
2330
         this.peerconnection.getStats(
2310
             null,
2331
             null,
2311
             callback,
2332
             callback,
2314
                 // Making sure that getStats won't fail if error callback is
2335
                 // Making sure that getStats won't fail if error callback is
2315
                 // not passed.
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
     } else {
2347
     } else {
2322
         this.peerconnection.getStats(callback);
2348
         this.peerconnection.getStats(callback);
2323
     }
2349
     }

+ 2
- 1
modules/browser/BrowserCapabilities.js 查看文件

91
     supportsBandwidthStatistics() {
91
     supportsBandwidthStatistics() {
92
         // FIXME bandwidth stats are currently not implemented for FF on our
92
         // FIXME bandwidth stats are currently not implemented for FF on our
93
         // side, but not sure if not possible ?
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 查看文件

2
 import { browsers } from 'js-utils';
2
 import { browsers } from 'js-utils';
3
 
3
 
4
 import * as StatisticsEvents from '../../service/statistics/Events';
4
 import * as StatisticsEvents from '../../service/statistics/Events';
5
+import * as MediaType from '../../service/RTC/MediaType';
5
 
6
 
6
 const GlobalOnErrorHandler = require('../util/GlobalOnErrorHandler');
7
 const GlobalOnErrorHandler = require('../util/GlobalOnErrorHandler');
7
 const logger = require('jitsi-meet-logger').getLogger(__filename);
8
 const logger = require('jitsi-meet-logger').getLogger(__filename);
10
 const browserSupported = browser.isChrome()
11
 const browserSupported = browser.isChrome()
11
         || browser.isOpera() || browser.isFirefox()
12
         || browser.isOpera() || browser.isFirefox()
12
         || browser.isNWJS() || browser.isElectron()
13
         || browser.isNWJS() || browser.isElectron()
13
-        || browser.isTemasysPluginUsed() || browser.isEdge();
14
+        || browser.isTemasysPluginUsed() || browser.isEdge()
15
+        || browser.isSafariWithWebrtc();
14
 
16
 
15
 /**
17
 /**
16
  * The lib-jitsi-meet browser-agnostic names of the browser-specific keys
18
  * The lib-jitsi-meet browser-agnostic names of the browser-specific keys
25
     'packetsSent': 'packetsSent',
27
     'packetsSent': 'packetsSent',
26
     'bytesReceived': 'bytesReceived',
28
     'bytesReceived': 'bytesReceived',
27
     'bytesSent': 'bytesSent',
29
     'bytesSent': 'bytesSent',
28
-    'framerateMean': 'framerateMean'
30
+    'framerateMean': 'framerateMean',
31
+    'ip': 'ipAddress',
32
+    'port': 'portNumber',
33
+    'protocol': 'transport'
29
 };
34
 };
30
 KEYS_BY_BROWSER_TYPE[browsers.CHROME] = {
35
 KEYS_BY_BROWSER_TYPE[browsers.CHROME] = {
31
     'receiveBandwidth': 'googAvailableReceiveBandwidth',
36
     'receiveBandwidth': 'googAvailableReceiveBandwidth',
50
     'audioOutputLevel': 'audioOutputLevel',
55
     'audioOutputLevel': 'audioOutputLevel',
51
     'currentRoundTripTime': 'googRtt',
56
     'currentRoundTripTime': 'googRtt',
52
     'remoteCandidateType': 'googRemoteCandidateType',
57
     'remoteCandidateType': 'googRemoteCandidateType',
53
-    'localCandidateType': 'googLocalCandidateType'
58
+    'localCandidateType': 'googLocalCandidateType',
59
+    'ip': 'ip',
60
+    'port': 'port',
61
+    'protocol': 'protocol'
54
 };
62
 };
55
 KEYS_BY_BROWSER_TYPE[browsers.EDGE] = {
63
 KEYS_BY_BROWSER_TYPE[browsers.EDGE] = {
56
     'sendBandwidth': 'googAvailableSendBandwidth',
64
     'sendBandwidth': 'googAvailableSendBandwidth',
242
      * @private
250
      * @private
243
      */
251
      */
244
     this._getStatValue = this._defineGetStatValueMethod(keys);
252
     this._getStatValue = this._defineGetStatValueMethod(keys);
253
+    this._getNewStatValue = this._defineNewGetStatValueMethod(keys);
245
 
254
 
246
     this.peerconnection = peerconnection;
255
     this.peerconnection = peerconnection;
247
     this.baselineAudioLevelsReport = null;
256
     this.baselineAudioLevelsReport = null;
313
                             results = report.result();
322
                             results = report.result();
314
                         }
323
                         }
315
                         self.currentAudioLevelsReport = results;
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
                         self.baselineAudioLevelsReport
332
                         self.baselineAudioLevelsReport
318
                             = self.currentAudioLevelsReport;
333
                             = self.currentAudioLevelsReport;
319
                     },
334
                     },
343
 
358
 
344
                         self.currentStatsReport = results;
359
                         self.currentStatsReport = results;
345
                         try {
360
                         try {
346
-                            self.processStatsReport();
361
+                            if (browser.isSafariWithWebrtc()
362
+                                || browser.isFirefox()) {
363
+                                self.processNewStatsReport();
364
+                            } else {
365
+                                self.processStatsReport();
366
+                            }
347
                         } catch (e) {
367
                         } catch (e) {
348
                             GlobalOnErrorHandler.callErrorHandler(e);
368
                             GlobalOnErrorHandler.callErrorHandler(e);
349
                             logger.error(`Unsupported key:${e}`, e);
369
                             logger.error(`Unsupported key:${e}`, e);
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
     // process stats
755
     // process stats
726
     const totalPackets = {
756
     const totalPackets = {
727
         download: 0,
757
         download: 0,
804
         ssrcStats.resetBitrate();
834
         ssrcStats.resetBitrate();
805
     }
835
     }
806
 
836
 
807
-    this.eventEmitter.emit(
808
-        StatisticsEvents.BYTE_SENT_STATS, this.peerconnection, byteSentStats);
809
-
810
     this.conferenceStats.bitrate = {
837
     this.conferenceStats.bitrate = {
811
         'upload': bitrateUpload,
838
         'upload': bitrateUpload,
812
         'download': bitrateDownload
839
         'download': bitrateDownload
940
 };
967
 };
941
 
968
 
942
 /* eslint-enable no-continue */
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
+ */

Loading…
取消
儲存