Browse Source

fix(unified-plan): use RTCRtpSender.replaceTrack for mute/unmute/replace operations

Trigger renegotation only when negotiationneeded event is fired on the browser.
Do not disable simulcast for screensharing in unified plan.
Fix the order of the simulcast streams for Firefox
dev1
Jaya Allamsetty 5 years ago
parent
commit
1c790e3049
3 changed files with 532 additions and 226 deletions
  1. 365
    0
      modules/RTC/TPCUtils.js
  2. 66
    156
      modules/RTC/TraceablePeerConnection.js
  3. 101
    70
      modules/xmpp/JingleSessionPC.js

+ 365
- 0
modules/RTC/TPCUtils.js View File

@@ -0,0 +1,365 @@
1
+import { getLogger } from 'jitsi-meet-logger';
2
+import transform from 'sdp-transform';
3
+
4
+import * as JitsiTrackEvents from '../../JitsiTrackEvents';
5
+import browser from '../browser';
6
+import RTCEvents from '../../service/RTC/RTCEvents';
7
+
8
+const logger = getLogger(__filename);
9
+const SIM_LAYER_1_RID = '1';
10
+const SIM_LAYER_2_RID = '2';
11
+const SIM_LAYER_3_RID = '3';
12
+
13
+export const SIM_LAYER_RIDS = [ SIM_LAYER_1_RID, SIM_LAYER_2_RID, SIM_LAYER_3_RID ];
14
+
15
+/**
16
+ * Handles track related operations on TraceablePeerConnection when browser is
17
+ * running in unified plan mode.
18
+ */
19
+export class TPCUtils {
20
+    /**
21
+     * @constructor
22
+     */
23
+    constructor(peerconnection) {
24
+        this.pc = peerconnection;
25
+
26
+        /**
27
+         * The simulcast encodings that will be configured on the RTCRtpSender
28
+         * for the video tracks in the unified plan mode.
29
+         */
30
+        this.simulcastEncodings = [
31
+            {
32
+                active: true,
33
+                maxBitrate: browser.isFirefox() ? 2500000 : 200000,
34
+                rid: SIM_LAYER_1_RID,
35
+                scaleResolutionDownBy: browser.isFirefox() ? 1.0 : 4.0
36
+            },
37
+            {
38
+                active: true,
39
+                maxBitrate: 700000,
40
+                rid: SIM_LAYER_2_RID,
41
+                scaleResolutionDownBy: 2.0
42
+            },
43
+            {
44
+                active: true,
45
+                maxBitrate: browser.isFirefox() ? 200000 : 2500000,
46
+                rid: SIM_LAYER_3_RID,
47
+                scaleResolutionDownBy: browser.isFirefox() ? 4.0 : 1.0
48
+            }
49
+        ];
50
+    }
51
+
52
+    /**
53
+     * Obtains local tracks for given {@link MediaType}.
54
+     * @param {MediaType} mediaType - audio or video.
55
+     * @return {Array<JitsiLocalTrack>} - array containing the local tracks
56
+     * attached to the peerconnection of the given media type.
57
+     */
58
+    _getLocalTracks(mediaType) {
59
+        const tracks = Array.from(this.pc.localTracks.values());
60
+
61
+        return tracks.filter(track => track.getType() === mediaType);
62
+    }
63
+
64
+    /**
65
+     * Obtains stream encodings that need to be configured on the given track.
66
+     * @param {JitsiLocalTrack} localTrack
67
+     */
68
+    _getStreamEncodings(localTrack) {
69
+        if (this.pc.isSimulcastOn() && localTrack.isVideoTrack()) {
70
+            return this.simulcastEncodings;
71
+        }
72
+
73
+        return [ { active: true } ];
74
+    }
75
+
76
+    /**
77
+     * Takes in a *unified plan* offer and inserts the appropriate
78
+     * parameters for adding simulcast receive support.
79
+     * @param {Object} desc - A session description object
80
+     * @param {String} desc.type - the type (offer/answer)
81
+     * @param {String} desc.sdp - the sdp content
82
+     *
83
+     * @return {Object} A session description (same format as above) object
84
+     * with its sdp field modified to advertise simulcast receive support
85
+     */
86
+    _insertUnifiedPlanSimulcastReceive(desc) {
87
+        // a=simulcast line is not needed on browsers where
88
+        // we munge SDP for turning on simulcast. Remove this check
89
+        // when we move to RID/MID based simulcast on all browsers.
90
+        if (browser.usesSdpMungingForSimulcast()) {
91
+            return desc;
92
+        }
93
+        const sdp = transform.parse(desc.sdp);
94
+        const idx = sdp.media.findIndex(mline => mline.type === 'video');
95
+
96
+        if (sdp.media[idx].rids && (sdp.media[idx].simulcast_03 || sdp.media[idx].simulcast)) {
97
+            // Make sure we don't have the simulcast recv line on video descriptions other than the
98
+            // the first video description.
99
+            sdp.media.forEach((mline, i) => {
100
+                if (mline.type === 'video' && i !== idx) {
101
+                    sdp.media[i].rids = undefined;
102
+                    sdp.media[i].simulcast = undefined;
103
+                }
104
+            });
105
+        }
106
+
107
+        // In order of highest to lowest spatial quality
108
+        sdp.media[idx].rids = [
109
+            {
110
+                id: SIM_LAYER_1_RID,
111
+                direction: 'recv'
112
+            },
113
+            {
114
+                id: SIM_LAYER_2_RID,
115
+                direction: 'recv'
116
+            },
117
+            {
118
+                id: SIM_LAYER_3_RID,
119
+                direction: 'recv'
120
+            }
121
+        ];
122
+
123
+        // Firefox 72 has stopped parsing the legacy rid= parameters in simulcast attributes.
124
+        // eslint-disable-next-line max-len
125
+        // https://www.fxsitecompat.dev/en-CA/docs/2019/pt-and-rid-in-webrtc-simulcast-attributes-are-no-longer-supported/
126
+        const simulcastLine = browser.isFirefox() && browser.isVersionGreaterThan(71)
127
+            ? `recv ${SIM_LAYER_RIDS.join(';')}`
128
+            : `recv rid=${SIM_LAYER_RIDS.join(';')}`;
129
+
130
+        // eslint-disable-next-line camelcase
131
+        sdp.media[idx].simulcast_03 = {
132
+            value: simulcastLine
133
+        };
134
+
135
+        return new RTCSessionDescription({
136
+            type: desc.type,
137
+            sdp: transform.write(sdp)
138
+        });
139
+    }
140
+
141
+    /**
142
+    * Adds {@link JitsiLocalTrack} to the WebRTC peerconnection for the first time.
143
+    * @param {JitsiLocalTrack} track - track to be added to the peerconnection.
144
+    * @returns {boolean} Returns true if the operation is successful,
145
+    * false otherwise.
146
+    */
147
+    addTrack(localTrack, isInitiator = true) {
148
+        const track = localTrack.getTrack();
149
+
150
+        if (isInitiator) {
151
+            // Use pc.addTransceiver() for the initiator case when local tracks are getting added
152
+            // to the peerconnection before a session-initiate is sent over to the peer.
153
+            const transceiverInit = {
154
+                direction: 'sendrecv',
155
+                streams: [ localTrack.getOriginalStream() ],
156
+                sendEncodings: []
157
+            };
158
+
159
+            if (!browser.isFirefox()) {
160
+                transceiverInit.sendEncodings = this._getStreamEncodings(localTrack);
161
+            }
162
+            this.pc.peerconnection.addTransceiver(track, transceiverInit);
163
+        } else {
164
+            // Use pc.addTrack() for responder case so that we can re-use the m-lines that were created
165
+            // when setRemoteDescription was called. pc.addTrack() automatically  attaches to any existing
166
+            // unused "recv-only" transceiver.
167
+            this.pc.peerconnection.addTrack(track);
168
+        }
169
+    }
170
+
171
+    /**
172
+     * Adds a track on the RTCRtpSender as part of the unmute operation.
173
+     * @param {JitsiLocalTrack} localTrack - track to be unmuted.
174
+     * @returns {boolean} Returns true if the operation is successful,
175
+     * false otherwise.
176
+     */
177
+    addTrackUnmute(localTrack) {
178
+        const mediaType = localTrack.getType();
179
+        const track = localTrack.getTrack();
180
+
181
+        // The assumption here is that the first transceiver of the specified
182
+        // media type is that of the local track.
183
+        const transceiver = this.pc.peerconnection.getTransceivers()
184
+            .find(t => t.receiver && t.receiver.track && t.receiver.track.kind === mediaType);
185
+
186
+        if (!transceiver) {
187
+            logger.error(`RTCRtpTransceiver for ${mediaType} on ${this.pc} not found`);
188
+
189
+            return false;
190
+        }
191
+        logger.info(`Adding ${localTrack} on ${this.pc}`);
192
+
193
+        // If the client starts with audio/video muted setting, the transceiver direction
194
+        // will be set to 'recvonly'. Use addStream here so that a MSID is generated for the stream.
195
+        if (transceiver.direction === 'recvonly') {
196
+            this.pc.peerconnection.addStream(localTrack.getOriginalStream());
197
+            this.setEncodings(localTrack);
198
+            this.pc.localTracks.set(localTrack.rtcId, localTrack);
199
+            transceiver.direction = 'sendrecv';
200
+
201
+            return true;
202
+        }
203
+        transceiver.sender.replaceTrack(track)
204
+            .then(() => {
205
+                this.pc.localTracks.set(localTrack.rtcId, localTrack);
206
+
207
+                return true;
208
+            })
209
+            .catch(err => {
210
+                logger.error(`Unmute track failed for ${mediaType} track on ${this.pc}, ${err}`);
211
+
212
+                return false;
213
+            });
214
+    }
215
+
216
+    /**
217
+     * Removes the track from the RTCRtpSender as part of the mute operation.
218
+     * @param {JitsiLocalTrack} localTrack - track to be removed.
219
+     * @returns {boolean} Returns true if the operation is successful,
220
+     * false otherwise.
221
+     */
222
+    removeTrackMute(localTrack) {
223
+        const mediaType = localTrack.getType();
224
+        const transceiver = this.pc.peerconnection.getTransceivers()
225
+            .find(t => t.sender && t.sender.track && t.sender.track.id === localTrack.getTrackId());
226
+
227
+        if (!transceiver) {
228
+            logger.error(`RTCRtpTransceiver for ${mediaType} on ${this.pc} not found`);
229
+
230
+            return false;
231
+        }
232
+
233
+        logger.info(`Removing ${localTrack} on ${this.pc}`);
234
+        transceiver.sender.replaceTrack(null)
235
+            .then(() => {
236
+                this.pc.localTracks.delete(localTrack.rtcId);
237
+                this.pc.localSSRCs.delete(localTrack.rtcId);
238
+
239
+                return true;
240
+            })
241
+            .catch(err => {
242
+                logger.error(`Mute track failed for ${mediaType} track on ${this.pc}, ${err}`);
243
+
244
+                return false;
245
+            });
246
+    }
247
+
248
+    /**
249
+     * Replaces the existing track on a RTCRtpSender with the given track.
250
+     * @param {JitsiLocalTrack} oldTrack - existing track on the sender that needs to be removed.
251
+     * @param {JitsiLocalTrack} newTrack - new track that needs to be added to the sender.
252
+     * @returns {Promise<false>} Promise that resolves with false as we don't want
253
+     * renegotiation to be triggered automatically after this operation. Renegotiation is
254
+     * done when the browser fires the negotiationeeded event.
255
+     */
256
+    replaceTrack(oldTrack, newTrack) {
257
+        if (oldTrack && newTrack) {
258
+            const mediaType = newTrack.getType();
259
+            const stream = newTrack.getOriginalStream();
260
+            const track = stream.getVideoTracks()[0];
261
+            const transceiver = this.pc.peerconnection.getTransceivers()
262
+                .find(t => t.receiver.track.kind === mediaType && !t.stopped);
263
+
264
+            if (!transceiver) {
265
+                return Promise.reject(new Error('replace track failed'));
266
+            }
267
+
268
+            return transceiver.sender.replaceTrack(track)
269
+                .then(() => {
270
+                    const ssrc = this.pc.localSSRCs.get(oldTrack.rtcId);
271
+
272
+                    this.pc.localTracks.delete(oldTrack.rtcId);
273
+                    this.pc.localSSRCs.delete(oldTrack.rtcId);
274
+                    this.pc._addedStreams = this.pc._addedStreams.filter(s => s !== stream);
275
+                    this.pc.localTracks.set(newTrack.rtcId, newTrack);
276
+
277
+                    this.pc._addedStreams.push(stream);
278
+                    this.pc.localSSRCs.set(newTrack.rtcId, ssrc);
279
+                    this.pc.eventEmitter.emit(RTCEvents.LOCAL_TRACK_SSRC_UPDATED,
280
+                        newTrack,
281
+                        this.pc._extractPrimarySSRC(ssrc));
282
+                });
283
+        } else if (oldTrack && !newTrack) {
284
+            if (!this.removeTrackMute(oldTrack)) {
285
+                return Promise.reject(new Error('replace track failed'));
286
+            }
287
+            this.pc.localTracks.delete(oldTrack.rtcId);
288
+            this.pc.localSSRCs.delete(oldTrack.rtcId);
289
+        } else if (newTrack && !oldTrack) {
290
+            const ssrc = this.pc.localSSRCs.get(newTrack.rtcId);
291
+
292
+            if (!this.addTrackUnmute(newTrack)) {
293
+                return Promise.reject(new Error('replace track failed'));
294
+            }
295
+            newTrack.emit(JitsiTrackEvents.TRACK_MUTE_CHANGED, newTrack);
296
+            this.pc.localTracks.set(newTrack.rtcId, newTrack);
297
+            this.pc.localSSRCs.set(newTrack.rtcId, ssrc);
298
+        }
299
+
300
+        return Promise.resolve(false);
301
+    }
302
+
303
+    /**
304
+     *
305
+     * @param {boolean} active
306
+     */
307
+    setAudioTransferActive(active) {
308
+        return this.setMediaTransferActive('audio', active);
309
+    }
310
+
311
+    /**
312
+     * Set the simulcast stream encoding properties on the RTCRtpSender.
313
+     * @param {*} track - the current track in use for which the encodings are to be set.
314
+     */
315
+    setEncodings(track) {
316
+        const transceiver = this.pc.peerconnection.getTransceivers()
317
+            .find(t => t.sender && t.sender.track && t.sender.track.kind === track.getType());
318
+        const parameters = transceiver.sender.getParameters();
319
+
320
+        parameters.encodings = this._getStreamEncodings(track);
321
+        transceiver.sender.setParameters(parameters);
322
+    }
323
+
324
+    /**
325
+     *
326
+     * @param {*} mediaType
327
+     * @param {boolean} active
328
+     */
329
+    setMediaTransferActive(mediaType, active) {
330
+        const transceivers = this.pc.peerconnection.getTransceivers()
331
+            .filter(t => t.receiver && t.receiver.track && t.receiver.track.kind === mediaType);
332
+
333
+        if (active) {
334
+            transceivers.forEach(transceiver => {
335
+                if (this._getLocalTracks(mediaType).length > 0) {
336
+                    transceiver.direction = 'sendrecv';
337
+                    const parameters = transceiver.sender.getParameters();
338
+
339
+                    if (parameters && parameters.encodings && parameters.encodings.length) {
340
+                        parameters.encodings.forEach(encoding => {
341
+                            encoding.active = true;
342
+                        });
343
+                        transceiver.sender.setParameters(parameters);
344
+                    }
345
+                } else {
346
+                    transceiver.direction = 'recvonly';
347
+                }
348
+            });
349
+        } else {
350
+            transceivers.forEach(transceiver => {
351
+                transceiver.direction = 'inactive';
352
+            });
353
+        }
354
+
355
+        return true;
356
+    }
357
+
358
+    /**
359
+     *
360
+     * @param {boolean} active
361
+     */
362
+    setVideoTransferActive(active) {
363
+        return this.setMediaTransferActive('video', active);
364
+    }
365
+}

+ 66
- 156
modules/RTC/TraceablePeerConnection.js View File

@@ -5,7 +5,6 @@ import transform from 'sdp-transform';
5 5
 
6 6
 import * as GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
7 7
 import JitsiRemoteTrack from './JitsiRemoteTrack';
8
-import * as JitsiTrackEvents from '../../JitsiTrackEvents';
9 8
 import * as MediaType from '../../service/RTC/MediaType';
10 9
 import LocalSdpMunger from './LocalSdpMunger';
11 10
 import RTC from './RTC';
@@ -13,6 +12,7 @@ import RTCUtils from './RTCUtils';
13 12
 import browser from '../browser';
14 13
 import RTCEvents from '../../service/RTC/RTCEvents';
15 14
 import RtxModifier from '../xmpp/RtxModifier';
15
+import { SIM_LAYER_RIDS, TPCUtils } from './TPCUtils';
16 16
 
17 17
 // FIXME SDP tools should end up in some kind of util module
18 18
 import SDP from '../xmpp/SDP';
@@ -22,12 +22,7 @@ import SDPUtil from '../xmpp/SDPUtil';
22 22
 import * as SignalingEvents from '../../service/RTC/SignalingEvents';
23 23
 
24 24
 const logger = getLogger(__filename);
25
-const SIMULCAST_LAYERS = 3;
26
-const SIM_LAYER_1_RID = '1';
27
-const SIM_LAYER_2_RID = '2';
28
-const SIM_LAYER_3_RID = '3';
29
-const SIM_LAYER_RIDS = [ SIM_LAYER_1_RID, SIM_LAYER_2_RID, SIM_LAYER_3_RID ];
30
-const SIM_LAYER_BITRATES_BPS = [ 200000, 700000, 2500000 ];
25
+const MAX_BITRATE = 2500000;
31 26
 const DESKSTOP_SHARE_RATE = 500000;
32 27
 
33 28
 /* eslint-disable max-params */
@@ -215,6 +210,7 @@ export default function TraceablePeerConnection(
215 210
 
216 211
     this.peerconnection
217 212
         = new RTCUtils.RTCPeerConnectionType(iceConfig, constraints);
213
+    this.tpcUtils = new TPCUtils(this);
218 214
     this.updateLog = [];
219 215
     this.stats = {};
220 216
     this.statsinterval = null;
@@ -229,8 +225,12 @@ export default function TraceablePeerConnection(
229 225
     this.interop = new Interop();
230 226
     const Simulcast = require('@jitsi/sdp-simulcast');
231 227
 
232
-    this.simulcast = new Simulcast({ numOfLayers: SIMULCAST_LAYERS,
233
-        explodeRemoteSimulcast: false });
228
+    this.simulcast = new Simulcast(
229
+        {
230
+            numOfLayers: SIM_LAYER_RIDS.length,
231
+            explodeRemoteSimulcast: false,
232
+            usesUnifiedPlan: browser.usesUnifiedPlan()
233
+        });
234 234
     this.sdpConsistency = new SdpConsistency(this.toString());
235 235
 
236 236
     /**
@@ -1268,7 +1268,7 @@ TraceablePeerConnection.prototype._injectSsrcGroupForUnifiedSimulcast
1268 1268
         const sdp = transform.parse(desc.sdp);
1269 1269
         const video = sdp.media.find(mline => mline.type === 'video');
1270 1270
 
1271
-        if (video.simulcast_03) {
1271
+        if (video.simulcast) {
1272 1272
             const ssrcs = [];
1273 1273
 
1274 1274
             video.ssrcs.forEach(ssrc => {
@@ -1377,7 +1377,7 @@ TraceablePeerConnection.prototype._getSSRC = function(rtcId) {
1377 1377
  * Add {@link JitsiLocalTrack} to this TPC.
1378 1378
  * @param {JitsiLocalTrack} track
1379 1379
  */
1380
-TraceablePeerConnection.prototype.addTrack = function(track) {
1380
+TraceablePeerConnection.prototype.addTrack = function(track, isInitiator = false) {
1381 1381
     const rtcId = track.rtcId;
1382 1382
 
1383 1383
     logger.info(`add ${track} to: ${this}`);
@@ -1389,6 +1389,9 @@ TraceablePeerConnection.prototype.addTrack = function(track) {
1389 1389
     }
1390 1390
 
1391 1391
     this.localTracks.set(rtcId, track);
1392
+    if (browser.usesUnifiedPlan() && isInitiator) {
1393
+        return this.tpcUtils.addTrack(track, isInitiator);
1394
+    }
1392 1395
 
1393 1396
     const webrtcStream = track.getOriginalStream();
1394 1397
 
@@ -1430,6 +1433,9 @@ TraceablePeerConnection.prototype.addTrack = function(track) {
1430 1433
             this.rtxModifier.setSsrcCache(rtxSsrcMapping);
1431 1434
         }
1432 1435
     }
1436
+    if (browser.usesUnifiedPlan() && !browser.usesSdpMungingForSimulcast()) {
1437
+        this.tpcUtils.setEncodings(track);
1438
+    }
1433 1439
 };
1434 1440
 
1435 1441
 /**
@@ -1446,6 +1452,9 @@ TraceablePeerConnection.prototype.addTrackUnmute = function(track) {
1446 1452
     }
1447 1453
 
1448 1454
     logger.info(`Adding ${track} as unmute to ${this}`);
1455
+    if (browser.usesUnifiedPlan()) {
1456
+        return this.tpcUtils.addTrackUnmute(track);
1457
+    }
1449 1458
     const webRtcStream = track.getOriginalStream();
1450 1459
 
1451 1460
     if (!webRtcStream) {
@@ -1525,6 +1534,9 @@ TraceablePeerConnection.prototype.isMediaStreamInPc = function(mediaStream) {
1525 1534
  *       The same applies to addTrack.
1526 1535
  */
1527 1536
 TraceablePeerConnection.prototype.removeTrack = function(localTrack) {
1537
+    if (browser.usesUnifiedPlan()) {
1538
+        return this.tpcUtils.removeTrack(localTrack);
1539
+    }
1528 1540
     const webRtcStream = localTrack.getOriginalStream();
1529 1541
 
1530 1542
     this.trace(
@@ -1590,50 +1602,9 @@ TraceablePeerConnection.prototype.findSenderByStream = function(stream) {
1590 1602
  * renegotiation will be needed. Otherwise no renegotiation is needed.
1591 1603
  */
1592 1604
 TraceablePeerConnection.prototype.replaceTrack = function(oldTrack, newTrack) {
1593
-    if (browser.supportsRtpSender() && oldTrack && newTrack) {
1594
-        // Add and than remove stream in FF leads to wrong local SDP. In order
1595
-        // to workaround the issue we need to use sender.replaceTrack().
1596
-        const sender = this.findSenderByStream(oldTrack.getOriginalStream());
1597
-        const stream = newTrack.getOriginalStream();
1598
-
1599
-        if (sender && stream) {
1600
-            const track = stream.getTracks()[0];
1601
-
1602
-            if (track) {
1603
-                return sender.replaceTrack(track, stream).then(() => {
1604
-                    // Since there is no need to do renegotiations we need to
1605
-                    // fix all ssrc-msid mappings here.
1606
-                    // NOTE: after sender.replaceTrack the sdp will remain the
1607
-                    // same but the stream attach to the new JitsiLocalTrack
1608
-                    // will have different msid. Luckily on FF we are not doing
1609
-                    // all the transformations related to video mute.
1610
-
1611
-                    const ssrc = this.localSSRCs.get(oldTrack.rtcId);
1612
-
1613
-                    this.localTracks.delete(oldTrack.rtcId);
1614
-                    this.localSSRCs.delete(oldTrack.rtcId);
1615
-                    this._addedStreams
1616
-                        = this._addedStreams.filter(s => s !== stream);
1617
-
1618
-                    this.localTracks.set(newTrack.rtcId, newTrack);
1619
-
1620
-                    // Override the msid of JitsiLocalTrack in order to be
1621
-                    // consistent with the SDP values.
1622
-                    newTrack.storedMSID = oldTrack.storedMSID;
1623
-                    this._addedStreams.push(stream);
1624
-
1625
-                    this.localSSRCs.set(newTrack.rtcId, ssrc);
1626
-                    this.eventEmitter.emit(
1627
-                        RTCEvents.LOCAL_TRACK_SSRC_UPDATED,
1628
-                        newTrack,
1629
-                        extractPrimarySSRC(ssrc));
1630
-
1631
-                    return false;
1632
-                });
1633
-            }
1634
-        }
1605
+    if (browser.usesUnifiedPlan()) {
1606
+        return this.tpcUtils.replaceTrack(oldTrack, newTrack);
1635 1607
     }
1636
-
1637 1608
     if (oldTrack) {
1638 1609
         this.removeTrack(oldTrack);
1639 1610
     }
@@ -1644,33 +1615,6 @@ TraceablePeerConnection.prototype.replaceTrack = function(oldTrack, newTrack) {
1644 1615
     return Promise.resolve(true);
1645 1616
 };
1646 1617
 
1647
-/**
1648
- * Replaces the existing media stream from the underlying peerconnection with the new
1649
- * mediastream that has been added to the JitsiLocalTrack. Renegotiation with the remote
1650
- * peer is not needed in this case.
1651
- * @param {JitsiLocalTrack} localTrack - the localtrack whose mediastream has been updated.
1652
- * @return {Promise} - Promise resolved with undefined if the track is replaced,
1653
- * or rejected with <tt>InvalidModificationError<tt> if the track cannot be replaced.
1654
- */
1655
-TraceablePeerConnection.prototype.replaceTrackWithoutOfferAnswer = function(localTrack) {
1656
-    const newTrack = localTrack.stream.getTracks()[0];
1657
-    const sender = this.findSenderByKind(newTrack.kind);
1658
-
1659
-    if (!sender) {
1660
-        return Promise.reject(new Error(`Could not find RTCRtpSender for ${newTrack.kind}`));
1661
-    }
1662
-
1663
-    return sender.replaceTrack(newTrack)
1664
-        .then(() => {
1665
-            this._addedStreams = this._addedStreams.filter(s => s !== localTrack._originalStream);
1666
-            this._addedStreams.push(localTrack.stream);
1667
-            localTrack.emit(JitsiTrackEvents.TRACK_MUTE_CHANGED, localTrack);
1668
-        })
1669
-        .catch(err => {
1670
-            logger.error(`replaceTrackWithoutOfferAnswer - replaceTrack failed for ${newTrack.kind}`, err);
1671
-        });
1672
-};
1673
-
1674 1618
 /**
1675 1619
  * Removes local track as part of the mute operation.
1676 1620
  * @param {JitsiLocalTrack} localTrack the local track to be remove as part of
@@ -1689,7 +1633,9 @@ TraceablePeerConnection.prototype.removeTrackMute = function(localTrack) {
1689 1633
         // Abort - nothing to be done here
1690 1634
         return false;
1691 1635
     }
1692
-
1636
+    if (browser.usesUnifiedPlan()) {
1637
+        return this.tpcUtils.removeTrackMute(localTrack);
1638
+    }
1693 1639
     if (webRtcStream) {
1694 1640
         logger.info(
1695 1641
             `Removing ${localTrack} as mute from ${this}`);
@@ -1896,6 +1842,9 @@ TraceablePeerConnection.prototype.setLocalDescription = function(description) {
1896 1842
  */
1897 1843
 TraceablePeerConnection.prototype.setAudioTransferActive = function(active) {
1898 1844
     logger.debug(`${this} audio transfer active: ${active}`);
1845
+    if (browser.usesUnifiedPlan()) {
1846
+        return this.tpcUtils.setAudioTransferActive(active);
1847
+    }
1899 1848
     const changed = this.audioTransferActive !== active;
1900 1849
 
1901 1850
     this.audioTransferActive = active;
@@ -1903,47 +1852,6 @@ TraceablePeerConnection.prototype.setAudioTransferActive = function(active) {
1903 1852
     return changed;
1904 1853
 };
1905 1854
 
1906
-/**
1907
- * Takes in a *unified plan* offer and inserts the appropriate
1908
- * parameters for adding simulcast receive support.
1909
- * @param {Object} desc - A session description object
1910
- * @param {String} desc.type - the type (offer/answer)
1911
- * @param {String} desc.sdp - the sdp content
1912
- *
1913
- * @return {Object} A session description (same format as above) object
1914
- * with its sdp field modified to advertise simulcast receive support
1915
- */
1916
-TraceablePeerConnection.prototype._insertUnifiedPlanSimulcastReceive
1917
-    = function(desc) {
1918
-        const sdp = transform.parse(desc.sdp);
1919
-        const video = sdp.media.find(mline => mline.type === 'video');
1920
-
1921
-        // In order of lowest to highest spatial quality
1922
-        video.rids = [
1923
-            {
1924
-                id: SIM_LAYER_1_RID,
1925
-                direction: 'recv'
1926
-            },
1927
-            {
1928
-                id: SIM_LAYER_2_RID,
1929
-                direction: 'recv'
1930
-            },
1931
-            {
1932
-                id: SIM_LAYER_3_RID,
1933
-                direction: 'recv'
1934
-            }
1935
-        ];
1936
-        // eslint-disable-next-line camelcase
1937
-        video.simulcast_03 = {
1938
-            value: `recv rid=${SIM_LAYER_RIDS.join(';')}`
1939
-        };
1940
-
1941
-        return new RTCSessionDescription({
1942
-            type: desc.type,
1943
-            sdp: transform.write(sdp)
1944
-        });
1945
-    };
1946
-
1947 1855
 /**
1948 1856
  * Sets the max bitrate on the RTCRtpSender so that the
1949 1857
  * bitrate of the enocder doesn't exceed the configured value.
@@ -1955,9 +1863,13 @@ TraceablePeerConnection.prototype._insertUnifiedPlanSimulcastReceive
1955 1863
 TraceablePeerConnection.prototype.setMaxBitRate = function(localTrack) {
1956 1864
     const mediaType = localTrack.type;
1957 1865
 
1958
-    if (!this.options.capScreenshareBitrate
1959
-        || mediaType === MediaType.AUDIO) {
1960
-
1866
+    // No need to set max bitrates on the streams in the following cases.
1867
+    // 1. When an audio track has been replaced.
1868
+    // 2. When a 'camera' track is replaced in plan-b mode, since its a new sender.
1869
+    // 3. When the config.js option for capping the SS bitrate is not enabled.
1870
+    if ((mediaType === MediaType.AUDIO)
1871
+        || (browser.usesPlanB() && !this.options.capScreenshareBitrate)
1872
+        || (browser.usesPlanB() && localTrack.videoType === 'camera')) {
1961 1873
         return;
1962 1874
     }
1963 1875
     if (!this.peerconnection.getSenders) {
@@ -1974,18 +1886,25 @@ TraceablePeerConnection.prototype.setMaxBitRate = function(localTrack) {
1974 1886
             try {
1975 1887
                 const parameters = sender.getParameters();
1976 1888
 
1977
-                if (parameters.encodings && parameters.encodings.length) {
1978
-                    logger.info('Setting max bitrate on video stream');
1979
-                    for (const encoding in parameters.encodings) {
1980
-                        if (parameters.encodings.hasOwnProperty(encoding)) {
1981
-                            parameters.encodings[encoding].maxBitrate
1982
-                                = videoType === 'desktop'
1983
-                                    ? DESKSTOP_SHARE_RATE
1984
-                                    : SIM_LAYER_BITRATES_BPS[encoding];
1985
-                        }
1889
+                if (!parameters.encodings || !parameters.encodings.length) {
1890
+                    return;
1891
+                }
1892
+                logger.info('Setting max bitrate on video stream');
1893
+                for (const encoding in parameters.encodings) {
1894
+                    if (parameters.encodings.hasOwnProperty(encoding)) {
1895
+                        parameters.encodings[encoding].maxBitrate
1896
+                            = videoType === 'desktop' && browser.usesPlanB()
1897
+                                ? DESKSTOP_SHARE_RATE
1898
+
1899
+                                // In unified plan, simulcast for SS is on by default.
1900
+                                // When simulcast is disabled through a config.js option,
1901
+                                // we cap the bitrate on desktop and camera tracks to 2500 Kbps.
1902
+                                : this.isSimulcastOn()
1903
+                                    ? this.tpcUtils.simulcastEncodings[encoding].maxBitrate
1904
+                                    : MAX_BITRATE;
1986 1905
                     }
1987
-                    sender.setParameters(parameters);
1988 1906
                 }
1907
+                sender.setParameters(parameters);
1989 1908
             } catch (err) {
1990 1909
                 logger.error('Browser does not support getParameters/setParamters '
1991 1910
                     + 'or setting max bitrate on the encodings: ', err);
@@ -2036,7 +1955,7 @@ TraceablePeerConnection.prototype.setRemoteDescription = function(description) {
2036 1955
 
2037 1956
         if (this.isSimulcastOn()) {
2038 1957
             // eslint-disable-next-line no-param-reassign
2039
-            description = this._insertUnifiedPlanSimulcastReceive(description);
1958
+            description = this.tpcUtils._insertUnifiedPlanSimulcastReceive(description);
2040 1959
             this.trace(
2041 1960
                 'setRemoteDescription::postTransform (sim receive)',
2042 1961
                 dumpSDP(description));
@@ -2164,6 +2083,9 @@ TraceablePeerConnection.prototype._injectH264IfNotPresent = function(
2164 2083
  */
2165 2084
 TraceablePeerConnection.prototype.setVideoTransferActive = function(active) {
2166 2085
     logger.debug(`${this} video transfer active: ${active}`);
2086
+    if (browser.usesUnifiedPlan()) {
2087
+        return this.tpcUtils.setVideoTransferActive(active);
2088
+    }
2167 2089
     const changed = this.videoTransferActive !== active;
2168 2090
 
2169 2091
     this.videoTransferActive = active;
@@ -2361,19 +2283,7 @@ TraceablePeerConnection.prototype.createAnswer = function(constraints) {
2361 2283
             = this.peerconnection.getSenders().find(sender =>
2362 2284
                 sender.track !== null && sender.track.kind === 'video');
2363 2285
         const simParams = {
2364
-            encodings: [
2365
-                {
2366
-                    rid: SIM_LAYER_1_RID,
2367
-                    scaleResolutionDownBy: 4
2368
-                },
2369
-                {
2370
-                    rid: SIM_LAYER_2_RID,
2371
-                    scaleResolutionDownBy: 2
2372
-                },
2373
-                {
2374
-                    rid: SIM_LAYER_3_RID
2375
-                }
2376
-            ]
2286
+            encodings: this.tpcUtils.simulcastEncodings
2377 2287
         };
2378 2288
 
2379 2289
         videoSender.setParameters(simParams);
@@ -2458,7 +2368,7 @@ TraceablePeerConnection.prototype._createOfferOrAnswer = function(
2458 2368
             // configure simulcast for camera tracks always and for
2459 2369
             // desktop tracks only when the testing flag for maxbitrates
2460 2370
             // in config.js is disabled.
2461
-            if (this.isSimulcastOn()
2371
+            if (this.isSimulcastOn() && browser.usesSdpMungingForSimulcast()
2462 2372
                 && (!this.options.capScreenshareBitrate
2463 2373
                 || (this.options.capScreenshareBitrate && hasCameraTrack(this)))) {
2464 2374
                 // eslint-disable-next-line no-param-reassign
@@ -2546,7 +2456,7 @@ TraceablePeerConnection.prototype._createOfferOrAnswer = function(
2546 2456
  * @param {TrackSSRCInfo} ssrcObj
2547 2457
  * @return {number|null} the primary SSRC or <tt>null</tt>
2548 2458
  */
2549
-function extractPrimarySSRC(ssrcObj) {
2459
+TraceablePeerConnection.prototype._extractPrimarySSRC = function(ssrcObj) {
2550 2460
     if (ssrcObj && ssrcObj.groups && ssrcObj.groups.length) {
2551 2461
         return ssrcObj.groups[0].ssrcs[0];
2552 2462
     } else if (ssrcObj && ssrcObj.ssrcs && ssrcObj.ssrcs.length) {
@@ -2554,7 +2464,7 @@ function extractPrimarySSRC(ssrcObj) {
2554 2464
     }
2555 2465
 
2556 2466
     return null;
2557
-}
2467
+};
2558 2468
 
2559 2469
 /**
2560 2470
  * Goes over the SSRC map extracted from the latest local description and tries
@@ -2576,8 +2486,8 @@ TraceablePeerConnection.prototype._processLocalSSRCsMap = function(ssrcMap) {
2576 2486
                 return;
2577 2487
             }
2578 2488
             const oldSSRC = this.localSSRCs.get(track.rtcId);
2579
-            const newSSRCNum = extractPrimarySSRC(newSSRC);
2580
-            const oldSSRCNum = extractPrimarySSRC(oldSSRC);
2489
+            const newSSRCNum = this._extractPrimarySSRC(newSSRC);
2490
+            const oldSSRCNum = this._extractPrimarySSRC(oldSSRC);
2581 2491
 
2582 2492
             // eslint-disable-next-line no-negated-condition
2583 2493
             if (newSSRCNum !== oldSSRCNum) {
@@ -2672,7 +2582,7 @@ TraceablePeerConnection.prototype.generateNewStreamSSRCInfo = function(track) {
2672 2582
             ssrcs: [],
2673 2583
             groups: []
2674 2584
         };
2675
-        for (let i = 0; i < SIMULCAST_LAYERS; i++) {
2585
+        for (let i = 0; i < SIM_LAYER_RIDS.length; i++) {
2676 2586
             ssrcInfo.ssrcs.push(SDPUtil.generateSsrc());
2677 2587
         }
2678 2588
         ssrcInfo.groups.push({

+ 101
- 70
modules/xmpp/JingleSessionPC.js View File

@@ -8,6 +8,7 @@ import { getLogger } from 'jitsi-meet-logger';
8 8
 import { $iq, Strophe } from 'strophe.js';
9 9
 import { integerHash } from '../util/StringUtils';
10 10
 
11
+import browser from './../browser';
11 12
 import JingleSession from './JingleSession';
12 13
 import * as JingleSessionState from './JingleSessionState';
13 14
 import SDP from './SDP';
@@ -510,8 +511,44 @@ export default class JingleSessionPC extends JingleSession {
510 511
                 break;
511 512
             }
512 513
         };
514
+
515
+        /**
516
+         * The negotiationneeded event is fired whenever we shake the media on the
517
+         * RTCPeerConnection object.
518
+         */
513 519
         this.peerconnection.onnegotiationneeded = () => {
520
+            const state = this.peerconnection.signalingState;
521
+            const remoteDescription = this.peerconnection.remoteDescription;
522
+
514 523
             this.room.eventEmitter.emit(XMPPEvents.PEERCONNECTION_READY, this);
524
+            if (browser.usesUnifiedPlan() && state.toString() === 'stable'
525
+                && remoteDescription && typeof remoteDescription.sdp === 'string') {
526
+                logger.debug(`onnegotiationneeded fired on ${this.peerconnection} in state: ${state}`);
527
+                const workFunction = finishedCallback => {
528
+                    const oldSdp = new SDP(this.peerconnection.localDescription.sdp);
529
+
530
+                    this._renegotiate()
531
+                        .then(() => {
532
+                            const newSdp = new SDP(this.peerconnection.localDescription.sdp);
533
+
534
+                            this.notifyMySSRCUpdate(oldSdp, newSdp);
535
+                            finishedCallback();
536
+                        })
537
+                        .catch(() => {
538
+                            finishedCallback();
539
+                        });
540
+                };
541
+
542
+                this.modificationQueue.push(
543
+                    workFunction,
544
+                    error => {
545
+                        if (error) {
546
+                            logger.error('onnegotiationneeded error', error);
547
+                        } else {
548
+                            logger.debug('onnegotiationneeded executed - OK');
549
+                        }
550
+                    });
551
+            }
515 552
         };
516 553
 
517 554
         // The signaling layer will bind it's listeners at this point
@@ -861,7 +898,7 @@ export default class JingleSessionPC extends JingleSession {
861 898
         }
862 899
         const workFunction = finishedCallback => {
863 900
             for (const localTrack of localTracks) {
864
-                this.peerconnection.addTrack(localTrack);
901
+                this.peerconnection.addTrack(localTrack, true /* isInitiator */);
865 902
             }
866 903
             this.peerconnection.createOffer(this.mediaConstraints)
867 904
                 .then(offerSdp => {
@@ -1705,19 +1742,6 @@ export default class JingleSessionPC extends JingleSession {
1705 1742
             });
1706 1743
     }
1707 1744
 
1708
-    /**
1709
-     * Replaces the existing mediaStream on the underlying peerconnection with the newly
1710
-     * added stream on the same JitsiLocalTrack wihtout the need to perform a offer/answer
1711
-     * cycle.
1712
-     * @param {JitsiLocalTrack} track - the current track in use whose media stream has been
1713
-     * updated.
1714
-     * @returns {Promise} which resolves once the replacement is complete or reject with an
1715
-     * error {string}.
1716
-     */
1717
-    replaceTrackWithoutOfferAnswer(track) {
1718
-        return this.peerconnection.replaceTrackWithoutOfferAnswer(track);
1719
-    }
1720
-
1721 1745
     /**
1722 1746
      * Replaces <tt>oldTrack</tt> with <tt>newTrack</tt> and performs a single
1723 1747
      * offer/answer cycle after both operations are done. Either
@@ -1734,67 +1758,72 @@ export default class JingleSessionPC extends JingleSession {
1734 1758
         const workFunction = finishedCallback => {
1735 1759
             const oldLocalSdp = this.peerconnection.localDescription.sdp;
1736 1760
 
1737
-            // NOTE the code below assumes that no more than 1 video track
1738
-            // can be added to the peer connection.
1739
-            // Transition from camera to desktop share
1740
-            // or transition from one camera source to another.
1741
-            if (this.peerconnection.options.capScreenshareBitrate
1742
-                && oldTrack && newTrack && newTrack.isVideoTrack()) {
1743
-                // Clearing current primary SSRC will make
1744
-                // the SdpConsistency generate a new one which will result
1745
-                // with:
1746
-                // 1. source-remove for the old video stream.
1747
-                // 2. source-add for the new video stream.
1748
-                this.peerconnection.clearRecvonlySsrc();
1749
-            }
1761
+            if (browser.usesPlanB()) {
1762
+                // NOTE the code below assumes that no more than 1 video track
1763
+                // can be added to the peer connection.
1764
+                // Transition from camera to desktop share
1765
+                // or transition from one camera source to another.
1766
+                if (this.peerconnection.options.capScreenshareBitrate
1767
+                    && oldTrack && newTrack && newTrack.isVideoTrack()) {
1768
+                    // Clearing current primary SSRC will make
1769
+                    // the SdpConsistency generate a new one which will result
1770
+                    // with:
1771
+                    // 1. source-remove for the old video stream.
1772
+                    // 2. source-add for the new video stream.
1773
+                    this.peerconnection.clearRecvonlySsrc();
1774
+                }
1750 1775
 
1751
-            // Transition from no video to video (unmute).
1752
-            if (!oldTrack && newTrack && newTrack.isVideoTrack()) {
1753
-                // Clearing current primary SSRC will make
1754
-                // the SdpConsistency generate a new one which will result
1755
-                // with:
1756
-                // 1. source-remove for the recvonly
1757
-                // 2. source-add for the new video stream
1758
-                this.peerconnection.clearRecvonlySsrc();
1759
-
1760
-            // Transition from video to no video
1761
-            } else if (oldTrack && oldTrack.isVideoTrack() && !newTrack) {
1762
-                // Clearing current primary SSRC and generating the recvonly
1763
-                // will result in:
1764
-                // 1. source-remove for the old video stream
1765
-                // 2. source-add for the recvonly stream
1766
-                this.peerconnection.clearRecvonlySsrc();
1767
-                this.peerconnection.generateRecvonlySsrc();
1776
+                // Transition from no video to video (unmute).
1777
+                if (!oldTrack && newTrack && newTrack.isVideoTrack()) {
1778
+                    // Clearing current primary SSRC will make
1779
+                    // the SdpConsistency generate a new one which will result
1780
+                    // with:
1781
+                    // 1. source-remove for the recvonly
1782
+                    // 2. source-add for the new video stream
1783
+                    this.peerconnection.clearRecvonlySsrc();
1784
+
1785
+                // Transition from video to no video
1786
+                } else if (oldTrack && oldTrack.isVideoTrack() && !newTrack) {
1787
+                    // Clearing current primary SSRC and generating the recvonly
1788
+                    // will result in:
1789
+                    // 1. source-remove for the old video stream
1790
+                    // 2. source-add for the recvonly stream
1791
+                    this.peerconnection.clearRecvonlySsrc();
1792
+                    this.peerconnection.generateRecvonlySsrc();
1793
+                }
1768 1794
             }
1769 1795
 
1770 1796
             this.peerconnection.replaceTrack(oldTrack, newTrack)
1771
-            .then(shouldRenegotiate => {
1772
-                if (shouldRenegotiate
1773
-                    && (oldTrack || newTrack)
1774
-                    && this.state === JingleSessionState.ACTIVE) {
1775
-                    this._renegotiate()
1776
-                        .then(() => {
1777
-                            const newLocalSDP
1778
-                                = new SDP(
1779
-                                    this.peerconnection.localDescription.sdp);
1780
-
1781
-                            this.notifyMySSRCUpdate(
1782
-                                new SDP(oldLocalSdp), newLocalSDP);
1783
-
1784
-                            // configure max bitrate only when media is routed
1785
-                            // through JVB. For p2p case, browser takes care of
1786
-                            // adjusting the uplink based on the feedback it
1787
-                            // gets from the peer.
1788
-                            if (newTrack && !this.isP2P) {
1789
-                                this.peerconnection.setMaxBitRate(newTrack);
1790
-                            }
1791
-                            finishedCallback();
1797
+                .then(shouldRenegotiate => {
1798
+                    let promise = Promise.resolve();
1799
+
1800
+                    if (shouldRenegotiate
1801
+                        && (oldTrack || newTrack)
1802
+                        && this.state === JingleSessionState.ACTIVE) {
1803
+                        promise = this._renegotiate().then(() => {
1804
+                            const newLocalSDP = new SDP(this.peerconnection.localDescription.sdp);
1805
+
1806
+                            this.notifyMySSRCUpdate(new SDP(oldLocalSdp), newLocalSDP);
1792 1807
                         },
1793 1808
                         finishedCallback /* will be called with en error */);
1794
-                } else {
1795
-                    finishedCallback();
1796
-                }
1797
-            });
1809
+                    }
1810
+
1811
+                    // Wait for the renegotation to be done if needed (plan-b) before adjusting
1812
+                    // the max bitrates on the video sender.
1813
+                    promise.then(() => {
1814
+                        // configure max bitrate only when media is routed
1815
+                        // through JVB. For p2p case, browser takes care of
1816
+                        // adjusting the uplink based on the feedback it
1817
+                        // gets from the peer.
1818
+                        if (newTrack && !this.isP2P) {
1819
+                            this.peerconnection.setMaxBitRate(newTrack);
1820
+                        }
1821
+                        finishedCallback();
1822
+                    }, finishedCallback /* will be called with en error */);
1823
+                })
1824
+                .catch(err => {
1825
+                    finishedCallback(err);
1826
+                });
1798 1827
         };
1799 1828
 
1800 1829
         return new Promise((resolve, reject) => {
@@ -1981,7 +2010,9 @@ export default class JingleSessionPC extends JingleSession {
1981 2010
 
1982 2011
             if (!tpcOperation()) {
1983 2012
                 finishedCallback(`${operationName} failed!`);
1984
-            } else if (!oldLocalSDP || !tpc.remoteDescription.sdp) {
2013
+
2014
+            // Do not renegotiate when browser is running in Unified-plan mode.
2015
+            } else if (!oldLocalSDP || !tpc.remoteDescription.sdp || browser.usesUnifiedPlan()) {
1985 2016
                 finishedCallback();
1986 2017
             } else {
1987 2018
                 this._renegotiate()

Loading…
Cancel
Save