Bläddra i källkod

edge: add ORTC based WebRTC shim for full Edge support

master
Iñaki Baz Castillo 8 år sedan
förälder
incheckning
e4133e067b

+ 10
- 2
modules/RTC/RTC.js Visa fil

@@ -554,13 +554,21 @@ export default class RTC extends Listenable {
554 554
     }
555 555
 
556 556
     /**
557
-     *
558
-     * @param stream
557
+     * Returns the id of the given stream.
558
+     * @param {MediaStream} stream
559 559
      */
560 560
     static getStreamID(stream) {
561 561
         return RTCUtils.getStreamID(stream);
562 562
     }
563 563
 
564
+    /**
565
+     * Returns the id of the given track.
566
+     * @param {MediaStreamTrack} track
567
+     */
568
+    static getTrackID(track) {
569
+        return RTCUtils.getTrackID(track);
570
+    }
571
+
564 572
     /**
565 573
      * Returns true if retrieving the the list of input devices is supported
566 574
      * and false if not.

+ 4
- 4
modules/RTC/RTCBrowserType.js Visa fil

@@ -34,7 +34,7 @@ const RTCBrowserType = {
34 34
      * strategy or <tt>false</tt> otherwise.
35 35
      */
36 36
     doesVideoMuteByStreamRemove() {
37
-        return !RTCBrowserType.isFirefox();
37
+        return !(RTCBrowserType.isFirefox() || RTCBrowserType.isEdge());
38 38
     },
39 39
 
40 40
     /**
@@ -129,7 +129,7 @@ const RTCBrowserType = {
129 129
      * otherwise.
130 130
      */
131 131
     isP2PSupported() {
132
-        return true;
132
+        return !RTCBrowserType.isEdge();
133 133
     },
134 134
 
135 135
     /**
@@ -215,7 +215,7 @@ const RTCBrowserType = {
215 215
     supportsBandwidthStatistics() {
216 216
         // FIXME bandwidth stats are currently not implemented for FF on our
217 217
         // side, but not sure if not possible ?
218
-        return !RTCBrowserType.isFirefox();
218
+        return !RTCBrowserType.isFirefox() && !RTCBrowserType.isEdge();
219 219
     },
220 220
 
221 221
     /**
@@ -241,7 +241,7 @@ const RTCBrowserType = {
241 241
         // (is reported as 1):
242 242
         // https://bugzilla.mozilla.org/show_bug.cgi?id=1241066
243 243
         // For Chrome and others we rely on 'googRtt'.
244
-        return !RTCBrowserType.isFirefox();
244
+        return !RTCBrowserType.isFirefox() && !RTCBrowserType.isEdge();
245 245
     },
246 246
 
247 247
     /**

+ 27
- 20
modules/RTC/RTCUtils.js Visa fil

@@ -29,9 +29,6 @@ import VideoType from '../../service/RTC/VideoType';
29 29
 
30 30
 const logger = getLogger(__filename);
31 31
 
32
-// Disable Edge until fully implemented.
33
-const ENABLE_EDGE = false;
34
-
35 32
 // XXX Don't require Temasys unless it's to be used because it doesn't run on
36 33
 // React Native, for example.
37 34
 const AdapterJS
@@ -121,10 +118,10 @@ function setResolutionConstraints(
121 118
     if (Resolutions[resolution]) {
122 119
         if (isNewStyleConstraintsSupported) {
123 120
             constraints.video.width = {
124
-                exact: Resolutions[resolution].width
121
+                ideal: Resolutions[resolution].width
125 122
             };
126 123
             constraints.video.height = {
127
-                exact: Resolutions[resolution].height
124
+                ideal: Resolutions[resolution].height
128 125
             };
129 126
         }
130 127
 
@@ -794,6 +791,9 @@ class RTCUtils extends Listenable {
794 791
 
795 792
                     return SDPUtil.filterSpecialChars(id);
796 793
                 };
794
+                this.getTrackID = function(track) {
795
+                    return track.id;
796
+                };
797 797
 
798 798
                 /* eslint-disable no-global-assign, no-native-reassign */
799 799
                 RTCSessionDescription = mozRTCSessionDescription;
@@ -844,6 +844,9 @@ class RTCUtils extends Listenable {
844 844
                             ? id
845 845
                             : SDPUtil.filterSpecialChars(id));
846 846
                 };
847
+                this.getTrackID = function(track) {
848
+                    return track.id;
849
+                };
847 850
 
848 851
                 this.pcConstraints = { optional: [] };
849 852
 
@@ -870,14 +873,6 @@ class RTCUtils extends Listenable {
870 873
                     };
871 874
                 }
872 875
             } else if (RTCBrowserType.isEdge()) {
873
-                // Disable until fully implemented.
874
-                if (!ENABLE_EDGE) {
875
-                    rejectWithWebRTCNotSupported(
876
-                        'Microsoft Edge not yet supported', reject);
877
-
878
-                    return;
879
-                }
880
-
881 876
                 this.RTCPeerConnectionType = ortcRTCPeerConnection;
882 877
                 this.getUserMedia
883 878
                     = wrapGetUserMedia(
@@ -895,14 +890,23 @@ class RTCUtils extends Listenable {
895 890
                         return element;
896 891
                     });
897 892
 
898
-                // TODO: needed in Edge?
893
+                // ORTC does not generate remote MediaStreams so those are
894
+                // manually created by the ORTC shim. This means that their
895
+                // id (internally generated) does not match the stream id
896
+                // signaled into the remote SDP. Therefore, the shim adds a
897
+                // custom jitsiRemoteId property with the original stream id.
899 898
                 this.getStreamID = function(stream) {
900
-                    const id = stream.id;
899
+                    const id = stream.jitsiRemoteId || stream.id;
901 900
 
902
-                    return (
903
-                        typeof id === 'number'
904
-                            ? id
905
-                            : SDPUtil.filterSpecialChars(id));
901
+                    return SDPUtil.filterSpecialChars(id);
902
+                };
903
+
904
+                // Remote MediaStreamTracks generated by ORTC (within a
905
+                // RTCRtpReceiver) have an internally/random id which does not
906
+                // match the track id signaled in the remote SDP. The shim adds
907
+                // a custom jitsi-id property with the original track id.
908
+                this.getTrackID = function(track) {
909
+                    return track.jitsiRemoteId || track.id;
906 910
                 };
907 911
             } else if (RTCBrowserType.isTemasysPluginUsed()) {
908 912
                 // Detect IE/Safari
@@ -942,6 +946,8 @@ class RTCUtils extends Listenable {
942 946
                         });
943 947
                     this.getStreamID
944 948
                         = stream => SDPUtil.filterSpecialChars(stream.label);
949
+                    this.getTrackID
950
+                        = track => track.id;
945 951
 
946 952
                     onReady(
947 953
                         options,
@@ -1294,7 +1300,8 @@ class RTCUtils extends Listenable {
1294 1300
                 || RTCBrowserType.isOpera()
1295 1301
                 || RTCBrowserType.isTemasysPluginUsed()
1296 1302
                 || RTCBrowserType.isNWJS()
1297
-                || RTCBrowserType.isElectron();
1303
+                || RTCBrowserType.isElectron()
1304
+                || RTCBrowserType.isEdge();
1298 1305
     }
1299 1306
 
1300 1307
     /**

+ 4
- 4
modules/RTC/TraceablePeerConnection.js Visa fil

@@ -517,12 +517,12 @@ TraceablePeerConnection.prototype._remoteStreamAdded = function(stream) {
517 517
 
518 518
     // Bind 'addtrack'/'removetrack' event handlers
519 519
     if (RTCBrowserType.isChrome() || RTCBrowserType.isNWJS()
520
-        || RTCBrowserType.isElectron()) {
520
+        || RTCBrowserType.isElectron() || RTCBrowserType.isEdge()) {
521 521
         stream.onaddtrack = event => {
522
-            this._remoteTrackAdded(event.target, event.track);
522
+            this._remoteTrackAdded(stream, event.track);
523 523
         };
524 524
         stream.onremovetrack = event => {
525
-            this._remoteTrackRemoved(event.target, event.track);
525
+            this._remoteTrackRemoved(stream, event.track);
526 526
         };
527 527
     }
528 528
 
@@ -726,7 +726,7 @@ TraceablePeerConnection.prototype._remoteStreamRemoved = function(stream) {
726 726
 TraceablePeerConnection.prototype._remoteTrackRemoved
727 727
 = function(stream, track) {
728 728
     const streamId = RTC.getStreamID(stream);
729
-    const trackId = track && track.id;
729
+    const trackId = track && RTC.getTrackID(track);
730 730
 
731 731
     logger.info(`${this} - remote track removed: ${streamId}, ${trackId}`);
732 732
 

+ 93
- 0
modules/RTC/ortc/README.md Visa fil

@@ -0,0 +1,93 @@
1
+# ORTC shim for Edge
2
+
3
+The `modules/RTC/ortc` folder contains a `RTCPeerConnection` shim for Edge based on ORTC among with other shims (such as `RTCSessionDescription`) and some utilities/helpers to deal with both, SDP and ORTC objects.
4
+
5
+
6
+## Files in `ortc` folder
7
+
8
+
9
+### RTCPeerConnection.js
10
+
11
+Exports a `RTCPeerConnection` shim. The interface is based on the [W3C specification of 2015](https://www.w3.org/TR/2015/WD-webrtc-20150210/), which matches (mostly) the current implementation of Chrome.
12
+
13
+It also implements Plan-B for multi-stream.
14
+
15
+
16
+#### Limitations
17
+
18
+* BUNDLE is assumed (single transport for all the local and remote media streams).
19
+* `rtcp-mux` is assumed (not a real problem nowadays).
20
+* Calling `createOffer()` is not implemented, so P2P mode is not supported (`RTCBrowserType.isP2PSupported()` returns `false`).
21
+* Calling `setRemoteDescription()` with a SDP answer with mangled SSRC values is currently unsupported (those new SSRC values will not be used for sending media).
22
+* If the app calls `createAnswer()`, mangles SSRC values, and applies by calling `setLocalDescription()`, those new SSRC values are currently ignored.
23
+* Simulcast not supported (currently Edge supports `maxFramerate` per encoding but it does not support `maxBitrate`, `resolutionScale` or `framerateScale`, so it's not worth it).
24
+* `RTCDataChannel` not supported (Edge does not implement it).
25
+* `addIceCandidate()` not supported (it's never called anyway, at least in non P2P mode). Edge does not support Trickle-ICE which means that, after reading all the remote ICE candidates from the remote SDP and applying them, an "empty" candidate must be immediately applied (otherwise the `RTCIceTransport` never enters the "completed" state) and, after that, new remote candidates cannot be added.
26
+
27
+
28
+### RTCSessionDescription.js
29
+
30
+Exports a `RTCSessionDescription` shim.
31
+
32
+The interface is the same as in the WebRTC specification (although internally it also handles a Object representation of the SDP generated by the [sdp-transform](https://www.npmjs.com/package/sdp-transform) library).
33
+
34
+
35
+### errors.js
36
+
37
+Exports some `Error` based classes needed in WebRTC (such as `InvalidStateError`).
38
+
39
+
40
+### utils.js
41
+
42
+Some utilities related to SDP and ORTC.
43
+
44
+
45
+## Issues
46
+
47
+
48
+### ICE error: Wrong MESSAGE-INTEGRITY
49
+
50
+Randomly, ICE Binding Requests from Edge are replied with WRONG MESSAGE-INTEGRITY error by the bridge. It seems that Edge, sometimes, adds a extra byte in the `USERNAME` attribute producing such an issue.
51
+
52
+* GitHub issue: https://github.com/jitsi/lib-jitsi-meet/issues/498
53
+* Edge issue: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12332457/
54
+
55
+
56
+### Wrong VP8 video from Edge to Chrome if ENABLE_VP8_PICID_REWRITING=True
57
+
58
+When VP8 rewriting is enabled in the bridge, VP8 video produced by Edge is wrongly rendered in Chrome/Firefox.
59
+
60
+* GitHub issue: https://github.com/jitsi/lib-jitsi-meet/issues/520
61
+
62
+
63
+### InvalidStateError when calling send() / receive()
64
+
65
+Randomly, sending a new track or receiving a new track fails with `InvalidStateError`. The issue reported in Edge tracker includes a way to reproduce it.
66
+
67
+As per conversations with Edge developers, it seems that Edge has some kind of limitation in the number of VP8 streams it can encode/decode at the same time. However, in my tests I've been able to send a VP8 stream and receive 5 VP8 streams at the same time.
68
+
69
+The problem happens much more often when calling `rtpSender.stop()` or `rtpReceiver.stop()` and then creating a new `RtpSender` or `RtpReceiver` and calling `send()` or `receive()` on it.
70
+
71
+* GitHub issue: https://github.com/jitsi/lib-jitsi-meet/issues/519
72
+* Edge issue: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12459320/
73
+
74
+
75
+### pc.getStats(): no way to get usable and needed stats
76
+
77
+WebRTC stats produced by Edge don't provide enough information to determine resolution, framerate or bitrate.
78
+
79
+* GitHub issue: https://github.com/jitsi/lib-jitsi-meet/issues/523
80
+
81
+
82
+### Device selection does not work
83
+
84
+In Edge, the device selector UI shows grayed drop down lists for mic and webcam.
85
+
86
+* GitHub issue: https://github.com/jitsi/lib-jitsi-meet/issues/530
87
+
88
+
89
+### Other issues in Edge
90
+
91
+Those issues/bugs in Edge do not affect lib-jitsi-meet because the code avoids them. However, for future changes, it's important to consider them:
92
+
93
+* `RtpReceiver.track` returns a different `MediaStreamTrack` every time: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12399497/

+ 1789
- 266
modules/RTC/ortc/RTCPeerConnection.js
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 42
- 16
modules/RTC/ortc/RTCSessionDescription.js Visa fil

@@ -6,20 +6,20 @@ import sdpTransform from 'sdp-transform';
6 6
 export default class RTCSessionDescription {
7 7
     /**
8 8
      * RTCSessionDescription constructor.
9
-     * @param {object} [data]
10
-     * @param {string} [data.type] "offer" / "answer".
11
-     * @param {string} [data.sdp] SDP string.
12
-     * @param {object} [data._sdpObject] SDP object generated by the
9
+     * @param {Object} [data]
10
+     * @param {String} [data.type] - 'offer' / 'answer'.
11
+     * @param {String} [data.sdp] - SDP string.
12
+     * @param {Object} [data._sdpObject] - SDP object generated by the
13 13
      * sdp-transform library.
14 14
      */
15 15
     constructor(data) {
16
-        // @type {string}
16
+        // @type {String}
17 17
         this._sdp = null;
18 18
 
19
-        // @type {object}
19
+        // @type {Object}
20 20
         this._sdpObject = null;
21 21
 
22
-        // @type {string}
22
+        // @type {String}
23 23
         this._type = null;
24 24
 
25 25
         switch (data.type) {
@@ -53,27 +53,53 @@ export default class RTCSessionDescription {
53 53
     }
54 54
 
55 55
     /**
56
-     * Get type field.
57
-     * @return {string}
56
+     * Get sdp field.
57
+     * @return {String}
58 58
      */
59
-    get type() {
60
-        return this._type;
59
+    get sdp() {
60
+        return this._sdp;
61 61
     }
62 62
 
63 63
     /**
64
-     * Get sdp field.
65
-     * @return {string}
64
+     * Set sdp field.
65
+     * NOTE: This is not allowed per spec, but lib-jitsi-meet uses it.
66
+     * @param {String} sdp
66 67
      */
67
-    get sdp() {
68
-        return this._sdp;
68
+    set sdp(sdp) {
69
+        try {
70
+            this._sdpObject = sdpTransform.parse(sdp);
71
+        } catch (error) {
72
+            throw new Error(`invalid sdp: ${error}`);
73
+        }
74
+
75
+        this._sdp = sdp;
69 76
     }
70 77
 
71 78
     /**
72 79
      * Gets the internal sdp object.
73
-     * @return {object}
80
+     * @return {Object}
74 81
      * @private
75 82
      */
76 83
     get sdpObject() {
77 84
         return this._sdpObject;
78 85
     }
86
+
87
+    /**
88
+     * Get type field.
89
+     * @return {String}
90
+     */
91
+    get type() {
92
+        return this._type;
93
+    }
94
+
95
+    /**
96
+     * Returns an object with type and sdp fields.
97
+     * @return {Object}
98
+     */
99
+    toJSON() {
100
+        return {
101
+            sdp: this._sdp,
102
+            type: this._type
103
+        };
104
+    }
79 105
 }

+ 458
- 0
modules/RTC/ortc/utils.js Visa fil

@@ -0,0 +1,458 @@
1
+/* global RTCRtpReceiver */
2
+
3
+import sdpTransform from 'sdp-transform';
4
+
5
+/**
6
+ * Extract RTP capabilities from remote description.
7
+ * @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
8
+ * @return {RTCRtpCapabilities}
9
+ */
10
+export function extractCapabilities(sdpObject) {
11
+    // Map of RtpCodecParameters indexed by payload type.
12
+    const codecsMap = new Map();
13
+
14
+    // Array of RtpHeaderExtensions.
15
+    const headerExtensions = [];
16
+
17
+    for (const m of sdpObject.media) {
18
+        // Media kind.
19
+        const kind = m.type;
20
+
21
+        if (kind !== 'audio' && kind !== 'video') {
22
+            continue; // eslint-disable-line no-continue
23
+        }
24
+
25
+        // Get codecs.
26
+        for (const rtp of m.rtp) {
27
+            const codec = {
28
+                clockRate: rtp.rate,
29
+                kind,
30
+                mimeType: `${kind}/${rtp.codec}`,
31
+                name: rtp.codec,
32
+                numChannels: rtp.encoding || 1,
33
+                parameters: {},
34
+                preferredPayloadType: rtp.payload,
35
+                rtcpFeedback: []
36
+            };
37
+
38
+            codecsMap.set(codec.preferredPayloadType, codec);
39
+        }
40
+
41
+        // Get codec parameters.
42
+        for (const fmtp of m.fmtp || []) {
43
+            const parameters = sdpTransform.parseFmtpConfig(fmtp.config);
44
+            const codec = codecsMap.get(fmtp.payload);
45
+
46
+            if (!codec) {
47
+                continue; // eslint-disable-line no-continue
48
+            }
49
+
50
+            codec.parameters = parameters;
51
+        }
52
+
53
+        // Get RTCP feedback for each codec.
54
+        for (const fb of m.rtcpFb || []) {
55
+            const codec = codecsMap.get(fb.payload);
56
+
57
+            if (!codec) {
58
+                continue; // eslint-disable-line no-continue
59
+            }
60
+
61
+            codec.rtcpFeedback.push({
62
+                parameter: fb.subtype || '',
63
+                type: fb.type
64
+            });
65
+        }
66
+
67
+        // Get RTP header extensions.
68
+        for (const ext of m.ext || []) {
69
+            const preferredId = ext.value;
70
+            const uri = ext.uri;
71
+            const headerExtension = {
72
+                kind,
73
+                uri,
74
+                preferredId
75
+            };
76
+
77
+            // Check if already present.
78
+            const duplicated = headerExtensions.find(savedHeaderExtension =>
79
+                headerExtension.kind === savedHeaderExtension.kind
80
+                    && headerExtension.uri === savedHeaderExtension.uri
81
+            );
82
+
83
+            if (!duplicated) {
84
+                headerExtensions.push(headerExtension);
85
+            }
86
+        }
87
+    }
88
+
89
+    return {
90
+        codecs: Array.from(codecsMap.values()),
91
+        fecMechanisms: [], // TODO
92
+        headerExtensions
93
+    };
94
+}
95
+
96
+/**
97
+ * Extract DTLS parameters from remote description.
98
+ * @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
99
+ * @return {RTCDtlsParameters}
100
+ */
101
+export function extractDtlsParameters(sdpObject) {
102
+    const media = getFirstActiveMediaSection(sdpObject);
103
+    const fingerprint = media.fingerprint || sdpObject.fingerprint;
104
+    let role;
105
+
106
+    switch (media.setup) {
107
+    case 'active':
108
+        role = 'client';
109
+        break;
110
+    case 'passive':
111
+        role = 'server';
112
+        break;
113
+    case 'actpass':
114
+        role = 'auto';
115
+        break;
116
+    }
117
+
118
+    return {
119
+        role,
120
+        fingerprints: [
121
+            {
122
+                algorithm: fingerprint.type,
123
+                value: fingerprint.hash
124
+            }
125
+        ]
126
+    };
127
+}
128
+
129
+/**
130
+ * Extract ICE candidates from remote description.
131
+ * NOTE: This implementation assumes a single BUNDLEd transport and rtcp-mux.
132
+ * @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
133
+ * @return {sequence<RTCIceCandidate>}
134
+ */
135
+export function extractIceCandidates(sdpObject) {
136
+    const media = getFirstActiveMediaSection(sdpObject);
137
+    const candidates = [];
138
+
139
+    for (const c of media.candidates) {
140
+        // Ignore RTCP candidates (we assume rtcp-mux).
141
+        if (c.component !== 1) {
142
+            continue; // eslint-disable-line no-continue
143
+        }
144
+
145
+        const candidate = {
146
+            foundation: c.foundation,
147
+            ip: c.ip,
148
+            port: c.port,
149
+            priority: c.priority,
150
+            protocol: c.transport.toLowerCase(),
151
+            type: c.type
152
+        };
153
+
154
+        candidates.push(candidate);
155
+    }
156
+
157
+    return candidates;
158
+}
159
+
160
+/**
161
+ * Extract ICE parameters from remote description.
162
+ * NOTE: This implementation assumes a single BUNDLEd transport.
163
+ * @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
164
+ * @return {RTCIceParameters}
165
+ */
166
+export function extractIceParameters(sdpObject) {
167
+    const media = getFirstActiveMediaSection(sdpObject);
168
+    const usernameFragment = media.iceUfrag;
169
+    const password = media.icePwd;
170
+    const icelite = sdpObject.icelite === 'ice-lite';
171
+
172
+    return {
173
+        icelite,
174
+        password,
175
+        usernameFragment
176
+    };
177
+}
178
+
179
+/**
180
+ * Extract MID values from remote description.
181
+ * @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
182
+ * @return {map<String, String>} Ordered Map with MID as key and kind as value.
183
+ */
184
+export function extractMids(sdpObject) {
185
+    const midToKind = new Map();
186
+
187
+    // Ignore disabled media sections.
188
+    for (const m of sdpObject.media) {
189
+        midToKind.set(m.mid, m.type);
190
+    }
191
+
192
+    return midToKind;
193
+}
194
+
195
+/**
196
+ * Extract tracks information.
197
+ * @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
198
+ * @return {Map}
199
+ */
200
+export function extractTrackInfos(sdpObject) {
201
+    // Map with info about receiving media.
202
+    // - index: Media SSRC
203
+    // - value: Object
204
+    //   - kind: 'audio' / 'video'
205
+    //   - ssrc: Media SSRC
206
+    //   - rtxSsrc: RTX SSRC (may be unset)
207
+    //   - streamId: MediaStream.jitsiRemoteId
208
+    //   - trackId: MediaStreamTrack.jitsiRemoteId
209
+    //   - cname: CNAME
210
+    // @type {map<Number, Object>}
211
+    const infos = new Map();
212
+
213
+    // Map with stream SSRC as index and associated RTX SSRC as value.
214
+    // @type {map<Number, Number>}
215
+    const rtxMap = new Map();
216
+
217
+    // Set of RTX SSRC values.
218
+    const rtxSet = new Set();
219
+
220
+    for (const m of sdpObject.media) {
221
+        const kind = m.type;
222
+
223
+        if (kind !== 'audio' && kind !== 'video') {
224
+            continue; // eslint-disable-line no-continue
225
+        }
226
+
227
+        // Get RTX information.
228
+        for (const ssrcGroup of m.ssrcGroups || []) {
229
+            // Just consider FID.
230
+            if (ssrcGroup.semantics !== 'FID') {
231
+                continue; // eslint-disable-line no-continue
232
+            }
233
+
234
+            const ssrcs
235
+                = ssrcGroup.ssrcs.split(' ').map(ssrc => Number(ssrc));
236
+            const ssrc = ssrcs[0];
237
+            const rtxSsrc = ssrcs[1];
238
+
239
+            rtxMap.set(ssrc, rtxSsrc);
240
+            rtxSet.add(rtxSsrc);
241
+        }
242
+
243
+        for (const ssrcObject of m.ssrcs || []) {
244
+            const ssrc = ssrcObject.id;
245
+
246
+            // Ignore RTX.
247
+            if (rtxSet.has(ssrc)) {
248
+                continue; // eslint-disable-line no-continue
249
+            }
250
+
251
+            let info = infos.get(ssrc);
252
+
253
+            if (!info) {
254
+                info = {
255
+                    kind,
256
+                    rtxSsrc: rtxMap.get(ssrc),
257
+                    ssrc
258
+                };
259
+
260
+                infos.set(ssrc, info);
261
+            }
262
+
263
+            switch (ssrcObject.attribute) {
264
+            case 'cname': {
265
+                info.cname = ssrcObject.value;
266
+                break;
267
+            }
268
+            case 'msid': {
269
+                const values = ssrcObject.value.split(' ');
270
+                const streamId = values[0];
271
+                const trackId = values[1];
272
+
273
+                info.streamId = streamId;
274
+                info.trackId = trackId;
275
+                break;
276
+            }
277
+            case 'mslabel': {
278
+                const streamId = ssrcObject.value;
279
+
280
+                info.streamId = streamId;
281
+                break;
282
+            }
283
+            case 'label': {
284
+                const trackId = ssrcObject.value;
285
+
286
+                info.trackId = trackId;
287
+                break;
288
+            }
289
+            }
290
+        }
291
+    }
292
+
293
+    return infos;
294
+}
295
+
296
+/**
297
+ * Get local ORTC RTP capabilities filtered and adapted to the given remote RTP
298
+ * capabilities.
299
+ * @param {RTCRtpCapabilities} filterWithCapabilities - RTP capabilities to
300
+ * filter with.
301
+ * @return {RTCRtpCapabilities}
302
+ */
303
+export function getLocalCapabilities(filterWithCapabilities) {
304
+    const localFullCapabilities = RTCRtpReceiver.getCapabilities();
305
+    const localCapabilities = {
306
+        codecs: [],
307
+        fecMechanisms: [],
308
+        headerExtensions: []
309
+    };
310
+
311
+    // Map of RTX and codec payloads.
312
+    // - index: Codec payloadType
313
+    // - value: Associated RTX payloadType
314
+    // @type {map<Number, Number>}
315
+    const remoteRtxMap = new Map();
316
+
317
+    // Set codecs.
318
+    for (const remoteCodec of filterWithCapabilities.codecs) {
319
+        const remoteCodecName = remoteCodec.name.toLowerCase();
320
+
321
+        if (remoteCodecName === 'rtx') {
322
+            remoteRtxMap.set(
323
+                remoteCodec.parameters.apt, remoteCodec.preferredPayloadType);
324
+
325
+            continue; // eslint-disable-line no-continue
326
+        }
327
+
328
+        const localCodec = localFullCapabilities.codecs.find(codec =>
329
+            codec.name.toLowerCase() === remoteCodecName
330
+                && codec.kind === remoteCodec.kind
331
+                && codec.clockRate === remoteCodec.clockRate
332
+        );
333
+
334
+        if (!localCodec) {
335
+            continue; // eslint-disable-line no-continue
336
+        }
337
+
338
+        const codec = {
339
+            clockRate: localCodec.clockRate,
340
+            kind: localCodec.kind,
341
+            mimeType: `${localCodec.kind}/${localCodec.name}`,
342
+            name: localCodec.name,
343
+            numChannels: localCodec.numChannels || 1,
344
+            parameters: {},
345
+            preferredPayloadType: remoteCodec.preferredPayloadType,
346
+            rtcpFeedback: []
347
+        };
348
+
349
+        for (const remoteParamName of Object.keys(remoteCodec.parameters)) {
350
+            const remoteParamValue
351
+                = remoteCodec.parameters[remoteParamName];
352
+
353
+            for (const localParamName of Object.keys(localCodec.parameters)) {
354
+                const localParamValue
355
+                    = localCodec.parameters[localParamName];
356
+
357
+                if (localParamName !== remoteParamName) {
358
+                    continue; // eslint-disable-line no-continue
359
+                }
360
+
361
+                // TODO: We should consider much more cases here, but Edge
362
+                // does not support many codec parameters.
363
+                if (localParamValue === remoteParamValue) {
364
+                    // Use this RTP parameter.
365
+                    codec.parameters[localParamName] = localParamValue;
366
+                    break;
367
+                }
368
+            }
369
+        }
370
+
371
+        for (const remoteFb of remoteCodec.rtcpFeedback) {
372
+            const localFb = localCodec.rtcpFeedback.find(fb =>
373
+                fb.type === remoteFb.type
374
+                    && fb.parameter === remoteFb.parameter
375
+            );
376
+
377
+            if (localFb) {
378
+                // Use this RTCP feedback.
379
+                codec.rtcpFeedback.push(localFb);
380
+            }
381
+        }
382
+
383
+        // Use this codec.
384
+        localCapabilities.codecs.push(codec);
385
+    }
386
+
387
+    // Add RTX for video codecs.
388
+    for (const codec of localCapabilities.codecs) {
389
+        const payloadType = codec.preferredPayloadType;
390
+
391
+        if (!remoteRtxMap.has(payloadType)) {
392
+            continue; // eslint-disable-line no-continue
393
+        }
394
+
395
+        const rtxCodec = {
396
+            clockRate: codec.clockRate,
397
+            kind: codec.kind,
398
+            mimeType: `${codec.kind}/rtx`,
399
+            name: 'rtx',
400
+            parameters: {
401
+                apt: payloadType
402
+            },
403
+            preferredPayloadType: remoteRtxMap.get(payloadType),
404
+            rtcpFeedback: []
405
+        };
406
+
407
+        // Add RTX codec.
408
+        localCapabilities.codecs.push(rtxCodec);
409
+    }
410
+
411
+    // Add RTP header extensions.
412
+    for (const remoteExtension of filterWithCapabilities.headerExtensions) {
413
+        const localExtension
414
+            = localFullCapabilities.headerExtensions.find(extension =>
415
+                extension.kind === remoteExtension.kind
416
+                    && extension.uri === remoteExtension.uri
417
+            );
418
+
419
+        if (localExtension) {
420
+            const extension = {
421
+                kind: localExtension.kind,
422
+                preferredEncrypt: Boolean(remoteExtension.preferredEncrypt),
423
+                preferredId: remoteExtension.preferredId,
424
+                uri: localExtension.uri
425
+            };
426
+
427
+            // Use this RTP header extension.
428
+            localCapabilities.headerExtensions.push(extension);
429
+        }
430
+    }
431
+
432
+    // Add FEC mechanisms.
433
+    // NOTE: We don't support FEC yet and, in fact, neither does Edge.
434
+    for (const remoteFecMechanism of filterWithCapabilities.fecMechanisms) {
435
+        const localFecMechanism
436
+            = localFullCapabilities.fecMechanisms.find(fec =>
437
+                fec === remoteFecMechanism
438
+            );
439
+
440
+        if (localFecMechanism) {
441
+            // Use this FEC mechanism.
442
+            localCapabilities.fecMechanisms.push(localFecMechanism);
443
+        }
444
+    }
445
+
446
+    return localCapabilities;
447
+}
448
+
449
+/**
450
+ * Get the first acive media section.
451
+ * @param {Object} sdpObject - SDP object generated by sdp-transform.
452
+ * @return {Object} SDP media section as parsed by sdp-transform.
453
+ */
454
+function getFirstActiveMediaSection(sdpObject) {
455
+    return sdpObject.media.find(m =>
456
+        m.iceUfrag && m.port !== 0
457
+    );
458
+}

+ 91
- 12
modules/statistics/RTPStatsCollector.js Visa fil

@@ -8,7 +8,7 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
8 8
 const browserSupported = RTCBrowserType.isChrome()
9 9
         || RTCBrowserType.isOpera() || RTCBrowserType.isFirefox()
10 10
         || RTCBrowserType.isNWJS() || RTCBrowserType.isElectron()
11
-        || RTCBrowserType.isTemasysPluginUsed();
11
+        || RTCBrowserType.isTemasysPluginUsed() || RTCBrowserType.isEdge();
12 12
 
13 13
 /**
14 14
  * The lib-jitsi-meet browser-agnostic names of the browser-specific keys
@@ -48,6 +48,28 @@ KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_CHROME] = {
48 48
     'audioOutputLevel': 'audioOutputLevel',
49 49
     'currentRoundTripTime': 'googRtt'
50 50
 };
51
+KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_EDGE] = {
52
+    'sendBandwidth': 'googAvailableSendBandwidth',
53
+    'remoteAddress': 'remoteAddress',
54
+    'transportType': 'protocol',
55
+    'localAddress': 'localAddress',
56
+    'activeConnection': 'activeConnection',
57
+    'ssrc': 'ssrc',
58
+    'packetsReceived': 'packetsReceived',
59
+    'packetsSent': 'packetsSent',
60
+    'packetsLost': 'packetsLost',
61
+    'bytesReceived': 'bytesReceived',
62
+    'bytesSent': 'bytesSent',
63
+    'googFrameHeightReceived': 'frameHeight',
64
+    'googFrameWidthReceived': 'frameWidth',
65
+    'googFrameHeightSent': 'frameHeight',
66
+    'googFrameWidthSent': 'frameWidth',
67
+    'googFrameRateReceived': 'framesPerSecond',
68
+    'googFrameRateSent': 'framesPerSecond',
69
+    'audioInputLevel': 'audioLevel',
70
+    'audioOutputLevel': 'audioLevel',
71
+    'currentRoundTripTime': 'roundTripTime'
72
+};
51 73
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_OPERA]
52 74
     = KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_CHROME];
53 75
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_NWJS]
@@ -314,6 +336,7 @@ StatsCollector.prototype.start = function(startAudioLevelStats) {
314 336
                             // chrome
315 337
                             results = report.result();
316 338
                         }
339
+
317 340
                         self.currentStatsReport = results;
318 341
                         try {
319 342
                             self.processStatsReport();
@@ -395,6 +418,9 @@ StatsCollector.prototype._defineGetStatValueMethod = function(keys) {
395 418
             return value;
396 419
         };
397 420
         break;
421
+    case RTCBrowserType.RTC_BROWSER_EDGE:
422
+        itemStatByKey = (item, key) => item[key];
423
+        break;
398 424
     default:
399 425
         itemStatByKey = (item, key) => item[key];
400 426
     }
@@ -513,13 +539,35 @@ StatsCollector.prototype.processStatsReport = function() {
513 539
             });
514 540
         }
515 541
 
542
+        // NOTE: Edge's proprietary stats via RTCIceTransport.msGetStats().
543
+        if (now.msType === 'transportdiagnostics') {
544
+            this.conferenceStats.transport.push({
545
+                ip: now.remoteAddress,
546
+                type: now.protocol,
547
+                localip: now.localAddress,
548
+                p2p: this.peerconnection.isP2P
549
+            });
550
+        }
551
+
516 552
         if (now.type !== 'ssrc' && now.type !== 'outboundrtp'
517
-            && now.type !== 'inboundrtp') {
553
+            && now.type !== 'inboundrtp' && now.type !== 'track') {
554
+            continue;
555
+        }
556
+
557
+        // NOTE: In Edge, stats with type "inboundrtp" and "outboundrtp" are
558
+        // completely useless, so ignore them.
559
+        if (RTCBrowserType.isEdge()
560
+            && (now.type === 'inboundrtp' || now.type === 'outboundrtp')) {
518 561
             continue;
519 562
         }
520 563
 
521 564
         const before = this.previousStatsReport[idx];
522
-        const ssrc = this.getNonNegativeStat(now, 'ssrc');
565
+        let ssrc = this.getNonNegativeStat(now, 'ssrc');
566
+
567
+        // If type="track", take the first SSRC from ssrcIds.
568
+        if (now.type === 'track' && Array.isArray(now.ssrcIds)) {
569
+            ssrc = Number(now.ssrcIds[0]);
570
+        }
523 571
 
524 572
         if (!before || !ssrc) {
525 573
             continue;
@@ -529,10 +577,12 @@ StatsCollector.prototype.processStatsReport = function() {
529 577
         // according to the spec
530 578
         // https://www.w3.org/TR/webrtc-stats/#dom-rtcrtpstreamstats-isremote
531 579
         // when isRemote is true indicates that the measurements were done at
532
-        // the remote endpoint and reported in an RTCP RR/XR
580
+        // the remote endpoint and reported in an RTCP RR/XR.
533 581
         // Fixes a problem where we are calculating local stats wrong adding
534
-        // the sent bytes to the local download bitrate
535
-        if (now.isRemote === true) {
582
+        // the sent bytes to the local download bitrate.
583
+        // In new W3 stats spec, type="track" has a remoteSource boolean
584
+        // property.
585
+        if (now.isRemote === true || now.remoteSource === true) {
536 586
             continue;
537 587
         }
538 588
 
@@ -615,8 +665,10 @@ StatsCollector.prototype.processStatsReport = function() {
615 665
             'upload': bitrateSentKbps
616 666
         });
617 667
 
618
-        const resolution = { height: null,
619
-            width: null };
668
+        const resolution = {
669
+            height: null,
670
+            width: null
671
+        };
620 672
 
621 673
         try {
622 674
             let height, width;
@@ -763,6 +815,7 @@ StatsCollector.prototype.processStatsReport = function() {
763 815
         upload:
764 816
             calculatePacketLoss(lostPackets.upload, totalPackets.upload)
765 817
     };
818
+
766 819
     this.eventEmitter.emit(
767 820
         StatisticsEvents.CONNECTION_STATS,
768 821
         this.peerconnection,
@@ -794,12 +847,16 @@ StatsCollector.prototype.processAudioLevelReport = function() {
794 847
 
795 848
         const now = this.currentAudioLevelsReport[idx];
796 849
 
797
-        if (now.type !== 'ssrc') {
850
+        if (now.type !== 'ssrc' && now.type !== 'track') {
798 851
             continue;
799 852
         }
800 853
 
801 854
         const before = this.baselineAudioLevelsReport[idx];
802
-        const ssrc = this.getNonNegativeStat(now, 'ssrc');
855
+        let ssrc = this.getNonNegativeStat(now, 'ssrc');
856
+
857
+        if (!ssrc && Array.isArray(now.ssrcIds)) {
858
+            ssrc = Number(now.ssrcIds[0]);
859
+        }
803 860
 
804 861
         if (!before) {
805 862
             logger.warn(`${ssrc} not enough data`);
@@ -828,11 +885,33 @@ StatsCollector.prototype.processAudioLevelReport = function() {
828 885
         }
829 886
 
830 887
         if (audioLevel) {
831
-            const isLocal = !getStatValue(now, 'packetsReceived');
888
+            let isLocal;
889
+
890
+            // If type="ssrc" (legacy) check whether they are received packets.
891
+            if (now.type === 'ssrc') {
892
+                isLocal = !getStatValue(now, 'packetsReceived');
893
+
894
+            // If type="track", check remoteSource boolean property.
895
+            } else {
896
+                isLocal = !now.remoteSource;
897
+            }
898
+
899
+            // According to the W3C WebRTC Stats spec, audioLevel should be in
900
+            // 0..1 range (0 == silence). However browsers don't behave that
901
+            // way so we must convert it to 0..1.
902
+            //
903
+            // In Edge the range is -100..0 (-100 == silence) measured in dB,
904
+            // so convert to linear. The levels are set to 0 for remote tracks,
905
+            // so don't convert those, since 0 means "the maximum" in Edge.
906
+            if (RTCBrowserType.isEdge()) {
907
+                audioLevel = audioLevel < 0 ? Math.pow(10, audioLevel / 20) : 0;
832 908
 
833 909
             // TODO: Can't find specs about what this value really is, but it
834 910
             // seems to vary between 0 and around 32k.
835
-            audioLevel = audioLevel / 32767;
911
+            } else {
912
+                audioLevel = audioLevel / 32767;
913
+            }
914
+
836 915
             this.eventEmitter.emit(
837 916
                 StatisticsEvents.AUDIO_LEVEL,
838 917
                 this.peerconnection,

Laddar…
Avbryt
Spara