Bläddra i källkod

feat(SDP) Convert SDP->Jingle directly w/o sdp-interop layer.

* ref(SDPDiffer) Convert to ES6 class.
Make it work directly with unified plan SDP that has multiple m-lines and add more unit tests.

* ref(xmpp) translate unified-plan SDP->Jingle directly.
Without having to run it through the SDPInterop.toPlanB cycle.

* ref(SDP) Always generate the MSID for signaling it to Jicofo.

* fix(SDPDiffer) Check explicitly for ssrc changes

* fix(SDP): Fix comments and cleanup.
Remove LOCAL_TRACK_SSRC_UPDATED event as the application ignores the event and no additional action needs to be taken when that event is fired.

* ref(SDP) Add a helper function for parsing the 'a=ssrc-group' line.

* squash: Address review comments
release-8443
Jaya Allamsetty 11 månader sedan
förälder
incheckning
a7476b126c
Inget konto är kopplat till bidragsgivarens mejladress

+ 10
- 3
modules/RTC/TPCUtils.js Visa fil

360
 
360
 
361
     /**
361
     /**
362
     * Adds {@link JitsiLocalTrack} to the WebRTC peerconnection for the first time.
362
     * Adds {@link JitsiLocalTrack} to the WebRTC peerconnection for the first time.
363
+    *
363
     * @param {JitsiLocalTrack} track - track to be added to the peerconnection.
364
     * @param {JitsiLocalTrack} track - track to be added to the peerconnection.
364
     * @param {boolean} isInitiator - boolean that indicates if the endpoint is offerer in a p2p connection.
365
     * @param {boolean} isInitiator - boolean that indicates if the endpoint is offerer in a p2p connection.
365
-    * @returns {void}
366
+    * @returns {RTCRtpTransceiver} - the transceiver that the track was added to.
366
     */
367
     */
367
     addTrack(localTrack, isInitiator) {
368
     addTrack(localTrack, isInitiator) {
368
         const track = localTrack.getTrack();
369
         const track = localTrack.getTrack();
370
+        let transceiver;
369
 
371
 
370
         if (isInitiator) {
372
         if (isInitiator) {
371
             const streams = [];
373
             const streams = [];
385
             if (!browser.isFirefox()) {
387
             if (!browser.isFirefox()) {
386
                 transceiverInit.sendEncodings = this._getStreamEncodings(localTrack);
388
                 transceiverInit.sendEncodings = this._getStreamEncodings(localTrack);
387
             }
389
             }
388
-            this.pc.peerconnection.addTransceiver(track, transceiverInit);
390
+            transceiver = this.pc.peerconnection.addTransceiver(track, transceiverInit);
389
         } else {
391
         } else {
390
             // Use pc.addTrack() for responder case so that we can re-use the m-lines that were created
392
             // Use pc.addTrack() for responder case so that we can re-use the m-lines that were created
391
             // when setRemoteDescription was called. pc.addTrack() automatically  attaches to any existing
393
             // when setRemoteDescription was called. pc.addTrack() automatically  attaches to any existing
392
             // unused "recv-only" transceiver.
394
             // unused "recv-only" transceiver.
393
-            this.pc.peerconnection.addTrack(track);
395
+            const sender = this.pc.peerconnection.addTrack(track);
396
+
397
+            // Find the corresponding transceiver that the track was attached to.
398
+            transceiver = this.pc.peerconnection.getTransceivers().find(t => t.sender === sender);
394
         }
399
         }
400
+
401
+        return transceiver;
395
     }
402
     }
396
 
403
 
397
     /**
404
     /**

+ 95
- 168
modules/RTC/TraceablePeerConnection.js Visa fil

327
      */
327
      */
328
     this._localTrackTransceiverMids = new Map();
328
     this._localTrackTransceiverMids = new Map();
329
 
329
 
330
+    /**
331
+     * Holds the SSRC map for the local tracks.
332
+     *
333
+     * @type {Map<string, TPCSSRCInfo>}
334
+     */
335
+    this._localSsrcMap = null;
336
+
330
     // override as desired
337
     // override as desired
331
     this.trace = (what, info) => {
338
     this.trace = (what, info) => {
332
         logger.trace(what, info);
339
         logger.trace(what, info);
1123
 };
1130
 };
1124
 
1131
 
1125
 /**
1132
 /**
1126
- * Returns a map with keys msid/mediaType and <tt>TrackSSRCInfo</tt> values.
1127
- * @param {RTCSessionDescription} desc the local description.
1128
- * @return {Map<string,TrackSSRCInfo>}
1133
+ * Processes the local SDP and creates an SSRC map for every local track.
1134
+ *
1135
+ * @param {string} localSDP - SDP from the local description.
1136
+ * @returns {void}
1129
  */
1137
  */
1130
-TraceablePeerConnection.prototype._extractSSRCMap = function(desc) {
1131
-    /**
1132
-     * Track SSRC infos mapped by stream ID (msid) or mediaType (unified-plan)
1133
-     * @type {Map<string,TrackSSRCInfo>}
1134
-     */
1138
+TraceablePeerConnection.prototype._processAndExtractSourceInfo = function(localSDP) {
1135
     const ssrcMap = new Map();
1139
     const ssrcMap = new Map();
1136
 
1140
 
1137
-    /**
1138
-     * Groups mapped by primary SSRC number
1139
-     * @type {Map<number,Array<SSRCGroupInfo>>}
1140
-     */
1141
-    const groupsMap = new Map();
1142
-
1143
-    if (typeof desc !== 'object' || desc === null
1144
-        || typeof desc.sdp !== 'string') {
1145
-        logger.warn('An empty description was passed as an argument');
1146
-
1147
-        return ssrcMap;
1141
+    if (!localSDP || typeof localSDP !== 'string') {
1142
+        throw new Error('Local SDP must be a valid string, aborting!!');
1148
     }
1143
     }
1144
+    const session = transform.parse(localSDP);
1145
+    const media = session.media.filter(mline => mline.direction === MediaDirection.SENDONLY
1146
+        || mline.direction === MediaDirection.SENDRECV);
1149
 
1147
 
1150
-    const session = transform.parse(desc.sdp);
1148
+    if (!Array.isArray(media)) {
1149
+        this._localSsrcMap = ssrcMap;
1151
 
1150
 
1152
-    if (!Array.isArray(session.media)) {
1153
-        return ssrcMap;
1151
+        return;
1154
     }
1152
     }
1155
 
1153
 
1156
-    let media = session.media;
1157
-
1158
-    media = media.filter(mline => mline.direction === MediaDirection.SENDONLY
1159
-        || mline.direction === MediaDirection.SENDRECV);
1154
+    for (const localTrack of this.localTracks.values()) {
1155
+        const sourceName = localTrack.getSourceName();
1156
+        const trackIndex = getSourceIndexFromSourceName(sourceName);
1157
+        const mediaType = localTrack.getType();
1158
+        const mLines = media.filter(m => m.type === mediaType);
1159
+        const ssrcGroups = mLines[trackIndex].ssrcGroups;
1160
+        let ssrcs = mLines[trackIndex].ssrcs;
1161
+
1162
+        if (ssrcs?.length) {
1163
+            // Filter the ssrcs with 'cname' attribute.
1164
+            ssrcs = ssrcs.filter(s => s.attribute === 'cname');
1165
+
1166
+            const msid = `${this.rtc.getLocalEndpointId()}-${mediaType}-${trackIndex}`;
1167
+            const ssrcInfo = {
1168
+                ssrcs: [],
1169
+                groups: [],
1170
+                msid
1171
+            };
1160
 
1172
 
1161
-    let index = 0;
1173
+            ssrcs.forEach(ssrc => ssrcInfo.ssrcs.push(ssrc.id));
1162
 
1174
 
1163
-    for (const mLine of media) {
1164
-        if (!Array.isArray(mLine.ssrcs)) {
1165
-            continue; // eslint-disable-line no-continue
1166
-        }
1175
+            if (ssrcGroups?.length) {
1176
+                for (const group of ssrcGroups) {
1177
+                    group.ssrcs = group.ssrcs.split(' ').map(ssrcStr => parseInt(ssrcStr, 10));
1178
+                    ssrcInfo.groups.push(group);
1179
+                }
1167
 
1180
 
1168
-        if (Array.isArray(mLine.ssrcGroups)) {
1169
-            for (const group of mLine.ssrcGroups) {
1170
-                if (typeof group.semantics !== 'undefined' && typeof group.ssrcs !== 'undefined') {
1171
-                    // Parse SSRCs and store as numbers
1172
-                    const groupSSRCs = group.ssrcs.split(' ').map(ssrcStr => parseInt(ssrcStr, 10));
1173
-                    const primarySSRC = groupSSRCs[0];
1181
+                const simGroup = ssrcGroups.find(group => group.semantics === 'SIM');
1174
 
1182
 
1175
-                    // Note that group.semantics is already present
1176
-                    group.ssrcs = groupSSRCs;
1183
+                // Add a SIM group if its missing in the description (happens on Firefox).
1184
+                if (this.isSpatialScalabilityOn() && !simGroup) {
1185
+                    const groupSsrcs = ssrcGroups.map(group => group.ssrcs[0]);
1177
 
1186
 
1178
-                    // eslint-disable-next-line max-depth
1179
-                    if (!groupsMap.has(primarySSRC)) {
1180
-                        groupsMap.set(primarySSRC, []);
1181
-                    }
1182
-                    groupsMap.get(primarySSRC).push(group);
1187
+                    ssrcInfo.groups.push({
1188
+                        semantics: 'SIM',
1189
+                        ssrcs: groupSsrcs
1190
+                    });
1183
                 }
1191
                 }
1184
             }
1192
             }
1185
 
1193
 
1186
-            const simGroup = mLine.ssrcGroups.find(group => group.semantics === 'SIM');
1194
+            ssrcMap.set(sourceName, ssrcInfo);
1187
 
1195
 
1188
-            // Add a SIM group if its missing in the description (happens on Firefox).
1189
-            if (!simGroup) {
1190
-                const groupSsrcs = mLine.ssrcGroups.map(group => group.ssrcs[0]);
1196
+            const oldSsrcInfo = this.localSSRCs.get(localTrack.rtcId);
1197
+            const oldSsrc = this._extractPrimarySSRC(oldSsrcInfo);
1198
+            const newSsrc = this._extractPrimarySSRC(ssrcInfo);
1191
 
1199
 
1192
-                groupsMap.get(groupSsrcs[0]).push({
1193
-                    semantics: 'SIM',
1194
-                    ssrcs: groupSsrcs
1195
-                });
1200
+            if (oldSsrc !== newSsrc) {
1201
+                oldSsrc && logger.debug(`${this} Overwriting SSRC for track=${localTrack}] with ssrc=${newSsrc}`);
1202
+                this.localSSRCs.set(localTrack.rtcId, ssrcInfo);
1203
+                localTrack.setSsrc(newSsrc);
1196
             }
1204
             }
1197
         }
1205
         }
1198
-
1199
-        let ssrcs = mLine.ssrcs;
1200
-
1201
-        // Filter the ssrcs with 'cname' attribute.
1202
-        ssrcs = ssrcs.filter(s => s.attribute === 'cname');
1203
-
1204
-        for (const ssrc of ssrcs) {
1205
-            // Use the mediaType as key for the source map for unified plan clients since msids are not part of
1206
-            // the standard and the unified plan SDPs do not have a proper msid attribute for the sources.
1207
-            // Also the ssrcs for sources do not change for Unified plan clients since RTCRtpSender#replaceTrack is
1208
-            // used for switching the tracks so it is safe to use the mediaType as the key for the TrackSSRCInfo map.
1209
-            const key = `${mLine.type}-${index}`;
1210
-            const ssrcNumber = ssrc.id;
1211
-            let ssrcInfo = ssrcMap.get(key);
1212
-
1213
-            if (!ssrcInfo) {
1214
-                ssrcInfo = {
1215
-                    ssrcs: [],
1216
-                    groups: [],
1217
-                    msid: key
1218
-                };
1219
-                ssrcMap.set(key, ssrcInfo);
1220
-            }
1221
-            ssrcInfo.ssrcs.push(ssrcNumber);
1222
-
1223
-            if (groupsMap.has(ssrcNumber)) {
1224
-                const ssrcGroups = groupsMap.get(ssrcNumber);
1225
-
1226
-                for (const group of ssrcGroups) {
1227
-                    ssrcInfo.groups.push(group);
1228
-                }
1229
-            }
1230
-        }
1231
-
1232
-        // Currently multi-stream is supported for video only.
1233
-        mLine.type === MediaType.VIDEO && index++;
1234
     }
1206
     }
1235
-
1236
-    return ssrcMap;
1207
+    this._localSsrcMap = ssrcMap;
1237
 };
1208
 };
1238
 
1209
 
1239
 /**
1210
 /**
1322
 
1293
 
1323
         // For a jvb connection, transform the SDP to Plan B first.
1294
         // For a jvb connection, transform the SDP to Plan B first.
1324
         if (!this.isP2P) {
1295
         if (!this.isP2P) {
1325
-            desc = this.interop.toPlanB(desc);
1326
-            this.trace('getLocalDescription::postTransform (Plan B)', dumpSDP(desc));
1327
-
1328
             desc = this._injectSsrcGroupForUnifiedSimulcast(desc);
1296
             desc = this._injectSsrcGroupForUnifiedSimulcast(desc);
1329
             this.trace('getLocalDescription::postTransform (inject ssrc group)', dumpSDP(desc));
1297
             this.trace('getLocalDescription::postTransform (inject ssrc group)', dumpSDP(desc));
1330
         }
1298
         }
1331
 
1299
 
1332
         // See the method's doc for more info about this transformation.
1300
         // See the method's doc for more info about this transformation.
1333
-        desc = this.localSdpMunger.transformStreamIdentifiers(desc);
1301
+        desc = this.localSdpMunger.transformStreamIdentifiers(desc, this._localSsrcMap);
1334
 
1302
 
1335
         return desc;
1303
         return desc;
1336
     },
1304
     },
1505
  */
1473
  */
1506
 TraceablePeerConnection.prototype.addTrack = function(track, isInitiator = false) {
1474
 TraceablePeerConnection.prototype.addTrack = function(track, isInitiator = false) {
1507
     const rtcId = track.rtcId;
1475
     const rtcId = track.rtcId;
1476
+    let transceiver;
1508
 
1477
 
1509
     logger.info(`${this} adding ${track}`);
1478
     logger.info(`${this} adding ${track}`);
1510
     if (this.localTracks.has(rtcId)) {
1479
     if (this.localTracks.has(rtcId)) {
1516
     const webrtcStream = track.getOriginalStream();
1485
     const webrtcStream = track.getOriginalStream();
1517
 
1486
 
1518
     try {
1487
     try {
1519
-        this.tpcUtils.addTrack(track, isInitiator);
1520
-        if (track) {
1521
-            if (track.isAudioTrack()) {
1522
-                this._hasHadAudioTrack = true;
1523
-            } else {
1524
-                this._hasHadVideoTrack = true;
1525
-            }
1526
-        }
1488
+        transceiver = this.tpcUtils.addTrack(track, isInitiator);
1527
     } catch (error) {
1489
     } catch (error) {
1528
         logger.error(`${this} Adding track=${track} failed: ${error?.message}`);
1490
         logger.error(`${this} Adding track=${track} failed: ${error?.message}`);
1529
 
1491
 
1530
         return Promise.reject(error);
1492
         return Promise.reject(error);
1531
     }
1493
     }
1532
 
1494
 
1495
+    if (transceiver?.mid) {
1496
+        this._localTrackTransceiverMids.set(track.rtcId, transceiver.mid.toString());
1497
+    }
1498
+
1499
+    if (track) {
1500
+        if (track.isAudioTrack()) {
1501
+            this._hasHadAudioTrack = true;
1502
+        } else {
1503
+            this._hasHadVideoTrack = true;
1504
+        }
1505
+    }
1506
+
1533
     let promiseChain = Promise.resolve();
1507
     let promiseChain = Promise.resolve();
1534
 
1508
 
1535
     // On Firefox, the encodings have to be configured on the sender only after the transceiver is created.
1509
     // On Firefox, the encodings have to be configured on the sender only after the transceiver is created.
1643
  * @returns {Array}
1617
  * @returns {Array}
1644
  */
1618
  */
1645
 TraceablePeerConnection.prototype.getConfiguredVideoCodecs = function(description) {
1619
 TraceablePeerConnection.prototype.getConfiguredVideoCodecs = function(description) {
1646
-    const currentSdp = description?.sdp ?? this.peerconnection.localDescription?.sdp;
1620
+    const currentSdp = description?.sdp ?? this.localDescription?.sdp;
1647
 
1621
 
1648
     if (!currentSdp) {
1622
     if (!currentSdp) {
1649
         return [];
1623
         return [];
1746
  * @returns {void}
1720
  * @returns {void}
1747
  */
1721
  */
1748
 TraceablePeerConnection.prototype.processLocalSdpForTransceiverInfo = function(localTracks) {
1722
 TraceablePeerConnection.prototype.processLocalSdpForTransceiverInfo = function(localTracks) {
1749
-    const localSdp = this.peerconnection.localDescription?.sdp;
1723
+    const localSdp = this.localDescription?.sdp;
1750
 
1724
 
1751
     if (!localSdp) {
1725
     if (!localSdp) {
1752
         return;
1726
         return;
1817
                 }
1791
                 }
1818
             }
1792
             }
1819
 
1793
 
1820
-            if (transceiver) {
1821
-                // In the scenario where we remove the oldTrack (oldTrack is not null and newTrack is null) on FF
1822
-                // if we change the direction to RECVONLY, create answer will generate SDP with only 1 receive
1823
-                // only ssrc instead of keeping all 6 ssrcs that we currently have. Stopping the screen sharing
1824
-                // and then starting it again will trigger 2 rounds of source-remove and source-add replacing
1825
-                // the 6 ssrcs for the screen sharing with 1 receive only ssrc and then removing the receive
1826
-                // only ssrc and adding the same 6 ssrcs. On the remote participant's side the same ssrcs will
1827
-                // be reused on a new m-line and if the remote participant is FF due to
1828
-                // https://bugzilla.mozilla.org/show_bug.cgi?id=1768729 the video stream won't be rendered.
1829
-                // That's why we need keep the direction to SENDRECV for FF.
1830
-                //
1831
-                // NOTE: If we return back to the approach of not removing the track for FF and instead using the
1832
-                // enabled property for mute or stopping screensharing we may need to change the direction to
1833
-                // RECVONLY if FF still sends the media even though the enabled flag is set to false.
1834
-                transceiver.direction
1835
-                    = newTrack || browser.isFirefox() ? MediaDirection.SENDRECV : MediaDirection.RECVONLY;
1836
-            }
1794
+            // In the scenario where we remove the oldTrack (oldTrack is not null and newTrack is null) on FF
1795
+            // if we change the direction to RECVONLY, create answer will generate SDP with only 1 receive
1796
+            // only ssrc instead of keeping all 6 ssrcs that we currently have. Stopping the screen sharing
1797
+            // and then starting it again will trigger 2 rounds of source-remove and source-add replacing
1798
+            // the 6 ssrcs for the screen sharing with 1 receive only ssrc and then removing the receive
1799
+            // only ssrc and adding the same 6 ssrcs. On the remote participant's side the same ssrcs will
1800
+            // be reused on a new m-line and if the remote participant is FF due to
1801
+            // https://bugzilla.mozilla.org/show_bug.cgi?id=1768729 the video stream won't be rendered.
1802
+            // That's why we need keep the direction to SENDRECV for FF.
1803
+            //
1804
+            // NOTE: If we return back to the approach of not removing the track for FF and instead using the
1805
+            // enabled property for mute or stopping screensharing we may need to change the direction to
1806
+            // RECVONLY if FF still sends the media even though the enabled flag is set to false.
1807
+            transceiver.direction
1808
+                = newTrack || browser.isFirefox() ? MediaDirection.SENDRECV : MediaDirection.RECVONLY;
1837
 
1809
 
1838
             // Avoid re-configuring the encodings on Chromium/Safari, this is needed only on Firefox.
1810
             // Avoid re-configuring the encodings on Chromium/Safari, this is needed only on Firefox.
1839
             const configureEncodingsPromise
1811
             const configureEncodingsPromise
2599
     return this._createOfferOrAnswer(true /* offer */, constraints);
2571
     return this._createOfferOrAnswer(true /* offer */, constraints);
2600
 };
2572
 };
2601
 
2573
 
2602
-TraceablePeerConnection.prototype._createOfferOrAnswer = function(
2603
-        isOffer,
2604
-        constraints) {
2574
+TraceablePeerConnection.prototype._createOfferOrAnswer = function(isOffer, constraints) {
2605
     const logName = isOffer ? 'Offer' : 'Answer';
2575
     const logName = isOffer ? 'Offer' : 'Answer';
2606
 
2576
 
2607
     this.trace(`create${logName}`, JSON.stringify(constraints, null, ' '));
2577
     this.trace(`create${logName}`, JSON.stringify(constraints, null, ' '));
2631
                     dumpSDP(resultSdp));
2601
                     dumpSDP(resultSdp));
2632
             }
2602
             }
2633
 
2603
 
2634
-            const ssrcMap = this._extractSSRCMap(resultSdp);
2635
-
2636
-            this._processLocalSSRCsMap(ssrcMap);
2604
+            this._processAndExtractSourceInfo(resultSdp.sdp);
2637
 
2605
 
2638
             resolveFn(resultSdp);
2606
             resolveFn(resultSdp);
2639
         } catch (e) {
2607
         } catch (e) {
2720
     return null;
2688
     return null;
2721
 };
2689
 };
2722
 
2690
 
2723
-/**
2724
- * Goes over the SSRC map extracted from the latest local description and tries
2725
- * to match them with the local tracks (by MSID). Will update the values
2726
- * currently stored in the {@link TraceablePeerConnection.localSSRCs} map.
2727
- * @param {Map<string,TrackSSRCInfo>} ssrcMap
2728
- * @private
2729
- */
2730
-TraceablePeerConnection.prototype._processLocalSSRCsMap = function(ssrcMap) {
2731
-    for (const track of this.localTracks.values()) {
2732
-        const sourceName = track.getSourceName();
2733
-        const sourceIndex = getSourceIndexFromSourceName(sourceName);
2734
-        const sourceIdentifier = `${track.getType()}-${sourceIndex}`;
2735
-
2736
-        if (ssrcMap.has(sourceIdentifier)) {
2737
-            const newSSRC = ssrcMap.get(sourceIdentifier);
2738
-
2739
-            if (!newSSRC) {
2740
-                logger.error(`${this} No SSRC found for stream=${sourceIdentifier}`);
2741
-
2742
-                return;
2743
-            }
2744
-            const oldSSRC = this.localSSRCs.get(track.rtcId);
2745
-            const newSSRCNum = this._extractPrimarySSRC(newSSRC);
2746
-            const oldSSRCNum = this._extractPrimarySSRC(oldSSRC);
2747
-
2748
-            // eslint-disable-next-line no-negated-condition
2749
-            if (newSSRCNum !== oldSSRCNum) {
2750
-                oldSSRCNum && logger.error(`${this} Overwriting SSRC for track=${track}] with ssrc=${newSSRC}`);
2751
-                this.localSSRCs.set(track.rtcId, newSSRC);
2752
-                track.setSsrc(newSSRCNum);
2753
-                this.eventEmitter.emit(RTCEvents.LOCAL_TRACK_SSRC_UPDATED, track, newSSRCNum);
2754
-            }
2755
-        } else if (!track.isVideoTrack() && !track.isMuted()) {
2756
-            // It is normal to find no SSRCs for a muted video track in
2757
-            // the local SDP as the recv-only SSRC is no longer munged in.
2758
-            // So log the warning only if it's not a muted video track.
2759
-            logger.warn(`${this} No SSRCs found in the local SDP for track=${track}, stream=${sourceIdentifier}`);
2760
-        }
2761
-    }
2762
-};
2763
-
2764
 /**
2691
 /**
2765
  * Track the SSRCs seen so far.
2692
  * Track the SSRCs seen so far.
2766
  * @param {number} ssrc - SSRC.
2693
  * @param {number} ssrc - SSRC.

+ 71
- 165
modules/sdp/LocalSdpMunger.js Visa fil

1
-import { getLogger } from '@jitsi/logger';
1
+import { isEqual } from 'lodash-es';
2
 
2
 
3
 import { MediaDirection } from '../../service/RTC/MediaDirection';
3
 import { MediaDirection } from '../../service/RTC/MediaDirection';
4
 import { MediaType } from '../../service/RTC/MediaType';
4
 import { MediaType } from '../../service/RTC/MediaType';
5
-import { getSourceNameForJitsiTrack } from '../../service/RTC/SignalingLayer';
6
 import browser from '../browser';
5
 import browser from '../browser';
7
 
6
 
8
 import { SdpTransformWrap } from './SdpTransformUtil';
7
 import { SdpTransformWrap } from './SdpTransformUtil';
9
 
8
 
10
-const logger = getLogger(__filename);
11
-
12
 /**
9
 /**
13
- * Fakes local SDP exposed to {@link JingleSessionPC} through the local
14
- * description getter. Modifies the SDP, so that it will contain muted local
15
- * video tracks description, even though their underlying {MediaStreamTrack}s
16
- * are no longer in the WebRTC peerconnection. That prevents from SSRC updates
17
- * being sent to Jicofo/remote peer and prevents sRD/sLD cycle on the remote
18
- * side.
10
+ * Fakes local SDP exposed to {@link JingleSessionPC} through the local description getter. Modifies the SDP, so that
11
+ * the stream identifiers are unique across all of the local PeerConnections and that the source names and video types
12
+ * are injected so that Jicofo can use them to identify the sources.
19
  */
13
  */
20
 export default class LocalSdpMunger {
14
 export default class LocalSdpMunger {
21
 
15
 
28
     constructor(tpc, localEndpointId) {
22
     constructor(tpc, localEndpointId) {
29
         this.tpc = tpc;
23
         this.tpc = tpc;
30
         this.localEndpointId = localEndpointId;
24
         this.localEndpointId = localEndpointId;
31
-        this.audioSourcesToMsidMap = new Map();
32
-        this.videoSourcesToMsidMap = new Map();
33
-    }
34
-
35
-    /**
36
-     * Returns a string that can be set as the MSID attribute for a source.
37
-     *
38
-     * @param {string} mediaType - Media type of the source.
39
-     * @param {string} trackId - Id of the MediaStreamTrack associated with the source.
40
-     * @param {string} streamId - Id of the MediaStream associated with the source.
41
-     * @returns {string|null}
42
-     */
43
-    _generateMsidAttribute(mediaType, trackId, streamId) {
44
-        if (!(mediaType && trackId)) {
45
-            logger.error(`Unable to munge local MSID - track id=${trackId} or media type=${mediaType} is missing`);
46
-
47
-            return null;
48
-        }
49
-        const pcId = this.tpc.id;
50
-
51
-        return `${streamId}-${pcId} ${trackId}-${pcId}`;
52
     }
25
     }
53
 
26
 
54
     /**
27
     /**
55
-     * Updates or adds a 'msid' attribute in the format '<endpoint_id>-<mediaType>-<trackIndex>-<tpcId>'
56
-     * example - d8ff91-video-0-1
57
-     * All other attributes like 'cname', 'label' and 'mslabel' are removed since these are not processed by Jicofo.
28
+     * Updates or adds a 'msid' attribute for the local sources in the SDP. Also adds 'sourceName' and 'videoType'
29
+     * (if applicable) attributes. All other source attributes like 'cname', 'label' and 'mslabel' are removed since
30
+     * these are not processed by Jicofo.
58
      *
31
      *
59
      * @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be
32
      * @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be
60
      * modified in place.
33
      * modified in place.
61
      * @returns {void}
34
      * @returns {void}
62
      * @private
35
      * @private
63
      */
36
      */
64
-    _transformMediaIdentifiers(mediaSection) {
65
-        const mediaType = mediaSection.mLine?.type;
66
-        const mediaDirection = mediaSection.mLine?.direction;
67
-        const msidLine = mediaSection.mLine?.msid;
68
-        const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
69
-        const streamId = `${this.localEndpointId}-${mediaType}`;
70
-        let trackId = msidLine ? msidLine.split(' ')[1] : `${this.localEndpointId}-${mediaSection.mLine.mid}`;
71
-
72
-        // Always overwrite msid since we want the msid to be in this format even if the browser generates one.
73
-        for (const source of sources) {
74
-            const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid');
75
-
76
-            if (msid) {
77
-                trackId = msid.value.split(' ')[1];
37
+    _transformMediaIdentifiers(mediaSection, ssrcMap) {
38
+        const mediaType = mediaSection.mLine.type;
39
+        const mediaDirection = mediaSection.mLine.direction;
40
+        const sources = [ ...new Set(mediaSection.mLine.ssrcs?.map(s => s.id)) ];
41
+        let sourceName;
42
+
43
+        if (ssrcMap.size) {
44
+            const sortedSources = sources.slice().sort();
45
+
46
+            for (const [ id, trackSsrcs ] of ssrcMap.entries()) {
47
+                if (isEqual(sortedSources, [ ...trackSsrcs.ssrcs ].sort())) {
48
+                    sourceName = id;
49
+                }
78
             }
50
             }
79
-            this._updateSourcesToMsidMap(mediaType, streamId, trackId);
80
-            const storedStreamId = mediaType === MediaType.VIDEO
81
-                ? this.videoSourcesToMsidMap.get(trackId)
82
-                : this.audioSourcesToMsidMap.get(trackId);
83
-
84
-            const generatedMsid = this._generateMsidAttribute(mediaType, trackId, storedStreamId);
85
-
86
-            // Update the msid if the 'msid' attribute exists.
87
-            if (msid) {
88
-                msid.value = generatedMsid;
89
-
90
-            // Generate the 'msid' attribute if there is a local source.
91
-            } else if (mediaDirection === MediaDirection.SENDONLY || mediaDirection === MediaDirection.SENDRECV) {
92
-                mediaSection.ssrcs.push({
93
-                    id: source,
94
-                    attribute: 'msid',
95
-                    value: generatedMsid
96
-                });
51
+            for (const source of sources) {
52
+                if ((mediaDirection === MediaDirection.SENDONLY || mediaDirection === MediaDirection.SENDRECV)
53
+                    && sourceName) {
54
+                    const msid = ssrcMap.get(sourceName).msid;
55
+                    const generatedMsid = `${msid}-${this.tpc.id}`;
56
+                    const existingMsid = mediaSection.ssrcs
57
+                        .find(ssrc => ssrc.id === source && ssrc.attribute === 'msid');
58
+
59
+                    // Always overwrite msid since we want the msid to be in this format even if the browser generates
60
+                    // one. '<endpoint_id>-<mediaType>-<trackIndex>-<tpcId>' example - d8ff91-video-0-1
61
+                    if (existingMsid) {
62
+                        existingMsid.value = generatedMsid;
63
+                    } else {
64
+                        mediaSection.ssrcs.push({
65
+                            id: source,
66
+                            attribute: 'msid',
67
+                            value: generatedMsid
68
+                        });
69
+                    }
70
+
71
+                    // Inject source names as a=ssrc:3124985624 name:endpointA-v0
72
+                    mediaSection.ssrcs.push({
73
+                        id: source,
74
+                        attribute: 'name',
75
+                        value: sourceName
76
+                    });
77
+
78
+                    const videoType = this.tpc.getLocalVideoTracks()
79
+                        .find(track => track.getSourceName() === sourceName)
80
+                        ?.getVideoType();
81
+
82
+                    if (mediaType === MediaType.VIDEO && videoType) {
83
+                        // Inject videoType as a=ssrc:1234 videoType:desktop.
84
+                        mediaSection.ssrcs.push({
85
+                            id: source,
86
+                            attribute: 'videoType',
87
+                            value: videoType
88
+                        });
89
+                    }
90
+                }
97
             }
91
             }
98
         }
92
         }
99
 
93
 
100
-        // Ignore the 'cname', 'label' and 'mslabel' attributes and only have the 'msid' attribute.
101
-        mediaSection.ssrcs = mediaSection.ssrcs.filter(ssrc => ssrc.attribute === 'msid');
94
+        // Ignore the 'cname', 'label' and 'mslabel' attributes.
95
+        mediaSection.ssrcs = mediaSection.ssrcs
96
+            .filter(ssrc => ssrc.attribute === 'msid' || ssrc.attribute === 'name' || ssrc.attribute === 'videoType');
102
 
97
 
103
         // On FF when the user has started muted create answer will generate a recv only SSRC. We don't want to signal
98
         // On FF when the user has started muted create answer will generate a recv only SSRC. We don't want to signal
104
         // this SSRC in order to reduce the load of the xmpp server for large calls. Therefore the SSRC needs to be
99
         // this SSRC in order to reduce the load of the xmpp server for large calls. Therefore the SSRC needs to be
122
     }
117
     }
123
 
118
 
124
     /**
119
     /**
125
-     * Updates the MSID map.
120
+     * This transformation will make sure that stream identifiers are unique across all of the local PeerConnections
121
+     * even if the same stream is used by multiple instances at the same time. It also injects 'sourceName' and
122
+     * 'videoType' attribute.
126
      *
123
      *
127
-     * @param {string} mediaType The media type.
128
-     * @param {string} streamId The stream id.
129
-     * @param {string} trackId The track id.
130
-     * @returns {void}
131
-     */
132
-    _updateSourcesToMsidMap(mediaType, streamId, trackId) {
133
-        if (mediaType === MediaType.VIDEO) {
134
-            if (!this.videoSourcesToMsidMap.has(trackId)) {
135
-                const generatedStreamId = `${streamId}-${this.videoSourcesToMsidMap.size}`;
136
-
137
-                this.videoSourcesToMsidMap.set(trackId, generatedStreamId);
138
-            }
139
-        } else if (!this.audioSourcesToMsidMap.has(trackId)) {
140
-            const generatedStreamId = `${streamId}-${this.audioSourcesToMsidMap.size}`;
141
-
142
-            this.audioSourcesToMsidMap.set(trackId, generatedStreamId);
143
-        }
144
-    }
145
-
146
-    /**
147
-     * This transformation will make sure that stream identifiers are unique
148
-     * across all of the local PeerConnections even if the same stream is used
149
-     * by multiple instances at the same time.
150
-     * Each PeerConnection assigns different SSRCs to the same local
151
-     * MediaStream, but the MSID remains the same as it's used to identify
152
-     * the stream by the WebRTC backend. The transformation will append
153
-     * {@link TraceablePeerConnection#id} at the end of each stream's identifier
154
-     * ("cname", "msid", "label" and "mslabel").
155
-     *
156
-     * @param {RTCSessionDescription} sessionDesc - The local session
157
-     * description (this instance remains unchanged).
124
+     * @param {RTCSessionDescription} sessionDesc - The local session description (this instance remains unchanged).
125
+     * @param {Map<string, TPCSSRCInfo>} ssrcMap - The SSRC and source map for the local tracks.
158
      * @return {RTCSessionDescription} - Transformed local session description
126
      * @return {RTCSessionDescription} - Transformed local session description
159
      * (a modified copy of the one given as the input).
127
      * (a modified copy of the one given as the input).
160
      */
128
      */
161
-    transformStreamIdentifiers(sessionDesc) {
129
+    transformStreamIdentifiers(sessionDesc, ssrcMap) {
162
         if (!sessionDesc || !sessionDesc.sdp || !sessionDesc.type) {
130
         if (!sessionDesc || !sessionDesc.sdp || !sessionDesc.type) {
163
             return sessionDesc;
131
             return sessionDesc;
164
         }
132
         }
167
         const audioMLine = transformer.selectMedia(MediaType.AUDIO)?.[0];
135
         const audioMLine = transformer.selectMedia(MediaType.AUDIO)?.[0];
168
 
136
 
169
         if (audioMLine) {
137
         if (audioMLine) {
170
-            this._transformMediaIdentifiers(audioMLine);
171
-            this._injectSourceNames(audioMLine);
138
+            this._transformMediaIdentifiers(audioMLine, ssrcMap);
172
         }
139
         }
173
 
140
 
174
         const videoMlines = transformer.selectMedia(MediaType.VIDEO);
141
         const videoMlines = transformer.selectMedia(MediaType.VIDEO);
175
 
142
 
176
         for (const videoMLine of videoMlines) {
143
         for (const videoMLine of videoMlines) {
177
-            this._transformMediaIdentifiers(videoMLine);
178
-            this._injectSourceNames(videoMLine);
144
+            this._transformMediaIdentifiers(videoMLine, ssrcMap);
179
         }
145
         }
180
 
146
 
181
-        // Reset the local tracks based maps for msid after every transformation since Chrome 122 is generating
182
-        // a new set of SSRCs for the same source when the direction of transceiver changes because of a remote
183
-        // source getting added on the p2p connection.
184
-        this.audioSourcesToMsidMap.clear();
185
-        this.videoSourcesToMsidMap.clear();
186
-
187
         return new RTCSessionDescription({
147
         return new RTCSessionDescription({
188
             type: sessionDesc.type,
148
             type: sessionDesc.type,
189
             sdp: transformer.toRawSDP()
149
             sdp: transformer.toRawSDP()
190
         });
150
         });
191
     }
151
     }
192
-
193
-    /**
194
-     * Injects source names. Source names are need to for multiple streams per endpoint support. The final plan is to
195
-     * use the "mid" attribute for source names, but because the SDP to Jingle conversion still operates in the Plan-B
196
-     * semantics (one source name per media), a custom "name" attribute is injected into SSRC lines..
197
-     *
198
-     * @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be
199
-     * modified in place.
200
-     * @returns {void}
201
-     * @private
202
-     */
203
-    _injectSourceNames(mediaSection) {
204
-        const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
205
-        const mediaType = mediaSection.mLine?.type;
206
-
207
-        if (!mediaType) {
208
-            throw new Error('_transformMediaIdentifiers - no media type in mediaSection');
209
-        }
210
-
211
-        for (const source of sources) {
212
-            const nameExists = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'name');
213
-            const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid').value;
214
-            const streamId = msid.split(' ')[0];
215
-
216
-            // Example stream id: d8ff91-video-8-1
217
-            // In the example above 8 is the track index
218
-            const trackIndexParts = streamId.split('-');
219
-            const trackIndex = trackIndexParts[trackIndexParts.length - 2];
220
-            const sourceName = getSourceNameForJitsiTrack(this.localEndpointId, mediaType, trackIndex);
221
-
222
-            if (!nameExists) {
223
-                // Inject source names as a=ssrc:3124985624 name:endpointA-v0
224
-                mediaSection.ssrcs.push({
225
-                    id: source,
226
-                    attribute: 'name',
227
-                    value: sourceName
228
-                });
229
-            }
230
-
231
-            if (mediaType === MediaType.VIDEO) {
232
-                const videoType = this.tpc.getLocalVideoTracks().find(track => track.getSourceName() === sourceName)
233
-                    ?.getVideoType();
234
-
235
-                if (videoType) {
236
-                    // Inject videoType as a=ssrc:1234 videoType:desktop.
237
-                    mediaSection.ssrcs.push({
238
-                        id: source,
239
-                        attribute: 'videoType',
240
-                        value: videoType
241
-                    });
242
-                }
243
-            }
244
-        }
245
-    }
246
 }
152
 }

+ 49
- 9
modules/sdp/LocalSdpMunger.spec.js Visa fil

2
 import * as transform from 'sdp-transform';
2
 import * as transform from 'sdp-transform';
3
 
3
 
4
 import { MockPeerConnection } from '../RTC/MockClasses';
4
 import { MockPeerConnection } from '../RTC/MockClasses';
5
-import FeatureFlags from '../flags/FeatureFlags';
6
 
5
 
7
 import LocalSdpMunger from './LocalSdpMunger';
6
 import LocalSdpMunger from './LocalSdpMunger';
8
 import { default as SampleSdpStrings } from './SampleSdpStrings.js';
7
 import { default as SampleSdpStrings } from './SampleSdpStrings.js';
26
     const localEndpointId = 'sRdpsdg';
25
     const localEndpointId = 'sRdpsdg';
27
 
26
 
28
     beforeEach(() => {
27
     beforeEach(() => {
29
-        FeatureFlags.init({ });
30
         localSdpMunger = new LocalSdpMunger(tpc, localEndpointId);
28
         localSdpMunger = new LocalSdpMunger(tpc, localEndpointId);
31
     });
29
     });
32
     describe('StripSsrcs', () => {
30
     describe('StripSsrcs', () => {
38
                 type: 'offer',
36
                 type: 'offer',
39
                 sdp: sdpStr
37
                 sdp: sdpStr
40
             });
38
             });
41
-            const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc);
39
+            const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, {});
42
             const newSdp = transform.parse(transformedDesc.sdp);
40
             const newSdp = transform.parse(transformedDesc.sdp);
43
             const audioSsrcs = getSsrcLines(newSdp, 'audio');
41
             const audioSsrcs = getSsrcLines(newSdp, 'audio');
44
             const videoSsrcs = getSsrcLines(newSdp, 'video');
42
             const videoSsrcs = getSsrcLines(newSdp, 'video');
56
                     type: 'offer',
54
                     type: 'offer',
57
                     sdp: sdpStr
55
                     sdp: sdpStr
58
                 });
56
                 });
59
-                const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc);
57
+                const ssrcMap = new Map();
58
+
59
+                ssrcMap.set('sRdpsdg-v0', {
60
+                    ssrcs: [ 1757014965, 1479742055, 1089111804 ],
61
+                    msid: 'sRdpsdg-video-0',
62
+                    groups: [ {
63
+                        semantics: 'SIM',
64
+                        ssrcs: [ 1757014965, 1479742055, 1089111804 ] } ]
65
+                });
66
+                ssrcMap.set('sRdpsdg-a0', {
67
+                    ssrcs: [ 124723944 ],
68
+                    msid: 'sRdpsdg-audio-0'
69
+                });
70
+                const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap);
60
                 const newSdp = transform.parse(transformedDesc.sdp);
71
                 const newSdp = transform.parse(transformedDesc.sdp);
61
 
72
 
62
                 audioSsrcs = getSsrcLines(newSdp, 'audio');
73
                 audioSsrcs = getSsrcLines(newSdp, 'audio');
79
                 type: 'offer',
90
                 type: 'offer',
80
                 sdp: sdpStr
91
                 sdp: sdpStr
81
             });
92
             });
82
-            const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc);
93
+            const ssrcMap = new Map();
94
+
95
+            ssrcMap.set('sRdpsdg-v0', {
96
+                ssrcs: [ 984899560 ],
97
+                msid: 'sRdpsdg-video-0'
98
+            });
99
+            ssrcMap.set('sRdpsdg-a0', {
100
+                ssrcs: [ 124723944 ],
101
+                msid: 'sRdpsdg-audio-0'
102
+            });
103
+            const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap);
83
             const newSdp = transform.parse(transformedDesc.sdp);
104
             const newSdp = transform.parse(transformedDesc.sdp);
84
 
105
 
85
             const videoSsrcs = getSsrcLines(newSdp, 'video');
106
             const videoSsrcs = getSsrcLines(newSdp, 'video');
86
 
107
 
87
             for (const ssrcLine of videoSsrcs) {
108
             for (const ssrcLine of videoSsrcs) {
88
                 if (ssrcLine.attribute === 'msid') {
109
                 if (ssrcLine.attribute === 'msid') {
89
-                    const msid = ssrcLine.value.split(' ')[0];
110
+                    const msid = ssrcLine.value;
90
 
111
 
91
                     expect(msid).toBe(`${localEndpointId}-video-0-${tpc.id}`);
112
                     expect(msid).toBe(`${localEndpointId}-video-0-${tpc.id}`);
92
                 }
113
                 }
102
                 type: 'offer',
123
                 type: 'offer',
103
                 sdp: sdpStr
124
                 sdp: sdpStr
104
             });
125
             });
105
-            const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc);
126
+            const ssrcMap = new Map();
127
+
128
+            ssrcMap.set('sRdpsdg-v0', {
129
+                ssrcs: [ 984899560 ],
130
+                msid: 'sRdpsdg-video-0'
131
+            });
132
+            ssrcMap.set('sRdpsdg-a0', {
133
+                ssrcs: [ 124723944 ],
134
+                msid: 'sRdpsdg-audio-0'
135
+            });
136
+            const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap);
106
             const newSdp = transform.parse(transformedDesc.sdp);
137
             const newSdp = transform.parse(transformedDesc.sdp);
107
             const videoSsrcs = getSsrcLines(newSdp, 'video');
138
             const videoSsrcs = getSsrcLines(newSdp, 'video');
108
             const msidExists = videoSsrcs.find(s => s.attribute === 'msid');
139
             const msidExists = videoSsrcs.find(s => s.attribute === 'msid');
124
             type: 'offer',
155
             type: 'offer',
125
             sdp: sdpStr
156
             sdp: sdpStr
126
         });
157
         });
127
-        const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc);
158
+        const ssrcMap = new Map();
159
+
160
+        ssrcMap.set('sRdpsdg-v0', {
161
+            ssrcs: [ 1757014965, 984899560, 1479742055, 855213044, 1089111804, 2963867077 ],
162
+            msid: 'sRdpsdg-video-0'
163
+        });
164
+        ssrcMap.set('sRdpsdg-a0', {
165
+            ssrcs: [ 124723944 ],
166
+            msid: 'sRdpsdg-audio-0'
167
+        });
168
+        const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap);
128
         const newSdp = transform.parse(transformedDesc.sdp);
169
         const newSdp = transform.parse(transformedDesc.sdp);
129
 
170
 
130
         audioMsidLine = getSsrcLines(newSdp, 'audio').find(ssrc => ssrc.attribute === 'msid')?.value;
171
         audioMsidLine = getSsrcLines(newSdp, 'audio').find(ssrc => ssrc.attribute === 'msid')?.value;
134
     };
175
     };
135
 
176
 
136
     it('should transform', () => {
177
     it('should transform', () => {
137
-        FeatureFlags.init({ });
138
         transformStreamIdentifiers();
178
         transformStreamIdentifiers();
139
 
179
 
140
         expect(audioMsid).toBe('sRdpsdg-audio-0-1');
180
         expect(audioMsid).toBe('sRdpsdg-audio-0-1');

+ 93
- 28
modules/sdp/SDP.js Visa fil

1
 import $ from 'jquery';
1
 import $ from 'jquery';
2
 import { cloneDeep } from 'lodash-es';
2
 import { cloneDeep } from 'lodash-es';
3
 import transform from 'sdp-transform';
3
 import transform from 'sdp-transform';
4
+import { Strophe } from 'strophe.js';
4
 
5
 
5
 import { MediaDirection } from '../../service/RTC/MediaDirection';
6
 import { MediaDirection } from '../../service/RTC/MediaDirection';
6
 import { MediaType } from '../../service/RTC/MediaType';
7
 import { MediaType } from '../../service/RTC/MediaType';
20
      *
21
      *
21
      * @param {string} sdp - The SDP generated by the browser when SDP->Jingle conversion is needed, an empty string
22
      * @param {string} sdp - The SDP generated by the browser when SDP->Jingle conversion is needed, an empty string
22
      * when Jingle->SDP conversion is needed.
23
      * when Jingle->SDP conversion is needed.
24
+     * @param {boolean} isP2P - Whether this SDP belongs to a p2p peerconnection.
23
      */
25
      */
24
-    constructor(sdp) {
26
+    constructor(sdp, isP2P = false) {
25
         const media = sdp.split('\r\nm=');
27
         const media = sdp.split('\r\nm=');
26
 
28
 
27
         for (let i = 1, length = media.length; i < length; i++) {
29
         for (let i = 1, length = media.length; i < length; i++) {
34
         }
36
         }
35
         const session = `${media.shift()}\r\n`;
37
         const session = `${media.shift()}\r\n`;
36
 
38
 
39
+        this.isP2P = isP2P;
37
         this.media = media;
40
         this.media = media;
38
         this.raw = session + media.join('');
41
         this.raw = session + media.join('');
39
         this.session = session;
42
         this.session = session;
86
      * @returns {boolean}
89
      * @returns {boolean}
87
      */
90
      */
88
     containsSSRC(ssrc) {
91
     containsSSRC(ssrc) {
89
-        const souceMap = this.getMediaSsrcMap();
92
+        const sourceMap = this.getMediaSsrcMap();
90
 
93
 
91
-        return Object.values(souceMap).some(media => media.ssrcs[ssrc]);
94
+        return [ ...sourceMap.values() ].some(media => media.ssrcs[ssrc]);
92
     }
95
     }
93
 
96
 
94
     /**
97
     /**
141
      * @returns {*}
144
      * @returns {*}
142
      */
145
      */
143
     getMediaSsrcMap() {
146
     getMediaSsrcMap() {
144
-        const mediaSSRCs = {};
147
+        const sourceInfo = new Map();
145
 
148
 
146
         this.media.forEach((mediaItem, mediaindex) => {
149
         this.media.forEach((mediaItem, mediaindex) => {
147
             const mid = SDPUtil.parseMID(SDPUtil.findLine(mediaItem, 'a=mid:'));
150
             const mid = SDPUtil.parseMID(SDPUtil.findLine(mediaItem, 'a=mid:'));
151
+            const mline = SDPUtil.parseMLine(mediaItem.split('\r\n')[0]);
148
             const media = {
152
             const media = {
149
                 mediaindex,
153
                 mediaindex,
154
+                mediaType: mline.media,
150
                 mid,
155
                 mid,
151
                 ssrcs: {},
156
                 ssrcs: {},
152
                 ssrcGroups: []
157
                 ssrcGroups: []
153
             };
158
             };
154
 
159
 
155
-            mediaSSRCs[mediaindex] = media;
156
-
157
             SDPUtil.findLines(mediaItem, 'a=ssrc:').forEach(line => {
160
             SDPUtil.findLines(mediaItem, 'a=ssrc:').forEach(line => {
158
                 const linessrc = line.substring(7).split(' ')[0];
161
                 const linessrc = line.substring(7).split(' ')[0];
159
 
162
 
179
                     });
182
                     });
180
                 }
183
                 }
181
             });
184
             });
185
+
186
+            sourceInfo.set(mediaindex, media);
182
         });
187
         });
183
 
188
 
184
-        return mediaSSRCs;
189
+        return sourceInfo;
185
     }
190
     }
186
 
191
 
187
     /**
192
     /**
444
                 xmlns: XEP.BUNDLE_MEDIA,
449
                 xmlns: XEP.BUNDLE_MEDIA,
445
                 semantics
450
                 semantics
446
             });
451
             });
447
-            parts.forEach(part => elem.c('content', { name: part }).up());
452
+
453
+            // Bundle all the media types. Jicofo expects the 'application' media type to be signaled as 'data'.
454
+            let mediaTypes = [ MediaType.AUDIO, MediaType.VIDEO, 'data' ];
455
+
456
+            // For p2p connection, 'mid' will be used in the bundle group.
457
+            if (this.isP2P) {
458
+                mediaTypes = this.media.map(mediaItem => SDPUtil.parseMID(SDPUtil.findLine(mediaItem, 'a=mid:')));
459
+            }
460
+            mediaTypes.forEach(type => elem.c('content', { name: type }).up());
448
             elem.up();
461
             elem.up();
449
         });
462
         });
450
 
463
 
451
         this.media.forEach((mediaItem, i) => {
464
         this.media.forEach((mediaItem, i) => {
452
             const mline = SDPUtil.parseMLine(mediaItem.split('\r\n')[0]);
465
             const mline = SDPUtil.parseMLine(mediaItem.split('\r\n')[0]);
453
-
454
-            if (![ MediaType.AUDIO, MediaType.VIDEO, MediaType.APPLICATION ].includes(mline.media)) {
455
-                return;
456
-            }
466
+            const mediaType = mline.media === MediaType.APPLICATION ? 'data' : mline.media;
457
 
467
 
458
             let ssrc = false;
468
             let ssrc = false;
459
             const assrcline = SDPUtil.findLine(mediaItem, 'a=ssrc:');
469
             const assrcline = SDPUtil.findLine(mediaItem, 'a=ssrc:');
462
                 ssrc = assrcline.substring(7).split(' ')[0];
472
                 ssrc = assrcline.substring(7).split(' ')[0];
463
             }
473
             }
464
 
474
 
475
+            const contents = $(elem.tree()).find(`content[name='${mediaType}']`);
476
+
477
+            // Append source groups from the new m-lines to the existing media description. The SDP will have multiple
478
+            // m-lines for audio and video including the recv-only ones for remote sources but there needs to be only
479
+            // one media description for a given media type that should include all the sources, i.e., both the camera
480
+            // and screenshare sources should be added to the 'video' description.
481
+            for (const content of contents) {
482
+                if (!content.hasAttribute('creator')) {
483
+                    // eslint-disable-next-line no-continue
484
+                    continue;
485
+                }
486
+
487
+                if (ssrc) {
488
+                    const description = $(content).find('description');
489
+                    const ssrcMap = SDPUtil.parseSSRC(mediaItem);
490
+
491
+                    for (const [ availableSsrc, ssrcParameters ] of ssrcMap) {
492
+                        const sourceName = SDPUtil.parseSourceNameLine(ssrcParameters);
493
+                        const videoType = SDPUtil.parseVideoTypeLine(ssrcParameters);
494
+                        const source = Strophe.xmlElement('source', {
495
+                            ssrc: availableSsrc,
496
+                            name: sourceName,
497
+                            videoType,
498
+                            xmlns: XEP.SOURCE_ATTRIBUTES
499
+                        });
500
+
501
+                        const msid = SDPUtil.parseMSIDAttribute(ssrcParameters);
502
+
503
+                        if (msid) {
504
+                            const param = Strophe.xmlElement('parameter', {
505
+                                name: 'msid',
506
+                                value: msid
507
+                            });
508
+
509
+                            source.append(param);
510
+                        }
511
+                        description.append(source);
512
+                    }
513
+
514
+                    const ssrcGroupLines = SDPUtil.findLines(mediaItem, 'a=ssrc-group:');
515
+
516
+                    ssrcGroupLines.forEach(line => {
517
+                        const { semantics, ssrcs } = SDPUtil.parseSSRCGroupLine(line);
518
+
519
+                        if (ssrcs.length) {
520
+                            const group = Strophe.xmlElement('ssrc-group', {
521
+                                semantics,
522
+                                xmlns: XEP.SOURCE_ATTRIBUTES
523
+                            });
524
+
525
+                            for (const val of ssrcs) {
526
+                                const src = Strophe.xmlElement('source', {
527
+                                    ssrc: val
528
+                                });
529
+
530
+                                group.append(src);
531
+                            }
532
+                            description.append(group);
533
+                        }
534
+                    });
535
+                }
536
+
537
+                return;
538
+            }
539
+            const mid = SDPUtil.parseMID(SDPUtil.findLine(mediaItem, 'a=mid:'));
540
+
465
             elem.c('content', {
541
             elem.c('content', {
466
                 creator: thecreator,
542
                 creator: thecreator,
467
-                name: mline.media
543
+                name: this.isP2P ? mid : mediaType
468
             });
544
             });
469
-            const amidline = SDPUtil.findLine(mediaItem, 'a=mid:');
470
 
545
 
471
-            if (amidline) {
472
-                // Prefer identifier from a=mid if present.
473
-                elem.attrs({ name: SDPUtil.parseMID(amidline) });
474
-            }
475
-
476
-            if (mline.media === MediaType.VIDEO && typeof this.initialLastN === 'number') {
546
+            if (mediaType === MediaType.VIDEO && typeof this.initialLastN === 'number') {
477
                 elem.c('initial-last-n', {
547
                 elem.c('initial-last-n', {
478
                     xmlns: 'jitsi:colibri2',
548
                     xmlns: 'jitsi:colibri2',
479
                     value: this.initialLastN
549
                     value: this.initialLastN
480
                 }).up();
550
                 }).up();
481
             }
551
             }
482
 
552
 
483
-            if ([ MediaType.AUDIO, MediaType.VIDEO ].includes(mline.media)) {
553
+            if ([ MediaType.AUDIO, MediaType.VIDEO ].includes(mediaType)) {
484
                 elem.c('description', {
554
                 elem.c('description', {
485
                     xmlns: XEP.RTP_MEDIA,
555
                     xmlns: XEP.RTP_MEDIA,
486
-                    media: mline.media
556
+                    media: mediaType
487
                 });
557
                 });
488
-                if (ssrc) {
489
-                    elem.attrs({ ssrc });
490
-                }
491
 
558
 
492
                 mline.fmt.forEach(format => {
559
                 mline.fmt.forEach(format => {
493
                     const rtpmap = SDPUtil.findLine(mediaItem, `a=rtpmap:${format}`);
560
                     const rtpmap = SDPUtil.findLine(mediaItem, `a=rtpmap:${format}`);
536
                     const ssrcGroupLines = SDPUtil.findLines(mediaItem, 'a=ssrc-group:');
603
                     const ssrcGroupLines = SDPUtil.findLines(mediaItem, 'a=ssrc-group:');
537
 
604
 
538
                     ssrcGroupLines.forEach(line => {
605
                     ssrcGroupLines.forEach(line => {
539
-                        const idx = line.indexOf(' ');
540
-                        const semantics = line.substr(0, idx).substr(13);
541
-                        const ssrcs = line.substr(14 + semantics.length).split(' ');
606
+                        const { semantics, ssrcs } = SDPUtil.parseSSRCGroupLine(line);
542
 
607
 
543
                         if (ssrcs.length) {
608
                         if (ssrcs.length) {
544
                             elem.c('ssrc-group', {
609
                             elem.c('ssrc-group', {

+ 866
- 7
modules/sdp/SDP.spec.js Visa fil

6
 
6
 
7
 import SDP from './SDP';
7
 import SDP from './SDP';
8
 
8
 
9
+/* eslint-disable max-len*/
10
+
9
 /**
11
 /**
10
  * @param {string} xml - raw xml of the stanza
12
  * @param {string} xml - raw xml of the stanza
11
  */
13
  */
18
         FeatureFlags.init({ });
20
         FeatureFlags.init({ });
19
     });
21
     });
20
     describe('toJingle', () => {
22
     describe('toJingle', () => {
21
-        /* eslint-disable max-len*/
22
         const testSdp = [
23
         const testSdp = [
23
             'v=0\r\n',
24
             'v=0\r\n',
24
             'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n',
25
             'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n',
79
             'a=ssrc-group:FID 4004 4005\r\n',
80
             'a=ssrc-group:FID 4004 4005\r\n',
80
             'a=rtcp-mux\r\n'
81
             'a=rtcp-mux\r\n'
81
         ].join('');
82
         ].join('');
82
-        /* eslint-enable max-len*/
83
 
83
 
84
         it('correctly groups ssrcs lines that are not in order', () => {
84
         it('correctly groups ssrcs lines that are not in order', () => {
85
             const sdp = new SDP(testSdp);
85
             const sdp = new SDP(testSdp);
95
                 sid: 'temp-sid'
95
                 sid: 'temp-sid'
96
             });
96
             });
97
 
97
 
98
-            sdp.toJingle(accept, false);
98
+            sdp.toJingle(accept, 'responder');
99
 
99
 
100
             const { nodeTree } = accept;
100
             const { nodeTree } = accept;
101
             const videoSources = nodeTree.querySelectorAll('description[media=\'video\']>source');
101
             const videoSources = nodeTree.querySelectorAll('description[media=\'video\']>source');
118
                     sid: 'temp-sid'
118
                     sid: 'temp-sid'
119
                 });
119
                 });
120
 
120
 
121
-            sdp.toJingle(accept, false);
121
+            sdp.toJingle(accept, 'responder');
122
 
122
 
123
             const { nodeTree } = accept;
123
             const { nodeTree } = accept;
124
 
124
 
135
         });
135
         });
136
     });
136
     });
137
 
137
 
138
+    describe('toJingle for multiple m-lines', () => {
139
+        const testSdp = [
140
+            'v=0\r\n',
141
+            'o=- 6251210045590020951 2 IN IP4 127.0.0.1\r\n',
142
+            's=-\r\n',
143
+            't=0 0\r\n',
144
+            'a=msid-semantic:  WMS\r\n',
145
+            'a=group:BUNDLE 0 1 2\r\n',
146
+            'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
147
+            'c=IN IP4 0.0.0.0\r\n',
148
+            'a=rtpmap:111 opus/48000/2\r\n',
149
+            'a=rtpmap:126 telephone-event/8000\r\n',
150
+            'a=fmtp:111 minptime=10;useinbandfec=1\r\n',
151
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
152
+            'a=rtcp-fb:111 transport-cc\r\n',
153
+            'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n',
154
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
155
+            'a=setup:active\r\n',
156
+            'a=mid:0\r\n',
157
+            'a=msid:- 5caf9eeb-f846-43cf-8868-78ed2e0fea74\r\n',
158
+            'a=sendrecv\r\n',
159
+            'a=ice-ufrag:gi+W\r\n',
160
+            'a=ice-pwd:NmFZJ6NWoC2gjagIudLFWI8Q\r\n',
161
+            'a=fingerprint:sha-256 41:1D:49:50:40:0D:68:9F:C6:AB:B2:14:98:67:E7:06:70:F0:B2:4A:5C:AB:03:F3:89:AF:B0:11:AF:05:2D:D6\r\n',
162
+            'a=ice-options:trickle\r\n',
163
+            'a=ssrc:3134174615 cname:Ypjacq/wapOqDJKy\r\n',
164
+            'a=rtcp-mux\r\n',
165
+            'a=extmap-allow-mixed\r\n',
166
+            'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n',
167
+            'c=IN IP4 0.0.0.0\r\n',
168
+            'a=rtpmap:101 VP9/90000\r\n',
169
+            'a=rtpmap:97 rtx/90000\r\n',
170
+            'a=rtpmap:100 VP8/90000\r\n',
171
+            'a=rtpmap:96 rtx/90000\r\n',
172
+            'a=rtpmap:107 H264/90000\r\n',
173
+            'a=rtpmap:99 rtx/90000\r\n',
174
+            'a=rtpmap:41 AV1/90000\r\n',
175
+            'a=rtpmap:42 rtx/90000\r\n',
176
+            'a=fmtp:101 profile-id=0\r\n',
177
+            'a=fmtp:97 apt=101\r\n',
178
+            'a=fmtp:96 apt=100\r\n',
179
+            'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n',
180
+            'a=fmtp:99 apt=107\r\n',
181
+            'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n',
182
+            'a=fmtp:42 apt=41\r\n',
183
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
184
+            'a=rtcp-fb:101 ccm fir\r\n',
185
+            'a=rtcp-fb:101 nack\r\n',
186
+            'a=rtcp-fb:101 nack pli\r\n',
187
+            'a=rtcp-fb:101 transport-cc\r\n',
188
+            'a=rtcp-fb:97 ccm fir\r\n',
189
+            'a=rtcp-fb:97 nack\r\n',
190
+            'a=rtcp-fb:97 nack pli\r\n',
191
+            'a=rtcp-fb:100 ccm fir\r\n',
192
+            'a=rtcp-fb:100 nack\r\n',
193
+            'a=rtcp-fb:100 nack pli\r\n',
194
+            'a=rtcp-fb:100 transport-cc\r\n',
195
+            'a=rtcp-fb:96 ccm fir\r\n',
196
+            'a=rtcp-fb:96 nack\r\n',
197
+            'a=rtcp-fb:96 nack pli\r\n',
198
+            'a=rtcp-fb:107 ccm fir\r\n',
199
+            'a=rtcp-fb:107 nack\r\n',
200
+            'a=rtcp-fb:107 nack pli\r\n',
201
+            'a=rtcp-fb:107 transport-cc\r\n',
202
+            'a=rtcp-fb:41 ccm fir\r\n',
203
+            'a=rtcp-fb:41 nack\r\n',
204
+            'a=rtcp-fb:41 nack pli\r\n',
205
+            'a=rtcp-fb:41 transport-cc\r\n',
206
+            'a=rtcp-fb:42 ccm fir\r\n',
207
+            'a=rtcp-fb:42 nack\r\n',
208
+            'a=rtcp-fb:42 nack pli\r\n',
209
+            'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n',
210
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
211
+            'a=setup:active\r\n',
212
+            'a=mid:1\r\n',
213
+            'a=msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n',
214
+            'a=sendrecv\r\n',
215
+            'a=ice-ufrag:gi+W\r\n',
216
+            'a=ice-pwd:NmFZJ6NWoC2gjagIudLFWI8Q\r\n',
217
+            'a=fingerprint:sha-256 41:1D:49:50:40:0D:68:9F:C6:AB:B2:14:98:67:E7:06:70:F0:B2:4A:5C:AB:03:F3:89:AF:B0:11:AF:05:2D:D6\r\n',
218
+            'a=ice-options:trickle\r\n',
219
+            'a=ssrc:691901703 cname:Ypjacq/wapOqDJKy\r\n',
220
+            'a=ssrc:3967743536 cname:Ypjacq/wapOqDJKy\r\n',
221
+            'a=ssrc:691901703 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n',
222
+            'a=ssrc:3967743536 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n',
223
+            'a=ssrc:4098097822 cname:Ypjacq/wapOqDJKy\r\n',
224
+            'a=ssrc:4098097822 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n',
225
+            'a=ssrc:731566086 cname:Ypjacq/wapOqDJKy\r\n',
226
+            'a=ssrc:731566086 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n',
227
+            'a=ssrc:2374965413 cname:Ypjacq/wapOqDJKy\r\n',
228
+            'a=ssrc:2374965413 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n',
229
+            'a=ssrc:3680614139 cname:Ypjacq/wapOqDJKy\r\n',
230
+            'a=ssrc:3680614139 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n',
231
+            'a=ssrc-group:FID 691901703 3967743536\r\n',
232
+            'a=ssrc-group:SIM 691901703 4098097822 731566086\r\n',
233
+            'a=ssrc-group:FID 4098097822 2374965413\r\n',
234
+            'a=ssrc-group:FID 731566086 3680614139\r\n',
235
+            'a=rtcp-mux\r\n',
236
+            'a=extmap-allow-mixed\r\n',
237
+            'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n',
238
+            'c=IN IP4 0.0.0.0\r\n',
239
+            'a=setup:active\r\n',
240
+            'a=mid:2\r\n',
241
+            'a=ice-ufrag:gi+W\r\n',
242
+            'a=ice-pwd:NmFZJ6NWoC2gjagIudLFWI8Q\r\n',
243
+            'a=fingerprint:sha-256 41:1D:49:50:40:0D:68:9F:C6:AB:B2:14:98:67:E7:06:70:F0:B2:4A:5C:AB:03:F3:89:AF:B0:11:AF:05:2D:D6\r\n',
244
+            'a=ice-options:trickle\r\n',
245
+            'a=sctp-port:5000\r\n',
246
+            'a=max-message-size:262144\r\n'
247
+        ].join('');
248
+
249
+        it('correctly groups ssrcs lines', () => {
250
+            const sdp = new SDP(testSdp);
251
+            const accept = $iq({
252
+                to: 'peerjid',
253
+                type: 'set'
254
+            })
255
+            .c('jingle', {
256
+                xmlns: 'urn:xmpp:jingle:1',
257
+                action: 'session-accept',
258
+                initiator: false,
259
+                responder: true,
260
+                sid: 'temp-sid'
261
+            });
262
+
263
+            sdp.toJingle(accept, 'responder');
264
+            const { nodeTree } = accept;
265
+            const content = nodeTree.querySelectorAll('jingle>content');
266
+
267
+            expect(content.length).toBe(3);
268
+            const videoSources = nodeTree.querySelectorAll('description[media=\'video\']>source');
269
+
270
+            expect(videoSources.length).toBe(6);
271
+            const audioSources = nodeTree.querySelectorAll('description[media=\'audio\']>source');
272
+
273
+            expect(audioSources.length).toBe(1);
274
+            const videoSourceGroups = nodeTree.querySelectorAll('description[media=\'video\']>ssrc-group');
275
+
276
+            expect(videoSourceGroups.length).toBe(4);
277
+            const data = nodeTree.querySelectorAll('jingle>content[name=\'data\']');
278
+
279
+            expect(data.length).toBe(1);
280
+        });
281
+    });
282
+
283
+    describe('toJingle for multiple m-lines with recv-only', () => {
284
+        const testSdp = [
285
+            'v=0\r\n',
286
+            'o=- 8014175770430016012 6 IN IP4 127.0.0.1\r\n',
287
+            's=-\r\n',
288
+            't=0 0\r\n',
289
+            'a=msid-semantic:  WMS\r\n',
290
+            'a=group:BUNDLE 0 1 2 3 4 5 6 7\r\n',
291
+            'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
292
+            'c=IN IP4 0.0.0.0\r\n',
293
+            'a=rtpmap:111 opus/48000/2\r\n',
294
+            'a=rtpmap:126 telephone-event/8000\r\n',
295
+            'a=fmtp:111 minptime=10;useinbandfec=1\r\n',
296
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
297
+            'a=rtcp-fb:111 transport-cc\r\n',
298
+            'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n',
299
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
300
+            'a=setup:active\r\n',
301
+            'a=mid:0\r\n',
302
+            'a=msid:- 836692af-4ea9-432f-811c-fef6ec7ee612\r\n',
303
+            'a=sendrecv\r\n',
304
+            'a=ice-ufrag:/5Yo\r\n',
305
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
306
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
307
+            'a=candidate:4240059272 1 UDP 2122260223 x.x.x.x 54192 typ host\r\n',
308
+            'a=ice-options:trickle\r\n',
309
+            'a=ssrc:2833013218 cname:0T+Z3AzTbva5NoHF\r\n',
310
+            'a=ssrc:2833013218 msid:- 836692af-4ea9-432f-811c-fef6ec7ee612\r\n',
311
+            'a=ssrc:2833013218 name:abcd-a0\r\n',
312
+            'a=rtcp-mux\r\n',
313
+            'a=extmap-allow-mixed\r\n',
314
+            'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n',
315
+            'c=IN IP4 0.0.0.0\r\n',
316
+            'a=rtpmap:101 VP9/90000\r\n',
317
+            'a=rtpmap:97 rtx/90000\r\n',
318
+            'a=rtpmap:100 VP8/90000\r\n',
319
+            'a=rtpmap:96 rtx/90000\r\n',
320
+            'a=rtpmap:107 H264/90000\r\n',
321
+            'a=rtpmap:99 rtx/90000\r\n',
322
+            'a=rtpmap:41 AV1/90000\r\n',
323
+            'a=rtpmap:42 rtx/90000\r\n',
324
+            'a=fmtp:101 profile-id=0\r\n',
325
+            'a=fmtp:97 apt=101\r\n',
326
+            'a=fmtp:96 apt=100\r\n',
327
+            'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n',
328
+            'a=fmtp:99 apt=107\r\n',
329
+            'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n',
330
+            'a=fmtp:42 apt=41\r\n',
331
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
332
+            'a=rtcp-fb:101 ccm fir\r\n',
333
+            'a=rtcp-fb:101 nack\r\n',
334
+            'a=rtcp-fb:101 nack pli\r\n',
335
+            'a=rtcp-fb:101 transport-cc\r\n',
336
+            'a=rtcp-fb:97 ccm fir\r\n',
337
+            'a=rtcp-fb:97 nack\r\n',
338
+            'a=rtcp-fb:97 nack pli\r\n',
339
+            'a=rtcp-fb:100 ccm fir\r\n',
340
+            'a=rtcp-fb:100 nack\r\n',
341
+            'a=rtcp-fb:100 nack pli\r\n',
342
+            'a=rtcp-fb:100 transport-cc\r\n',
343
+            'a=rtcp-fb:96 ccm fir\r\n',
344
+            'a=rtcp-fb:96 nack\r\n',
345
+            'a=rtcp-fb:96 nack pli\r\n',
346
+            'a=rtcp-fb:107 ccm fir\r\n',
347
+            'a=rtcp-fb:107 nack\r\n',
348
+            'a=rtcp-fb:107 nack pli\r\n',
349
+            'a=rtcp-fb:107 transport-cc\r\n',
350
+            'a=rtcp-fb:41 ccm fir\r\n',
351
+            'a=rtcp-fb:41 nack\r\n',
352
+            'a=rtcp-fb:41 nack pli\r\n',
353
+            'a=rtcp-fb:41 transport-cc\r\n',
354
+            'a=rtcp-fb:42 ccm fir\r\n',
355
+            'a=rtcp-fb:42 nack\r\n',
356
+            'a=rtcp-fb:42 nack pli\r\n',
357
+            'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n',
358
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
359
+            'a=setup:active\r\n',
360
+            'a=mid:1\r\n',
361
+            'a=msid:- 72254a21-ae73-4c0e-bd47-e84a2d3b9474\r\n',
362
+            'a=sendrecv\r\n',
363
+            'a=ice-ufrag:/5Yo\r\n',
364
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
365
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
366
+            'a=ice-options:trickle\r\n',
367
+            'a=ssrc:1261622218 cname:0T+Z3AzTbva5NoHF\r\n',
368
+            'a=ssrc:1261622218 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n',
369
+            'a=ssrc:1261622218 videoType:camera\r\n',
370
+            'a=ssrc:1261622218 name:abcd-v0\r\n',
371
+            'a=ssrc:2809057491 cname:0T+Z3AzTbva5NoHF\r\n',
372
+            'a=ssrc:2809057491 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n',
373
+            'a=ssrc:2809057491 videoType:camera\r\n',
374
+            'a=ssrc:2809057491 name:abcd-v0\r\n',
375
+            'a=ssrc:4223705690 cname:0T+Z3AzTbva5NoHF\r\n',
376
+            'a=ssrc:4223705690 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n',
377
+            'a=ssrc:4223705690 videoType:camera\r\n',
378
+            'a=ssrc:4223705690 name:abcd-v0\r\n',
379
+            'a=ssrc:44482421 cname:0T+Z3AzTbva5NoHF\r\n',
380
+            'a=ssrc:44482421 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n',
381
+            'a=ssrc:44482421 videoType:camera\r\n',
382
+            'a=ssrc:44482421 name:abcd-v0\r\n',
383
+            'a=ssrc:1408200021 cname:0T+Z3AzTbva5NoHF\r\n',
384
+            'a=ssrc:1408200021 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n',
385
+            'a=ssrc:1408200021 videoType:camera\r\n',
386
+            'a=ssrc:1408200021 name:abcd-v0\r\n',
387
+            'a=ssrc:712505591 cname:0T+Z3AzTbva5NoHF\r\n',
388
+            'a=ssrc:712505591 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n',
389
+            'a=ssrc:712505591 videoType:camera\r\n',
390
+            'a=ssrc:712505591 name:abcd-v0\r\n',
391
+            'a=ssrc-group:FID 1261622218 2809057491\r\n',
392
+            'a=ssrc-group:SIM 1261622218 4223705690 44482421\r\n',
393
+            'a=ssrc-group:FID 4223705690 1408200021\r\n',
394
+            'a=ssrc-group:FID 44482421 712505591\r\n',
395
+            'a=rtcp-mux\r\n',
396
+            'a=extmap-allow-mixed\r\n',
397
+            'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n',
398
+            'c=IN IP4 0.0.0.0\r\n',
399
+            'a=setup:active\r\n',
400
+            'a=mid:2\r\n',
401
+            'a=ice-ufrag:/5Yo\r\n',
402
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
403
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
404
+            'a=ice-options:trickle\r\n',
405
+            'a=sctp-port:5000\r\n',
406
+            'a=max-message-size:262144\r\n',
407
+            'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n',
408
+            'c=IN IP4 0.0.0.0\r\n',
409
+            'a=rtpmap:101 VP9/90000\r\n',
410
+            'a=rtpmap:97 rtx/90000\r\n',
411
+            'a=rtpmap:100 VP8/90000\r\n',
412
+            'a=rtpmap:96 rtx/90000\r\n',
413
+            'a=rtpmap:107 H264/90000\r\n',
414
+            'a=rtpmap:99 rtx/90000\r\n',
415
+            'a=rtpmap:41 AV1/90000\r\n',
416
+            'a=rtpmap:42 rtx/90000\r\n',
417
+            'a=fmtp:101 profile-id=0\r\n',
418
+            'a=fmtp:97 apt=101\r\n',
419
+            'a=fmtp:96 apt=100\r\n',
420
+            'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n',
421
+            'a=fmtp:99 apt=107\r\n',
422
+            'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n',
423
+            'a=fmtp:42 apt=41\r\n',
424
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
425
+            'a=rtcp-fb:101 ccm fir\r\n',
426
+            'a=rtcp-fb:101 nack\r\n',
427
+            'a=rtcp-fb:101 nack pli\r\n',
428
+            'a=rtcp-fb:101 transport-cc\r\n',
429
+            'a=rtcp-fb:97 ccm fir\r\n',
430
+            'a=rtcp-fb:97 nack\r\n',
431
+            'a=rtcp-fb:97 nack pli\r\n',
432
+            'a=rtcp-fb:100 ccm fir\r\n',
433
+            'a=rtcp-fb:100 nack\r\n',
434
+            'a=rtcp-fb:100 nack pli\r\n',
435
+            'a=rtcp-fb:100 transport-cc\r\n',
436
+            'a=rtcp-fb:96 ccm fir\r\n',
437
+            'a=rtcp-fb:96 nack\r\n',
438
+            'a=rtcp-fb:96 nack pli\r\n',
439
+            'a=rtcp-fb:107 ccm fir\r\n',
440
+            'a=rtcp-fb:107 nack\r\n',
441
+            'a=rtcp-fb:107 nack pli\r\n',
442
+            'a=rtcp-fb:107 transport-cc\r\n',
443
+            'a=rtcp-fb:41 ccm fir\r\n',
444
+            'a=rtcp-fb:41 nack\r\n',
445
+            'a=rtcp-fb:41 nack pli\r\n',
446
+            'a=rtcp-fb:41 transport-cc\r\n',
447
+            'a=rtcp-fb:42 ccm fir\r\n',
448
+            'a=rtcp-fb:42 nack\r\n',
449
+            'a=rtcp-fb:42 nack pli\r\n',
450
+            'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n',
451
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
452
+            'a=extmap:11 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension\r\n',
453
+            'a=setup:active\r\n',
454
+            'a=mid:3\r\n',
455
+            'a=recvonly\r\n',
456
+            'a=ice-ufrag:/5Yo\r\n',
457
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
458
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
459
+            'a=ice-options:trickle\r\n',
460
+            'a=rtcp-mux\r\n',
461
+            'a=extmap-allow-mixed\r\n',
462
+            'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n',
463
+            'c=IN IP4 0.0.0.0\r\n',
464
+            'a=rtpmap:101 VP9/90000\r\n',
465
+            'a=rtpmap:97 rtx/90000\r\n',
466
+            'a=rtpmap:100 VP8/90000\r\n',
467
+            'a=rtpmap:96 rtx/90000\r\n',
468
+            'a=rtpmap:107 H264/90000\r\n',
469
+            'a=rtpmap:99 rtx/90000\r\n',
470
+            'a=rtpmap:41 AV1/90000\r\n',
471
+            'a=rtpmap:42 rtx/90000\r\n',
472
+            'a=fmtp:101 profile-id=0\r\n',
473
+            'a=fmtp:97 apt=101\r\n',
474
+            'a=fmtp:96 apt=100\r\n',
475
+            'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n',
476
+            'a=fmtp:99 apt=107\r\n',
477
+            'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n',
478
+            'a=fmtp:42 apt=41\r\n',
479
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
480
+            'a=rtcp-fb:101 ccm fir\r\n',
481
+            'a=rtcp-fb:101 nack\r\n',
482
+            'a=rtcp-fb:101 nack pli\r\n',
483
+            'a=rtcp-fb:101 transport-cc\r\n',
484
+            'a=rtcp-fb:97 ccm fir\r\n',
485
+            'a=rtcp-fb:97 nack\r\n',
486
+            'a=rtcp-fb:97 nack pli\r\n',
487
+            'a=rtcp-fb:100 ccm fir\r\n',
488
+            'a=rtcp-fb:100 nack\r\n',
489
+            'a=rtcp-fb:100 nack pli\r\n',
490
+            'a=rtcp-fb:100 transport-cc\r\n',
491
+            'a=rtcp-fb:96 ccm fir\r\n',
492
+            'a=rtcp-fb:96 nack\r\n',
493
+            'a=rtcp-fb:96 nack pli\r\n',
494
+            'a=rtcp-fb:107 ccm fir\r\n',
495
+            'a=rtcp-fb:107 nack\r\n',
496
+            'a=rtcp-fb:107 nack pli\r\n',
497
+            'a=rtcp-fb:107 transport-cc\r\n',
498
+            'a=rtcp-fb:41 ccm fir\r\n',
499
+            'a=rtcp-fb:41 nack\r\n',
500
+            'a=rtcp-fb:41 nack pli\r\n',
501
+            'a=rtcp-fb:41 transport-cc\r\n',
502
+            'a=rtcp-fb:42 ccm fir\r\n',
503
+            'a=rtcp-fb:42 nack\r\n',
504
+            'a=rtcp-fb:42 nack pli\r\n',
505
+            'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n',
506
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
507
+            'a=extmap:11 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension\r\n',
508
+            'a=setup:active\r\n',
509
+            'a=mid:4\r\n',
510
+            'a=recvonly\r\n',
511
+            'a=ice-ufrag:/5Yo\r\n',
512
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
513
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
514
+            'a=ice-options:trickle\r\n',
515
+            'a=rtcp-mux\r\n',
516
+            'a=extmap-allow-mixed\r\n',
517
+            'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
518
+            'c=IN IP4 0.0.0.0\r\n',
519
+            'a=rtpmap:111 opus/48000/2\r\n',
520
+            'a=rtpmap:126 telephone-event/8000\r\n',
521
+            'a=fmtp:111 minptime=10;useinbandfec=1\r\n',
522
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
523
+            'a=rtcp-fb:111 transport-cc\r\n',
524
+            'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n',
525
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
526
+            'a=setup:active\r\n',
527
+            'a=mid:5\r\n',
528
+            'a=recvonly\r\n',
529
+            'a=ice-ufrag:/5Yo\r\n',
530
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
531
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
532
+            'a=ice-options:trickle\r\n',
533
+            'a=rtcp-mux\r\n',
534
+            'a=extmap-allow-mixed\r\n',
535
+            'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
536
+            'c=IN IP4 0.0.0.0\r\n',
537
+            'a=rtpmap:111 opus/48000/2\r\n',
538
+            'a=rtpmap:126 telephone-event/8000\r\n',
539
+            'a=fmtp:111 minptime=10;useinbandfec=1\r\n',
540
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
541
+            'a=rtcp-fb:111 transport-cc\r\n',
542
+            'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n',
543
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
544
+            'a=setup:active\r\n',
545
+            'a=mid:6\r\n',
546
+            'a=recvonly\r\n',
547
+            'a=ice-ufrag:/5Yo\r\n',
548
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
549
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
550
+            'a=ice-options:trickle\r\n',
551
+            'a=rtcp-mux\r\n',
552
+            'a=extmap-allow-mixed\r\n',
553
+            'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n',
554
+            'c=IN IP4 0.0.0.0\r\n',
555
+            'a=rtpmap:101 VP9/90000\r\n',
556
+            'a=rtpmap:97 rtx/90000\r\n',
557
+            'a=rtpmap:100 VP8/90000\r\n',
558
+            'a=rtpmap:96 rtx/90000\r\n',
559
+            'a=rtpmap:107 H264/90000\r\n',
560
+            'a=rtpmap:99 rtx/90000\r\n',
561
+            'a=rtpmap:41 AV1/90000\r\n',
562
+            'a=rtpmap:42 rtx/90000\r\n',
563
+            'a=fmtp:101 profile-id=0\r\n',
564
+            'a=fmtp:97 apt=101\r\n',
565
+            'a=fmtp:96 apt=100\r\n',
566
+            'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n',
567
+            'a=fmtp:99 apt=107\r\n',
568
+            'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n',
569
+            'a=fmtp:42 apt=41\r\n',
570
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
571
+            'a=rtcp-fb:101 ccm fir\r\n',
572
+            'a=rtcp-fb:101 nack\r\n',
573
+            'a=rtcp-fb:101 nack pli\r\n',
574
+            'a=rtcp-fb:101 transport-cc\r\n',
575
+            'a=rtcp-fb:97 ccm fir\r\n',
576
+            'a=rtcp-fb:97 nack\r\n',
577
+            'a=rtcp-fb:97 nack pli\r\n',
578
+            'a=rtcp-fb:100 ccm fir\r\n',
579
+            'a=rtcp-fb:100 nack\r\n',
580
+            'a=rtcp-fb:100 nack pli\r\n',
581
+            'a=rtcp-fb:100 transport-cc\r\n',
582
+            'a=rtcp-fb:96 ccm fir\r\n',
583
+            'a=rtcp-fb:96 nack\r\n',
584
+            'a=rtcp-fb:96 nack pli\r\n',
585
+            'a=rtcp-fb:107 ccm fir\r\n',
586
+            'a=rtcp-fb:107 nack\r\n',
587
+            'a=rtcp-fb:107 nack pli\r\n',
588
+            'a=rtcp-fb:107 transport-cc\r\n',
589
+            'a=rtcp-fb:41 ccm fir\r\n',
590
+            'a=rtcp-fb:41 nack\r\n',
591
+            'a=rtcp-fb:41 nack pli\r\n',
592
+            'a=rtcp-fb:41 transport-cc\r\n',
593
+            'a=rtcp-fb:42 ccm fir\r\n',
594
+            'a=rtcp-fb:42 nack\r\n',
595
+            'a=rtcp-fb:42 nack pli\r\n',
596
+            'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n',
597
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
598
+            'a=setup:active\r\n',
599
+            'a=mid:7\r\n',
600
+            'a=msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n',
601
+            'a=sendonly\r\n',
602
+            'a=ice-ufrag:/5Yo\r\n',
603
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
604
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
605
+            'a=ice-options:trickle\r\n',
606
+            'a=ssrc:4074534577 cname:0T+Z3AzTbva5NoHF\r\n',
607
+            'a=ssrc:4074534577 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n',
608
+            'a=ssrc:4074534577 videoType:desktop\r\n',
609
+            'a=ssrc:4074534577 name:abcd-v1\r\n',
610
+            'a=ssrc:3122913012 cname:0T+Z3AzTbva5NoHF\r\n',
611
+            'a=ssrc:3122913012 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n',
612
+            'a=ssrc:3122913012 videoType:desktop\r\n',
613
+            'a=ssrc:3122913012 name:abcd-v1\r\n',
614
+            'a=ssrc:3145321104 cname:0T+Z3AzTbva5NoHF\r\n',
615
+            'a=ssrc:3145321104 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n',
616
+            'a=ssrc:3145321104 videoType:desktop\r\n',
617
+            'a=ssrc:3145321104 name:abcd-v1\r\n',
618
+            'a=ssrc:2686550307 cname:0T+Z3AzTbva5NoHF\r\n',
619
+            'a=ssrc:2686550307 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n',
620
+            'a=ssrc:2686550307 videoType:desktop\r\n',
621
+            'a=ssrc:2686550307 name:abcd-v1\r\n',
622
+            'a=ssrc:2960588630 cname:0T+Z3AzTbva5NoHF\r\n',
623
+            'a=ssrc:2960588630 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n',
624
+            'a=ssrc:2960588630 videoType:desktop\r\n',
625
+            'a=ssrc:2960588630 name:abcd-v1\r\n',
626
+            'a=ssrc:3495860096 cname:0T+Z3AzTbva5NoHF\r\n',
627
+            'a=ssrc:3495860096 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n',
628
+            'a=ssrc:3495860096 videoType:desktop\r\n',
629
+            'a=ssrc:3495860096 name:abcd-v1\r\n',
630
+            'a=ssrc-group:FID 4074534577 3122913012\r\n',
631
+            'a=ssrc-group:SIM 4074534577 3145321104 2686550307\r\n',
632
+            'a=ssrc-group:FID 3145321104 2960588630\r\n',
633
+            'a=ssrc-group:FID 2686550307 3495860096\r\n',
634
+            'a=rtcp-mux\r\n',
635
+            'a=extmap-allow-mixed\r\n'
636
+        ].join('');
637
+
638
+        it('correctly groups ssrcs lines', () => {
639
+            const sdp = new SDP(testSdp);
640
+            const accept = $iq({
641
+                to: 'peerjid',
642
+                type: 'set'
643
+            })
644
+            .c('jingle', {
645
+                xmlns: 'urn:xmpp:jingle:1',
646
+                action: 'session-accept',
647
+                initiator: false,
648
+                responder: true,
649
+                sid: 'temp-sid'
650
+            });
651
+
652
+            sdp.toJingle(accept, 'responder');
653
+            const { nodeTree } = accept;
654
+            const content = nodeTree.querySelectorAll('jingle>content');
655
+
656
+            expect(content.length).toBe(3);
657
+            const videoSources = nodeTree.querySelectorAll('description[media=\'video\']>source');
658
+
659
+            expect(videoSources.length).toBe(12);
660
+            const audioSources = nodeTree.querySelectorAll('description[media=\'audio\']>source');
661
+
662
+            expect(audioSources.length).toBe(1);
663
+            const videoSourceGroups = nodeTree.querySelectorAll('description[media=\'video\']>ssrc-group');
664
+
665
+            expect(videoSourceGroups.length).toBe(8);
666
+            const data = nodeTree.querySelectorAll('jingle>content[name=\'data\']');
667
+
668
+            expect(data.length).toBe(1);
669
+        });
670
+    });
671
+
672
+    describe('toJingle for multiple m-lines with only recv-only', () => {
673
+        const testSdp = [
674
+            'v=0\r\n',
675
+            'o=- 8014175770430016012 6 IN IP4 127.0.0.1\r\n',
676
+            's=-\r\n',
677
+            't=0 0\r\n',
678
+            'a=msid-semantic:  WMS\r\n',
679
+            'a=group:BUNDLE 0 1 2 3 4 5 6 7\r\n',
680
+            'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
681
+            'c=IN IP4 0.0.0.0\r\n',
682
+            'a=rtpmap:111 opus/48000/2\r\n',
683
+            'a=rtpmap:126 telephone-event/8000\r\n',
684
+            'a=fmtp:111 minptime=10;useinbandfec=1\r\n',
685
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
686
+            'a=rtcp-fb:111 transport-cc\r\n',
687
+            'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n',
688
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
689
+            'a=setup:active\r\n',
690
+            'a=mid:0\r\n',
691
+            'a=msid:- 836692af-4ea9-432f-811c-fef6ec7ee612\r\n',
692
+            'a=recvonly\r\n',
693
+            'a=ice-ufrag:/5Yo\r\n',
694
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
695
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
696
+            'a=candidate:4240059272 1 UDP 2122260223 x.x.x.x 54192 typ host\r\n',
697
+            'a=ice-options:trickle\r\n',
698
+            'a=rtcp-mux\r\n',
699
+            'a=extmap-allow-mixed\r\n',
700
+            'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n',
701
+            'c=IN IP4 0.0.0.0\r\n',
702
+            'a=rtpmap:101 VP9/90000\r\n',
703
+            'a=rtpmap:97 rtx/90000\r\n',
704
+            'a=rtpmap:100 VP8/90000\r\n',
705
+            'a=rtpmap:96 rtx/90000\r\n',
706
+            'a=rtpmap:107 H264/90000\r\n',
707
+            'a=rtpmap:99 rtx/90000\r\n',
708
+            'a=rtpmap:41 AV1/90000\r\n',
709
+            'a=rtpmap:42 rtx/90000\r\n',
710
+            'a=fmtp:101 profile-id=0\r\n',
711
+            'a=fmtp:97 apt=101\r\n',
712
+            'a=fmtp:96 apt=100\r\n',
713
+            'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n',
714
+            'a=fmtp:99 apt=107\r\n',
715
+            'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n',
716
+            'a=fmtp:42 apt=41\r\n',
717
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
718
+            'a=rtcp-fb:101 ccm fir\r\n',
719
+            'a=rtcp-fb:101 nack\r\n',
720
+            'a=rtcp-fb:101 nack pli\r\n',
721
+            'a=rtcp-fb:101 transport-cc\r\n',
722
+            'a=rtcp-fb:97 ccm fir\r\n',
723
+            'a=rtcp-fb:97 nack\r\n',
724
+            'a=rtcp-fb:97 nack pli\r\n',
725
+            'a=rtcp-fb:100 ccm fir\r\n',
726
+            'a=rtcp-fb:100 nack\r\n',
727
+            'a=rtcp-fb:100 nack pli\r\n',
728
+            'a=rtcp-fb:100 transport-cc\r\n',
729
+            'a=rtcp-fb:96 ccm fir\r\n',
730
+            'a=rtcp-fb:96 nack\r\n',
731
+            'a=rtcp-fb:96 nack pli\r\n',
732
+            'a=rtcp-fb:107 ccm fir\r\n',
733
+            'a=rtcp-fb:107 nack\r\n',
734
+            'a=rtcp-fb:107 nack pli\r\n',
735
+            'a=rtcp-fb:107 transport-cc\r\n',
736
+            'a=rtcp-fb:41 ccm fir\r\n',
737
+            'a=rtcp-fb:41 nack\r\n',
738
+            'a=rtcp-fb:41 nack pli\r\n',
739
+            'a=rtcp-fb:41 transport-cc\r\n',
740
+            'a=rtcp-fb:42 ccm fir\r\n',
741
+            'a=rtcp-fb:42 nack\r\n',
742
+            'a=rtcp-fb:42 nack pli\r\n',
743
+            'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n',
744
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
745
+            'a=setup:active\r\n',
746
+            'a=mid:1\r\n',
747
+            'a=recvonly\r\n',
748
+            'a=ice-ufrag:/5Yo\r\n',
749
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
750
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
751
+            'a=ice-options:trickle\r\n',
752
+            'a=rtcp-mux\r\n',
753
+            'a=extmap-allow-mixed\r\n',
754
+            'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n',
755
+            'c=IN IP4 0.0.0.0\r\n',
756
+            'a=setup:active\r\n',
757
+            'a=mid:2\r\n',
758
+            'a=ice-ufrag:/5Yo\r\n',
759
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
760
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
761
+            'a=ice-options:trickle\r\n',
762
+            'a=sctp-port:5000\r\n',
763
+            'a=max-message-size:262144\r\n',
764
+            'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n',
765
+            'c=IN IP4 0.0.0.0\r\n',
766
+            'a=rtpmap:101 VP9/90000\r\n',
767
+            'a=rtpmap:97 rtx/90000\r\n',
768
+            'a=rtpmap:100 VP8/90000\r\n',
769
+            'a=rtpmap:96 rtx/90000\r\n',
770
+            'a=rtpmap:107 H264/90000\r\n',
771
+            'a=rtpmap:99 rtx/90000\r\n',
772
+            'a=rtpmap:41 AV1/90000\r\n',
773
+            'a=rtpmap:42 rtx/90000\r\n',
774
+            'a=fmtp:101 profile-id=0\r\n',
775
+            'a=fmtp:97 apt=101\r\n',
776
+            'a=fmtp:96 apt=100\r\n',
777
+            'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n',
778
+            'a=fmtp:99 apt=107\r\n',
779
+            'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n',
780
+            'a=fmtp:42 apt=41\r\n',
781
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
782
+            'a=rtcp-fb:101 ccm fir\r\n',
783
+            'a=rtcp-fb:101 nack\r\n',
784
+            'a=rtcp-fb:101 nack pli\r\n',
785
+            'a=rtcp-fb:101 transport-cc\r\n',
786
+            'a=rtcp-fb:97 ccm fir\r\n',
787
+            'a=rtcp-fb:97 nack\r\n',
788
+            'a=rtcp-fb:97 nack pli\r\n',
789
+            'a=rtcp-fb:100 ccm fir\r\n',
790
+            'a=rtcp-fb:100 nack\r\n',
791
+            'a=rtcp-fb:100 nack pli\r\n',
792
+            'a=rtcp-fb:100 transport-cc\r\n',
793
+            'a=rtcp-fb:96 ccm fir\r\n',
794
+            'a=rtcp-fb:96 nack\r\n',
795
+            'a=rtcp-fb:96 nack pli\r\n',
796
+            'a=rtcp-fb:107 ccm fir\r\n',
797
+            'a=rtcp-fb:107 nack\r\n',
798
+            'a=rtcp-fb:107 nack pli\r\n',
799
+            'a=rtcp-fb:107 transport-cc\r\n',
800
+            'a=rtcp-fb:41 ccm fir\r\n',
801
+            'a=rtcp-fb:41 nack\r\n',
802
+            'a=rtcp-fb:41 nack pli\r\n',
803
+            'a=rtcp-fb:41 transport-cc\r\n',
804
+            'a=rtcp-fb:42 ccm fir\r\n',
805
+            'a=rtcp-fb:42 nack\r\n',
806
+            'a=rtcp-fb:42 nack pli\r\n',
807
+            'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n',
808
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
809
+            'a=extmap:11 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension\r\n',
810
+            'a=setup:active\r\n',
811
+            'a=mid:3\r\n',
812
+            'a=recvonly\r\n',
813
+            'a=ice-ufrag:/5Yo\r\n',
814
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
815
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
816
+            'a=ice-options:trickle\r\n',
817
+            'a=rtcp-mux\r\n',
818
+            'a=extmap-allow-mixed\r\n',
819
+            'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n',
820
+            'c=IN IP4 0.0.0.0\r\n',
821
+            'a=rtpmap:101 VP9/90000\r\n',
822
+            'a=rtpmap:97 rtx/90000\r\n',
823
+            'a=rtpmap:100 VP8/90000\r\n',
824
+            'a=rtpmap:96 rtx/90000\r\n',
825
+            'a=rtpmap:107 H264/90000\r\n',
826
+            'a=rtpmap:99 rtx/90000\r\n',
827
+            'a=rtpmap:41 AV1/90000\r\n',
828
+            'a=rtpmap:42 rtx/90000\r\n',
829
+            'a=fmtp:101 profile-id=0\r\n',
830
+            'a=fmtp:97 apt=101\r\n',
831
+            'a=fmtp:96 apt=100\r\n',
832
+            'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n',
833
+            'a=fmtp:99 apt=107\r\n',
834
+            'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n',
835
+            'a=fmtp:42 apt=41\r\n',
836
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
837
+            'a=rtcp-fb:101 ccm fir\r\n',
838
+            'a=rtcp-fb:101 nack\r\n',
839
+            'a=rtcp-fb:101 nack pli\r\n',
840
+            'a=rtcp-fb:101 transport-cc\r\n',
841
+            'a=rtcp-fb:97 ccm fir\r\n',
842
+            'a=rtcp-fb:97 nack\r\n',
843
+            'a=rtcp-fb:97 nack pli\r\n',
844
+            'a=rtcp-fb:100 ccm fir\r\n',
845
+            'a=rtcp-fb:100 nack\r\n',
846
+            'a=rtcp-fb:100 nack pli\r\n',
847
+            'a=rtcp-fb:100 transport-cc\r\n',
848
+            'a=rtcp-fb:96 ccm fir\r\n',
849
+            'a=rtcp-fb:96 nack\r\n',
850
+            'a=rtcp-fb:96 nack pli\r\n',
851
+            'a=rtcp-fb:107 ccm fir\r\n',
852
+            'a=rtcp-fb:107 nack\r\n',
853
+            'a=rtcp-fb:107 nack pli\r\n',
854
+            'a=rtcp-fb:107 transport-cc\r\n',
855
+            'a=rtcp-fb:41 ccm fir\r\n',
856
+            'a=rtcp-fb:41 nack\r\n',
857
+            'a=rtcp-fb:41 nack pli\r\n',
858
+            'a=rtcp-fb:41 transport-cc\r\n',
859
+            'a=rtcp-fb:42 ccm fir\r\n',
860
+            'a=rtcp-fb:42 nack\r\n',
861
+            'a=rtcp-fb:42 nack pli\r\n',
862
+            'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n',
863
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
864
+            'a=extmap:11 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension\r\n',
865
+            'a=setup:active\r\n',
866
+            'a=mid:4\r\n',
867
+            'a=recvonly\r\n',
868
+            'a=ice-ufrag:/5Yo\r\n',
869
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
870
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
871
+            'a=ice-options:trickle\r\n',
872
+            'a=rtcp-mux\r\n',
873
+            'a=extmap-allow-mixed\r\n',
874
+            'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
875
+            'c=IN IP4 0.0.0.0\r\n',
876
+            'a=rtpmap:111 opus/48000/2\r\n',
877
+            'a=rtpmap:126 telephone-event/8000\r\n',
878
+            'a=fmtp:111 minptime=10;useinbandfec=1\r\n',
879
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
880
+            'a=rtcp-fb:111 transport-cc\r\n',
881
+            'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n',
882
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
883
+            'a=setup:active\r\n',
884
+            'a=mid:5\r\n',
885
+            'a=recvonly\r\n',
886
+            'a=ice-ufrag:/5Yo\r\n',
887
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
888
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
889
+            'a=ice-options:trickle\r\n',
890
+            'a=rtcp-mux\r\n',
891
+            'a=extmap-allow-mixed\r\n',
892
+            'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
893
+            'c=IN IP4 0.0.0.0\r\n',
894
+            'a=rtpmap:111 opus/48000/2\r\n',
895
+            'a=rtpmap:126 telephone-event/8000\r\n',
896
+            'a=fmtp:111 minptime=10;useinbandfec=1\r\n',
897
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
898
+            'a=rtcp-fb:111 transport-cc\r\n',
899
+            'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n',
900
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
901
+            'a=setup:active\r\n',
902
+            'a=mid:6\r\n',
903
+            'a=recvonly\r\n',
904
+            'a=ice-ufrag:/5Yo\r\n',
905
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
906
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
907
+            'a=ice-options:trickle\r\n',
908
+            'a=rtcp-mux\r\n',
909
+            'a=extmap-allow-mixed\r\n',
910
+            'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n',
911
+            'c=IN IP4 0.0.0.0\r\n',
912
+            'a=rtpmap:101 VP9/90000\r\n',
913
+            'a=rtpmap:97 rtx/90000\r\n',
914
+            'a=rtpmap:100 VP8/90000\r\n',
915
+            'a=rtpmap:96 rtx/90000\r\n',
916
+            'a=rtpmap:107 H264/90000\r\n',
917
+            'a=rtpmap:99 rtx/90000\r\n',
918
+            'a=rtpmap:41 AV1/90000\r\n',
919
+            'a=rtpmap:42 rtx/90000\r\n',
920
+            'a=fmtp:101 profile-id=0\r\n',
921
+            'a=fmtp:97 apt=101\r\n',
922
+            'a=fmtp:96 apt=100\r\n',
923
+            'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n',
924
+            'a=fmtp:99 apt=107\r\n',
925
+            'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n',
926
+            'a=fmtp:42 apt=41\r\n',
927
+            'a=rtcp:9 IN IP4 0.0.0.0\r\n',
928
+            'a=rtcp-fb:101 ccm fir\r\n',
929
+            'a=rtcp-fb:101 nack\r\n',
930
+            'a=rtcp-fb:101 nack pli\r\n',
931
+            'a=rtcp-fb:101 transport-cc\r\n',
932
+            'a=rtcp-fb:97 ccm fir\r\n',
933
+            'a=rtcp-fb:97 nack\r\n',
934
+            'a=rtcp-fb:97 nack pli\r\n',
935
+            'a=rtcp-fb:100 ccm fir\r\n',
936
+            'a=rtcp-fb:100 nack\r\n',
937
+            'a=rtcp-fb:100 nack pli\r\n',
938
+            'a=rtcp-fb:100 transport-cc\r\n',
939
+            'a=rtcp-fb:96 ccm fir\r\n',
940
+            'a=rtcp-fb:96 nack\r\n',
941
+            'a=rtcp-fb:96 nack pli\r\n',
942
+            'a=rtcp-fb:107 ccm fir\r\n',
943
+            'a=rtcp-fb:107 nack\r\n',
944
+            'a=rtcp-fb:107 nack pli\r\n',
945
+            'a=rtcp-fb:107 transport-cc\r\n',
946
+            'a=rtcp-fb:41 ccm fir\r\n',
947
+            'a=rtcp-fb:41 nack\r\n',
948
+            'a=rtcp-fb:41 nack pli\r\n',
949
+            'a=rtcp-fb:41 transport-cc\r\n',
950
+            'a=rtcp-fb:42 ccm fir\r\n',
951
+            'a=rtcp-fb:42 nack\r\n',
952
+            'a=rtcp-fb:42 nack pli\r\n',
953
+            'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n',
954
+            'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n',
955
+            'a=setup:active\r\n',
956
+            'a=mid:7\r\n',
957
+            'a=recvonly\r\n',
958
+            'a=ice-ufrag:/5Yo\r\n',
959
+            'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n',
960
+            'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n',
961
+            'a=ice-options:trickle\r\n',
962
+            'a=rtcp-mux\r\n',
963
+            'a=extmap-allow-mixed\r\n'
964
+        ].join('');
965
+
966
+        it('correctly groups ssrcs lines', () => {
967
+            const sdp = new SDP(testSdp);
968
+            const accept = $iq({
969
+                to: 'peerjid',
970
+                type: 'set'
971
+            })
972
+            .c('jingle', {
973
+                xmlns: 'urn:xmpp:jingle:1',
974
+                action: 'session-accept',
975
+                initiator: false,
976
+                responder: true,
977
+                sid: 'temp-sid'
978
+            });
979
+
980
+            sdp.toJingle(accept, 'responder');
981
+            const { nodeTree } = accept;
982
+            const content = nodeTree.querySelectorAll('jingle>content');
983
+
984
+            expect(content.length).toBe(3);
985
+            const videoSources = nodeTree.querySelectorAll('description[media=\'video\']>source');
986
+
987
+            expect(videoSources.length).toBe(0);
988
+            const audioSources = nodeTree.querySelectorAll('description[media=\'audio\']>source');
989
+
990
+            expect(audioSources.length).toBe(0);
991
+            const videoSourceGroups = nodeTree.querySelectorAll('description[media=\'video\']>ssrc-group');
992
+
993
+            expect(videoSourceGroups.length).toBe(0);
994
+            const data = nodeTree.querySelectorAll('jingle>content[name=\'data\']');
995
+
996
+            expect(data.length).toBe(1);
997
+        });
998
+    });
999
+
138
     describe('fromJingle', () => {
1000
     describe('fromJingle', () => {
139
-        /* eslint-disable max-len*/
140
         const stanza = `<iq>
1001
         const stanza = `<iq>
141
 <jingle action='session-initiate' initiator='focus' sid='123' xmlns='urn:xmpp:jingle:1'>
1002
 <jingle action='session-initiate' initiator='focus' sid='123' xmlns='urn:xmpp:jingle:1'>
142
     <content creator='initiator' name='audio' senders='both'>
1003
     <content creator='initiator' name='audio' senders='both'>
269
 a=ssrc:3758540092 msid:mixedmslabel mixedlabelvideo0
1130
 a=ssrc:3758540092 msid:mixedmslabel mixedlabelvideo0
270
 a=ssrc:3758540092 mslabel:mixedmslabel
1131
 a=ssrc:3758540092 mslabel:mixedmslabel
271
 `.split('\n').join('\r\n');
1132
 `.split('\n').join('\r\n');
272
-        /* eslint-enable max-len*/
273
 
1133
 
274
         it('gets converted to SDP', () => {
1134
         it('gets converted to SDP', () => {
275
             const offer = createStanzaElement(stanza);
1135
             const offer = createStanzaElement(stanza);
283
     });
1143
     });
284
 
1144
 
285
     describe('fromJingleWithJSONFormat', () => {
1145
     describe('fromJingleWithJSONFormat', () => {
286
-        /* eslint-disable max-len*/
287
         const stanza = `
1146
         const stanza = `
288
     <iq>
1147
     <iq>
289
         <jingle xmlns="urn:xmpp:jingle:1" action="session-initiate" initiator="focus" sid="123">
1148
         <jingle xmlns="urn:xmpp:jingle:1" action="session-initiate" initiator="focus" sid="123">

+ 94
- 193
modules/sdp/SDPDiffer.js Visa fil

1
+import { isEqual } from 'lodash-es';
1
 
2
 
2
 import { XEP } from '../../service/xmpp/XMPPExtensioProtocols';
3
 import { XEP } from '../../service/xmpp/XMPPExtensioProtocols';
3
 
4
 
4
 import SDPUtil from './SDPUtil';
5
 import SDPUtil from './SDPUtil';
5
 
6
 
6
-// this could be useful in Array.prototype.
7
 /**
7
 /**
8
- *
9
- * @param array1
10
- * @param array2
8
+ * A class that provides methods for comparing the source information present in two different SDPs so that the delta
9
+ * can be signaled to Jicofo via 'source-remove' or 'source-add'.
11
  */
10
  */
12
-function arrayEquals(array1, array2) {
13
-    // if the other array is a falsy value, return
14
-    if (!array2) {
15
-        return false;
11
+export class SDPDiffer {
12
+    /**
13
+     * Constructor.
14
+     *
15
+     * @param {SDP} mySdp - the new SDP.
16
+     * @param {SDP} othersSdp - the old SDP.
17
+     * @param {boolean} isP2P - Whether the SDPs belong to a p2p peerconnection.
18
+     */
19
+    constructor(mySdp, othersSdp, isP2P = false) {
20
+        this.isP2P = isP2P;
21
+        this.mySdp = mySdp;
22
+        this.othersSdp = othersSdp;
16
     }
23
     }
17
 
24
 
18
-    // compare lengths - can save a lot of time
19
-    if (array1.length !== array2.length) {
20
-        return false;
21
-    }
25
+    /**
26
+     * Returns a map of the sources that are present in 'othersSdp' but not in 'mySdp'.
27
+     *
28
+     * @returns {*}
29
+     */
30
+    getNewMedia() {
31
+        const mySources = this.mySdp.getMediaSsrcMap();
32
+        const othersSources = this.othersSdp.getMediaSsrcMap();
33
+        const diff = {};
34
+
35
+        for (const [ index, othersSource ] of othersSources.entries()) {
36
+            const mySource = mySources.get(index);
37
+
38
+            if (!mySource) {
39
+                diff[index] = othersSource;
40
+                continue; // eslint-disable-line no-continue
41
+            }
42
+
43
+            const othersSsrcs = Object.keys(othersSource.ssrcs);
22
 
44
 
23
-    for (let i = 0, l = array1.length; i < l; i++) {
24
-        // Check if we have nested arrays
25
-        if (array1[i] instanceof Array && array2[i] instanceof Array) {
26
-            // recurse into the nested arrays
27
-            if (!array1[i].equals(array2[i])) {
28
-                return false;
45
+            if (othersSsrcs.length && !isEqual(Object.keys(mySource.ssrcs).sort(), [ ...othersSsrcs ].sort())) {
46
+                diff[index] = othersSource;
29
             }
47
             }
30
-        } else if (array1[i] !== array2[i]) {
31
-            // Warning - two different object instances will never be
32
-            // equal: {x:20} != {x:20}
33
-            return false;
34
         }
48
         }
35
-    }
36
-
37
-    return true;
38
-}
39
 
49
 
40
-/**
41
- *
42
- * @param mySDP
43
- * @param otherSDP
44
- */
45
-export default function SDPDiffer(mySDP, otherSDP) {
46
-    this.mySDP = mySDP;
47
-    this.otherSDP = otherSDP;
48
-    if (!mySDP) {
49
-        throw new Error('"mySDP" is undefined!');
50
-    } else if (!otherSDP) {
51
-        throw new Error('"otherSDP" is undefined!');
50
+        return diff;
52
     }
51
     }
53
-}
54
 
52
 
55
-/**
56
- * Returns map of MediaChannel that contains media contained in
57
- * 'mySDP', but not contained in 'otherSdp'. Mapped by channel idx.
58
- */
59
-SDPDiffer.prototype.getNewMedia = function() {
60
-
61
-    const myMedias = this.mySDP.getMediaSsrcMap();
62
-    const othersMedias = this.otherSDP.getMediaSsrcMap();
63
-    const newMedia = {};
64
-
65
-    Object.keys(othersMedias).forEach(othersMediaIdx => {
66
-        const myMedia = myMedias[othersMediaIdx];
67
-        const othersMedia = othersMedias[othersMediaIdx];
53
+    /**
54
+     * Adds the diff source info to the provided IQ stanza.
55
+     *
56
+     * @param {*} modify - Stanza IQ.
57
+     * @returns {boolean}
58
+     */
59
+    toJingle(modify) {
60
+        let modified = false;
61
+        const diffSourceInfo = this.getNewMedia();
62
+
63
+        for (const media of Object.values(diffSourceInfo)) {
64
+            modified = true;
65
+            modify.c('content', { name: this.isP2P ? media.mid : media.mediaType });
66
+
67
+            modify.c('description', {
68
+                xmlns: XEP.RTP_MEDIA,
69
+                media: media.mediaType
70
+            });
68
 
71
 
69
-        if (!myMedia && othersMedia) {
70
-            // Add whole channel
71
-            newMedia[othersMediaIdx] = othersMedia;
72
+            Object.keys(media.ssrcs).forEach(ssrcNum => {
73
+                const mediaSsrc = media.ssrcs[ssrcNum];
74
+                const ssrcLines = mediaSsrc.lines;
75
+                const sourceName = SDPUtil.parseSourceNameLine(ssrcLines);
76
+                const videoType = SDPUtil.parseVideoTypeLine(ssrcLines);
77
+
78
+                modify.c('source', { xmlns: XEP.SOURCE_ATTRIBUTES });
79
+                modify.attrs({
80
+                    name: sourceName,
81
+                    videoType,
82
+                    ssrc: mediaSsrc.ssrc
83
+                });
72
 
84
 
73
-            return;
74
-        }
85
+                // Only MSID attribute is sent
86
+                const msid = SDPUtil.parseMSIDAttribute(ssrcLines);
75
 
87
 
76
-        // Look for new ssrcs across the channel
77
-        Object.keys(othersMedia.ssrcs).forEach(ssrc => {
78
-            if (Object.keys(myMedia.ssrcs).indexOf(ssrc) === -1) {
79
-                // Allocate channel if we've found ssrc that doesn't exist in
80
-                // our channel
81
-                if (!newMedia[othersMediaIdx]) {
82
-                    newMedia[othersMediaIdx] = {
83
-                        mediaindex: othersMedia.mediaindex,
84
-                        mid: othersMedia.mid,
85
-                        ssrcs: {},
86
-                        ssrcGroups: []
87
-                    };
88
+                if (msid) {
89
+                    modify.c('parameter');
90
+                    modify.attrs({ name: 'msid' });
91
+                    modify.attrs({ value: msid });
92
+                    modify.up();
88
                 }
93
                 }
89
-                newMedia[othersMediaIdx].ssrcs[ssrc] = othersMedia.ssrcs[ssrc];
90
-            } else if (othersMedia.ssrcs[ssrc].lines
91
-                        && myMedia.ssrcs[ssrc].lines) {
92
-                // we want to detect just changes in adding/removing msid
93
-                const myContainMsid = myMedia.ssrcs[ssrc].lines.find(
94
-                    line => line.indexOf('msid') !== -1) !== undefined;
95
-                const newContainMsid = othersMedia.ssrcs[ssrc].lines.find(
96
-                    line => line.indexOf('msid') !== -1) !== undefined;
97
-
98
-                if (myContainMsid !== newContainMsid) {
99
-                    if (!newMedia[othersMediaIdx]) {
100
-                        newMedia[othersMediaIdx] = {
101
-                            mediaindex: othersMedia.mediaindex,
102
-                            mid: othersMedia.mid,
103
-                            ssrcs: {},
104
-                            ssrcGroups: []
105
-                        };
106
-                    }
107
-                    newMedia[othersMediaIdx].ssrcs[ssrc]
108
-                        = othersMedia.ssrcs[ssrc];
109
-                }
110
-            }
111
-        });
112
-
113
-        // Look for new ssrc groups across the channels
114
-        othersMedia.ssrcGroups.forEach(otherSsrcGroup => {
115
 
94
 
116
-            // try to match the other ssrc-group with an ssrc-group of ours
117
-            let matched = false;
95
+                modify.up(); // end of source
96
+            });
118
 
97
 
119
-            for (let i = 0; i < myMedia.ssrcGroups.length; i++) {
120
-                const mySsrcGroup = myMedia.ssrcGroups[i];
98
+            // generate source groups from lines
99
+            media.ssrcGroups.forEach(ssrcGroup => {
100
+                if (ssrcGroup.ssrcs.length) {
121
 
101
 
122
-                if (otherSsrcGroup.semantics === mySsrcGroup.semantics
123
-                    && arrayEquals(otherSsrcGroup.ssrcs, mySsrcGroup.ssrcs)) {
102
+                    modify.c('ssrc-group', {
103
+                        semantics: ssrcGroup.semantics,
104
+                        xmlns: XEP.SOURCE_ATTRIBUTES
105
+                    });
124
 
106
 
125
-                    matched = true;
126
-                    break;
107
+                    ssrcGroup.ssrcs.forEach(ssrc => {
108
+                        modify.c('source', { ssrc })
109
+                            .up(); // end of source
110
+                    });
111
+                    modify.up(); // end of ssrc-group
127
                 }
112
                 }
128
-            }
129
-
130
-            if (!matched) {
131
-                // Allocate channel if we've found an ssrc-group that doesn't
132
-                // exist in our channel
133
-
134
-                if (!newMedia[othersMediaIdx]) {
135
-                    newMedia[othersMediaIdx] = {
136
-                        mediaindex: othersMedia.mediaindex,
137
-                        mid: othersMedia.mid,
138
-                        ssrcs: {},
139
-                        ssrcGroups: []
140
-                    };
141
-                }
142
-                newMedia[othersMediaIdx].ssrcGroups.push(otherSsrcGroup);
143
-            }
144
-        });
145
-    });
146
-
147
-    return newMedia;
148
-};
149
-
150
-/**
151
- * TODO: document!
152
- */
153
-SDPDiffer.prototype.toJingle = function(modify) {
154
-    const sdpMediaSsrcs = this.getNewMedia();
155
-
156
-    let modified = false;
157
-
158
-    Object.keys(sdpMediaSsrcs).forEach(mediaindex => {
159
-        modified = true;
160
-        const media = sdpMediaSsrcs[mediaindex];
161
-
162
-        modify.c('content', { name: media.mid });
163
-
164
-        modify.c('description', {
165
-            xmlns: XEP.RTP_MEDIA,
166
-            media: media.mid
167
-        });
168
-
169
-        // FIXME: not completely sure this operates on blocks and / or handles
170
-        // different ssrcs correctly
171
-        // generate sources from lines
172
-        Object.keys(media.ssrcs).forEach(ssrcNum => {
173
-            const mediaSsrc = media.ssrcs[ssrcNum];
174
-            const ssrcLines = mediaSsrc.lines;
175
-            const sourceName = SDPUtil.parseSourceNameLine(ssrcLines);
176
-            const videoType = SDPUtil.parseVideoTypeLine(ssrcLines);
177
-
178
-            modify.c('source', { xmlns: XEP.SOURCE_ATTRIBUTES });
179
-            modify.attrs({
180
-                name: sourceName,
181
-                videoType,
182
-                ssrc: mediaSsrc.ssrc
183
             });
113
             });
184
 
114
 
185
-            // Only MSID attribute is sent
186
-            const msid = SDPUtil.parseMSIDAttribute(ssrcLines);
187
-
188
-            if (msid) {
189
-                modify.c('parameter');
190
-                modify.attrs({ name: 'msid' });
191
-                modify.attrs({ value: msid });
192
-                modify.up();
193
-            }
194
-
195
-            modify.up(); // end of source
196
-        });
197
-
198
-        // generate source groups from lines
199
-        media.ssrcGroups.forEach(ssrcGroup => {
200
-            if (ssrcGroup.ssrcs.length) {
201
-
202
-                modify.c('ssrc-group', {
203
-                    semantics: ssrcGroup.semantics,
204
-                    xmlns: XEP.SOURCE_ATTRIBUTES
205
-                });
206
-
207
-                ssrcGroup.ssrcs.forEach(ssrc => {
208
-                    modify.c('source', { ssrc })
209
-                        .up(); // end of source
210
-                });
211
-                modify.up(); // end of ssrc-group
212
-            }
213
-        });
214
-
215
-        modify.up(); // end of description
216
-        modify.up(); // end of content
217
-    });
115
+            modify.up(); // end of description
116
+            modify.up(); // end of content
117
+        }
218
 
118
 
219
-    return modified;
220
-};
119
+        return modified;
120
+    }
121
+}

+ 293
- 35
modules/sdp/SDPDiffer.spec.js Visa fil

3
 import FeatureFlags from '../flags/FeatureFlags';
3
 import FeatureFlags from '../flags/FeatureFlags';
4
 
4
 
5
 import SDP from './SDP';
5
 import SDP from './SDP';
6
-import SDPDiffer from './SDPDiffer';
6
+import { SDPDiffer } from './SDPDiffer';
7
+import SampleSdpStrings from './SampleSdpStrings';
8
+
9
+/* eslint-disable max-len*/
7
 
10
 
8
 describe('SDPDiffer', () => {
11
 describe('SDPDiffer', () => {
9
     beforeEach(() => {
12
     beforeEach(() => {
10
         FeatureFlags.init({ });
13
         FeatureFlags.init({ });
11
     });
14
     });
12
     describe('toJingle', () => {
15
     describe('toJingle', () => {
13
-        /* eslint-disable max-len*/
14
-        const testSdpOld = [
15
-            'v=0\r\n',
16
-            'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n',
17
-            's=-\r\n',
18
-            't=0 0\r\n',
19
-            'a=group:BUNDLE audio video\r\n',
20
-            'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
21
-            'a=mid:audio\r\n',
22
-            'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n',
23
-            'a=ssrc:2002 cname:juejgy8a01\r\n',
24
-            'a=ssrc:2002 name:a8f7g30-a0\r\n',
25
-            'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n',
26
-            'a=mid:video\r\n'
27
-        ].join('');
28
-        const testSdpNew = [
29
-            'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
30
-            'a=mid:audio\r\n',
31
-            'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n',
32
-            'a=mid:video\r\n',
33
-            'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
34
-            'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
35
-            'a=ssrc:4004 cname:juejgy8a01\r\n',
36
-            'a=ssrc:4005 cname:juejgy8a01\r\n',
37
-            'a=ssrc:4004 name:a8f7g30-v0\r\n',
38
-            'a=ssrc:4005 name:a8f7g30-v0\r\n',
39
-            'a=ssrc-group:FID 4004 4005\r\n'
40
-        ].join('');
41
-        /* eslint-enable max-len*/
42
-
43
         it('should include source names in added/removed sources', () => {
16
         it('should include source names in added/removed sources', () => {
44
             FeatureFlags.init({ });
17
             FeatureFlags.init({ });
45
 
18
 
19
+            const testSdpOld = [
20
+                'v=0\r\n',
21
+                'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n',
22
+                's=-\r\n',
23
+                't=0 0\r\n',
24
+                'a=group:BUNDLE audio video\r\n',
25
+                'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
26
+                'a=mid:audio\r\n',
27
+                'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n',
28
+                'a=ssrc:2002 cname:juejgy8a01\r\n',
29
+                'a=ssrc:2002 name:a8f7g30-a0\r\n',
30
+                'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n',
31
+                'a=mid:video\r\n'
32
+            ].join('');
33
+            const testSdpNew = [
34
+                'v=0\r\n',
35
+                'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n',
36
+                's=-\r\n',
37
+                't=0 0\r\n',
38
+                'a=group:BUNDLE audio video\r\n',
39
+                'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
40
+                'a=mid:audio\r\n',
41
+                'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n',
42
+                'a=mid:video\r\n',
43
+                'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
44
+                'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
45
+                'a=ssrc:4004 cname:juejgy8a01\r\n',
46
+                'a=ssrc:4005 cname:juejgy8a01\r\n',
47
+                'a=ssrc:4004 name:a8f7g30-v0\r\n',
48
+                'a=ssrc:4005 name:a8f7g30-v0\r\n',
49
+                'a=ssrc-group:FID 4004 4005\r\n'
50
+            ].join('');
46
             const newToOldDiff = new SDPDiffer(new SDP(testSdpNew), new SDP(testSdpOld));
51
             const newToOldDiff = new SDPDiffer(new SDP(testSdpNew), new SDP(testSdpOld));
47
             const sourceRemoveIq = $iq({})
52
             const sourceRemoveIq = $iq({})
48
                 .c('jingle', { action: 'source-remove' });
53
                 .c('jingle', { action: 'source-remove' });
49
 
54
 
50
             newToOldDiff.toJingle(sourceRemoveIq);
55
             newToOldDiff.toJingle(sourceRemoveIq);
51
 
56
 
52
-            const removedAudioSources = sourceRemoveIq.nodeTree
53
-                .querySelectorAll('description[media=\'audio\']>source');
57
+            const removedAudioSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'audio\']>source');
54
 
58
 
55
             expect(removedAudioSources[0].getAttribute('name')).toBe('a8f7g30-a0');
59
             expect(removedAudioSources[0].getAttribute('name')).toBe('a8f7g30-a0');
56
 
60
 
60
 
64
 
61
             oldToNewDiff.toJingle(sourceAddIq);
65
             oldToNewDiff.toJingle(sourceAddIq);
62
 
66
 
63
-            const addedVideoSources = sourceAddIq.nodeTree
64
-                .querySelectorAll('description[media=\'video\']>source');
67
+            const addedVideoSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>source');
68
+            const addedVideoSourceGroups = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>ssrc-group');
65
 
69
 
66
             expect(addedVideoSources.length).toBe(2);
70
             expect(addedVideoSources.length).toBe(2);
67
             expect(addedVideoSources[0].getAttribute('name')).toBe('a8f7g30-v0');
71
             expect(addedVideoSources[0].getAttribute('name')).toBe('a8f7g30-v0');
68
             expect(addedVideoSources[1].getAttribute('name')).toBe('a8f7g30-v0');
72
             expect(addedVideoSources[1].getAttribute('name')).toBe('a8f7g30-v0');
73
+            expect(addedVideoSourceGroups.length).toBe(1);
74
+        });
75
+
76
+        it('should send source-remove/source-add when ssrc changes', () => {
77
+            FeatureFlags.init({ });
78
+
79
+            const testSdpOld = [
80
+                'v=0\r\n',
81
+                'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n',
82
+                's=-\r\n',
83
+                't=0 0\r\n',
84
+                'a=group:BUNDLE audio video\r\n',
85
+                'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
86
+                'a=mid:audio\r\n',
87
+                'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n',
88
+                'a=ssrc:2002 cname:juejgy8a01\r\n',
89
+                'a=ssrc:2002 name:a8f7g30-a0\r\n',
90
+                'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n',
91
+                'a=mid:video\r\n',
92
+                'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
93
+                'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
94
+                'a=ssrc:4004 cname:juejgy8a01\r\n',
95
+                'a=ssrc:4005 cname:juejgy8a01\r\n',
96
+                'a=ssrc:4004 name:a8f7g30-v0\r\n',
97
+                'a=ssrc:4005 name:a8f7g30-v0\r\n',
98
+                'a=ssrc-group:FID 4004 4005\r\n'
99
+            ].join('');
100
+            const testSdpNew = [
101
+                'v=0\r\n',
102
+                'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n',
103
+                's=-\r\n',
104
+                't=0 0\r\n',
105
+                'a=group:BUNDLE audio video\r\n',
106
+                'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
107
+                'a=mid:audio\r\n',
108
+                'a=ssrc:2003 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n',
109
+                'a=ssrc:2003 cname:juejgy8a01\r\n',
110
+                'a=ssrc:2003 name:a8f7g30-a0\r\n',
111
+                'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n',
112
+                'a=mid:video\r\n',
113
+                'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
114
+                'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
115
+                'a=ssrc:4004 cname:juejgy8a01\r\n',
116
+                'a=ssrc:4005 cname:juejgy8a01\r\n',
117
+                'a=ssrc:4004 name:a8f7g30-v0\r\n',
118
+                'a=ssrc:4005 name:a8f7g30-v0\r\n',
119
+                'a=ssrc-group:FID 4004 4005\r\n'
120
+            ].join('');
121
+            const newToOldDiff = new SDPDiffer(new SDP(testSdpNew), new SDP(testSdpOld));
122
+            const sourceRemoveIq = $iq({})
123
+                .c('jingle', { action: 'source-remove' });
124
+
125
+            newToOldDiff.toJingle(sourceRemoveIq);
126
+
127
+            const removedAudioSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'audio\']>source');
128
+            const removedVideoSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'video\']>source');
129
+
130
+            expect(removedAudioSources.length).toBe(1);
131
+            expect(removedAudioSources[0].getAttribute('name')).toBe('a8f7g30-a0');
132
+            expect(removedAudioSources[0].getAttribute('ssrc')).toBe('2002');
133
+            expect(removedVideoSources.length).toBe(0);
134
+
135
+            const oldToNewDiff = new SDPDiffer(new SDP(testSdpOld), new SDP(testSdpNew));
136
+            const sourceAddIq = $iq({})
137
+                .c('jingle', { action: 'source-add' });
138
+
139
+            oldToNewDiff.toJingle(sourceAddIq);
140
+
141
+            const addedAudioSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'audio\']>source');
142
+            const addedVideoSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>source');
143
+
144
+            expect(addedAudioSources.length).toBe(1);
145
+            expect(addedAudioSources[0].getAttribute('name')).toBe('a8f7g30-a0');
146
+            expect(addedAudioSources[0].getAttribute('ssrc')).toBe('2003');
147
+            expect(addedVideoSources.length).toBe(0);
148
+        });
149
+
150
+        it('should not send source-remove/source-add when nothing changes', () => {
151
+            FeatureFlags.init({ });
152
+
153
+            const testSdpOld = [
154
+                'v=0\r\n',
155
+                'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n',
156
+                's=-\r\n',
157
+                't=0 0\r\n',
158
+                'a=group:BUNDLE audio video\r\n',
159
+                'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
160
+                'a=mid:audio\r\n',
161
+                'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n',
162
+                'a=ssrc:2002 cname:juejgy8a01\r\n',
163
+                'a=ssrc:2002 name:a8f7g30-a0\r\n',
164
+                'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n',
165
+                'a=mid:video\r\n',
166
+                'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
167
+                'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
168
+                'a=ssrc:4004 cname:juejgy8a01\r\n',
169
+                'a=ssrc:4005 cname:juejgy8a01\r\n',
170
+                'a=ssrc:4004 name:a8f7g30-v0\r\n',
171
+                'a=ssrc:4005 name:a8f7g30-v0\r\n',
172
+                'a=ssrc-group:FID 4004 4005\r\n'
173
+            ].join('');
174
+            const testSdpNew = [
175
+                'v=0\r\n',
176
+                'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n',
177
+                's=-\r\n',
178
+                't=0 0\r\n',
179
+                'a=group:BUNDLE audio video\r\n',
180
+                'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
181
+                'a=mid:audio\r\n',
182
+                'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n',
183
+                'a=ssrc:2002 cname:juejgy8a01\r\n',
184
+                'a=ssrc:2002 name:a8f7g30-a0\r\n',
185
+                'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n',
186
+                'a=mid:video\r\n',
187
+                'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
188
+                'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
189
+                'a=ssrc:4004 cname:juejgy8a01\r\n',
190
+                'a=ssrc:4005 cname:juejgy8a01\r\n',
191
+                'a=ssrc:4004 name:a8f7g30-v0\r\n',
192
+                'a=ssrc:4005 name:a8f7g30-v0\r\n',
193
+                'a=ssrc-group:FID 4004 4005\r\n'
194
+            ].join('');
195
+            const newToOldDiff = new SDPDiffer(new SDP(testSdpNew), new SDP(testSdpOld));
196
+            const sourceRemoveIq = $iq({})
197
+                .c('jingle', { action: 'source-remove' });
198
+
199
+            newToOldDiff.toJingle(sourceRemoveIq);
200
+
201
+            const removedAudioSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'audio\']>source');
202
+            const removedVideoSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'video\']>source');
203
+
204
+            expect(removedAudioSources.length).toBe(0);
205
+            expect(removedVideoSources.length).toBe(0);
206
+
207
+            const oldToNewDiff = new SDPDiffer(new SDP(testSdpOld), new SDP(testSdpNew));
208
+            const sourceAddIq = $iq({})
209
+                .c('jingle', { action: 'source-add' });
210
+
211
+            oldToNewDiff.toJingle(sourceAddIq);
212
+
213
+            const addedAudioSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'audio\']>source');
214
+            const addedVideoSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>source');
215
+
216
+            expect(addedAudioSources.length).toBe(0);
217
+            expect(addedVideoSources.length).toBe(0);
218
+        });
219
+
220
+        it('should send source-adds for 2 sources', () => {
221
+            FeatureFlags.init({ });
222
+
223
+            const testSdpOld = [
224
+                'v=0\r\n',
225
+                'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n',
226
+                's=-\r\n',
227
+                't=0 0\r\n',
228
+                'a=group:BUNDLE audio video\r\n',
229
+                'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
230
+                'a=mid:audio\r\n',
231
+                'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n',
232
+                'a=mid:video\r\n'
233
+            ].join('');
234
+            const testSdpNew = [
235
+                'v=0\r\n',
236
+                'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n',
237
+                's=-\r\n',
238
+                't=0 0\r\n',
239
+                'a=group:BUNDLE audio video\r\n',
240
+                'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n',
241
+                'a=mid:audio\r\n',
242
+                'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n',
243
+                'a=ssrc:2002 cname:juejgy8a01\r\n',
244
+                'a=ssrc:2002 name:a8f7g30-a0\r\n',
245
+                'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n',
246
+                'a=mid:video\r\n',
247
+                'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
248
+                'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n',
249
+                'a=ssrc:4004 cname:juejgy8a01\r\n',
250
+                'a=ssrc:4005 cname:juejgy8a01\r\n',
251
+                'a=ssrc:4004 name:a8f7g30-v0\r\n',
252
+                'a=ssrc:4005 name:a8f7g30-v0\r\n',
253
+                'a=ssrc-group:FID 4004 4005\r\n'
254
+            ].join('');
255
+            const newToOldDiff = new SDPDiffer(new SDP(testSdpNew), new SDP(testSdpOld));
256
+            const sourceRemoveIq = $iq({})
257
+                .c('jingle', { action: 'source-remove' });
258
+
259
+            newToOldDiff.toJingle(sourceRemoveIq);
260
+
261
+            const removedAudioSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'audio\']>source');
262
+            const removedVideoSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'video\']>source');
263
+
264
+            expect(removedAudioSources.length).toBe(0);
265
+            expect(removedVideoSources.length).toBe(0);
266
+
267
+            const oldToNewDiff = new SDPDiffer(new SDP(testSdpOld), new SDP(testSdpNew));
268
+            const sourceAddIq = $iq({})
269
+                .c('jingle', { action: 'source-add' });
270
+
271
+            oldToNewDiff.toJingle(sourceAddIq);
272
+
273
+            const addedAudioSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'audio\']>source');
274
+            const addedVideoSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>source');
275
+            const addedVideoSourceGroups = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>ssrc-group');
276
+
277
+            expect(addedAudioSources.length).toBe(1);
278
+            expect(addedVideoSources.length).toBe(2);
279
+            expect(addedVideoSourceGroups.length).toBe(1);
280
+        });
281
+    });
282
+
283
+    describe('getNewMedia', () => {
284
+        it(' should generate sources for source-remove when SSCRs are missing from the new SDP', () => {
285
+            const oldSdp = new SDP(SampleSdpStrings.simulcastSdpStr);
286
+            const newSdp = new SDP(SampleSdpStrings.recvOnlySdpStrChrome);
287
+
288
+            let sdpDiffer = new SDPDiffer(newSdp, oldSdp, false);
289
+            let diff = sdpDiffer.getNewMedia();
290
+
291
+            // There should be 2 sources for source-remove.
292
+            expect(Object.keys(diff).length).toBe(2);
293
+
294
+            sdpDiffer = new SDPDiffer(oldSdp, newSdp, false);
295
+            diff = sdpDiffer.getNewMedia();
296
+
297
+            // There should zero sources for source-add.
298
+            expect(Object.keys(diff).length).toBe(0);
299
+        });
300
+
301
+        it(' should not generate sources for source-remove or source-add if the SDP does not change', () => {
302
+            const oldSdp = new SDP(SampleSdpStrings.simulcastSdpStr);
303
+            const newSdp = new SDP(SampleSdpStrings.simulcastSdpStr);
304
+
305
+            const sdpDiffer = new SDPDiffer(newSdp, oldSdp, false);
306
+            const diff = sdpDiffer.getNewMedia();
307
+
308
+            // There should be zero sources in diff.
309
+            expect(Object.keys(diff).length).toBe(0);
310
+        });
311
+
312
+        it(' should generate sources for source-remove and source-add when SSRC changes', () => {
313
+            const oldSdp = new SDP(SampleSdpStrings.simulcastSdpStr);
314
+            const newSdp = new SDP(SampleSdpStrings.simulcastDifferentSsrcSdpStr);
315
+
316
+            let sdpDiffer = new SDPDiffer(newSdp, oldSdp, false);
317
+            let diff = sdpDiffer.getNewMedia();
318
+
319
+            // There should be 1 source for source-remove.
320
+            expect(Object.keys(diff).length).toBe(1);
321
+
322
+            sdpDiffer = new SDPDiffer(oldSdp, newSdp, false);
323
+            diff = sdpDiffer.getNewMedia();
324
+
325
+            // There should 1 source for source-add.
326
+            expect(Object.keys(diff).length).toBe(1);
69
         });
327
         });
70
     });
328
     });
71
 });
329
 });

+ 15
- 0
modules/sdp/SDPUtil.js Visa fil

283
         return data;
283
         return data;
284
     },
284
     },
285
 
285
 
286
+    /**
287
+     * Parses the 'a=ssrc-group' line.
288
+     *
289
+     * @param {string} line - The media line to parse.
290
+     * @returns {object}
291
+     */
292
+    parseSSRCGroupLine(line) {
293
+        const parts = line.substr(13).split(' ');
294
+
295
+        return {
296
+            semantics: parts.shift(),
297
+            ssrcs: parts
298
+        };
299
+    },
300
+
286
     /**
301
     /**
287
      * Gets the source name out of the name attribute "a=ssrc:254321 name:name1".
302
      * Gets the source name out of the name attribute "a=ssrc:254321 name:name1".
288
      *
303
      *

+ 114
- 1
modules/sdp/SampleSdpStrings.js Visa fil

36
 + 'a=ssrc:124723944 label:40abf2d3-a415-4c68-8c17-2a038e8bebcf\r\n'
36
 + 'a=ssrc:124723944 label:40abf2d3-a415-4c68-8c17-2a038e8bebcf\r\n'
37
 + 'a=rtcp-mux\r\n';
37
 + 'a=rtcp-mux\r\n';
38
 
38
 
39
+const baseAudioMLineSdpDifferentSSRC = ''
40
++ 'm=audio 54405 RTP/SAVPF 111 103 104 126\r\n'
41
++ 'c=IN IP4 172.29.32.39\r\n'
42
++ 'a=rtpmap:111 opus/48000/2\r\n'
43
++ 'a=rtpmap:103 ISAC/16000\r\n'
44
++ 'a=rtpmap:104 ISAC/32000\r\n'
45
++ 'a=rtpmap:126 telephone-event/8000\r\n'
46
++ 'a=fmtp:111 minptime=10;useinbandfec=1\r\n'
47
++ 'a=rtcp:9 IN IP4 0.0.0.0\r\n'
48
++ 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n'
49
++ 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n'
50
++ 'a=setup:passive\r\n'
51
++ 'a=mid:audio\r\n'
52
++ 'a=msid:- 40abf2d3-a415-4c68-8c17-2a038e8bebcf\r\n'
53
++ 'a=sendrecv\r\n'
54
++ 'a=ice-ufrag:adPg\r\n'
55
++ 'a=ice-pwd:Xsr05Mq8S7CR44DAnusZE26F\r\n'
56
++ 'a=fingerprint:sha-256 6A:39:DE:11:24:AD:2E:4E:63:D6:69:D3:85:05:53:C7:3C:38:A4:B7:91:74:C0:91:44:FC:94:63:7F:01:AB:A9\r\n'
57
++ 'a=candidate:1581043602 1 udp 2122260223 172.29.32.39 54405 typ host generation 0\r\n'
58
++ 'a=ssrc:1757014965 cname:peDGrDD6WsxUOki/\r\n'
59
++ 'a=ssrc:1757014965 msid:dcbb0236-cea5-402e-9e9a-595c65ffcc2a 40abf2d3-a415-4c68-8c17-2a038e8bebcf\r\n'
60
++ 'a=ssrc:1757014965 mslabel:dcbb0236-cea5-402e-9e9a-595c65ffcc2a\r\n'
61
++ 'a=ssrc:1757014965 label:40abf2d3-a415-4c68-8c17-2a038e8bebcf\r\n'
62
++ 'a=rtcp-mux\r\n';
63
+
39
 // A basic sdp application mline
64
 // A basic sdp application mline
40
 const baseDataMLineSdp = ''
65
 const baseDataMLineSdp = ''
41
 + 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n'
66
 + 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n'
324
 + 'a=ssrc:124723944 cname:peDGrDD6WsxUOki\r\n'
349
 + 'a=ssrc:124723944 cname:peDGrDD6WsxUOki\r\n'
325
 + 'a=rtcp-mux\r\n';
350
 + 'a=rtcp-mux\r\n';
326
 
351
 
352
+const recvOnlyAudioMlineChrome = ''
353
++ 'm=audio 54405 RTP/SAVPF 111 103 104 126\r\n'
354
++ 'c=IN IP4 172.29.32.39\r\n'
355
++ 'a=rtpmap:111 opus/48000/2\r\n'
356
++ 'a=rtpmap:103 ISAC/16000\r\n'
357
++ 'a=rtpmap:104 ISAC/32000\r\n'
358
++ 'a=rtpmap:126 telephone-event/8000\r\n'
359
++ 'a=fmtp:111 minptime=10;useinbandfec=1\r\n'
360
++ 'a=rtcp:9 IN IP4 0.0.0.0\r\n'
361
++ 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n'
362
++ 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n'
363
++ 'a=setup:passive\r\n'
364
++ 'a=mid:audio\r\n'
365
++ 'a=recvonly\r\n'
366
++ 'a=ice-ufrag:adPg\r\n'
367
++ 'a=ice-pwd:Xsr05Mq8S7CR44DAnusZE26F\r\n'
368
++ 'a=fingerprint:sha-256 6A:39:DE:11:24:AD:2E:4E:63:D6:69:D3:85:05:53:C7:3C:38:A4:B7:91:74:C0:91:44:FC:94:63:7F:01:AB:A9\r\n'
369
++ 'a=candidate:1581043602 1 udp 2122260223 172.29.32.39 54405 typ host generation 0\r\n'
370
++ 'a=rtcp-mux\r\n';
371
+
327
 const recvOnlyVideoMline = ''
372
 const recvOnlyVideoMline = ''
328
 + 'm=video 9 RTP/SAVPF 96 97 98 99 102 121 127 120\r\n'
373
 + 'm=video 9 RTP/SAVPF 96 97 98 99 102 121 127 120\r\n'
329
 + 'c=IN IP4 0.0.0.0\r\n'
374
 + 'c=IN IP4 0.0.0.0\r\n'
372
 + 'a=ssrc:1757014965 cname:peDGrDD6WsxUOki\r\n'
417
 + 'a=ssrc:1757014965 cname:peDGrDD6WsxUOki\r\n'
373
 + 'a=rtcp-mux\r\n';
418
 + 'a=rtcp-mux\r\n';
374
 
419
 
420
+const recvOnlyVideoMlineChrome = ''
421
++ 'm=video 9 RTP/SAVPF 96 97 98 99 102 121 127 120\r\n'
422
++ 'c=IN IP4 0.0.0.0\r\n'
423
++ 'a=rtpmap:96 VP8/90000\r\n'
424
++ 'a=rtpmap:97 rtx/90000\r\n'
425
++ 'a=rtpmap:98 VP9/90000\r\n'
426
++ 'a=rtpmap:99 rtx/90000\r\n'
427
++ 'a=rtpmap:102 H264/90000\r\n'
428
++ 'a=rtpmap:121 rtx/90000\r\n'
429
++ 'a=rtpmap:127 H264/90000\r\n'
430
++ 'a=rtpmap:120 rtx/90000\r\n'
431
++ 'a=rtcp:9 IN IP4 0.0.0.0\r\n'
432
++ 'a=rtcp-fb:96 ccm fir\r\n'
433
++ 'a=rtcp-fb:96 transport-cc\r\n'
434
++ 'a=rtcp-fb:96 nack\r\n'
435
++ 'a=rtcp-fb:96 nack pli\r\n'
436
++ 'a=rtcp-fb:96 goog-remb\r\n'
437
++ 'a=rtcp-fb:98 ccm fir\r\n'
438
++ 'a=rtcp-fb:98 transport-cc\r\n'
439
++ 'a=rtcp-fb:98 nack\r\n'
440
++ 'a=rtcp-fb:98 nack pli\r\n'
441
++ 'a=rtcp-fb:98 goog-remb\r\n'
442
++ 'a=rtcp-fb:102 ccm fir\r\n'
443
++ 'a=rtcp-fb:102 transport-cc\r\n'
444
++ 'a=rtcp-fb:102 nack\r\n'
445
++ 'a=rtcp-fb:102 nack pli\r\n'
446
++ 'a=rtcp-fb:102 goog-remb\r\n'
447
++ 'a=rtcp-fb:127 ccm fir\r\n'
448
++ 'a=rtcp-fb:127 transport-cc\r\n'
449
++ 'a=rtcp-fb:127 nack\r\n'
450
++ 'a=rtcp-fb:127 nack pli\r\n'
451
++ 'a=rtcp-fb:127 goog-remb\r\n'
452
++ 'a=fmtp:97 apt=96\r\n'
453
++ 'a=fmtp:98 profile-id=0\r\n'
454
++ 'a=fmtp:102 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1\r\n'
455
++ 'a=fmtp:121 apt=102\r\n'
456
++ 'a=fmtp:127 profile-level-id=42e01f;level-asymmetry-allowed=1:packetization-mode=0\r\n'
457
++ 'a=fmtp:120 apt=127\r\n'
458
++ 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n'
459
++ 'a=setup:passive\r\n'
460
++ 'a=mid:video\r\n'
461
++ 'a=recvonly\r\n'
462
++ 'a=ice-ufrag:adPg\r\n'
463
++ 'a=ice-pwd:Xsr05Mq8S7CR44DAnusZE26F\r\n'
464
++ 'a=fingerprint:sha-256 6A:39:DE:11:24:AD:2E:4E:63:D6:69:D3:85:05:53:C7:3C:38:A4:B7:91:74:C0:91:44:FC:94:63:7F:01:AB:A9\r\n'
465
++ 'a=rtcp-mux\r\n';
466
+
375
 const videoMlineFF = ''
467
 const videoMlineFF = ''
376
 + 'm=video 9 RTP/SAVPF 100 96\r\n'
468
 + 'm=video 9 RTP/SAVPF 100 96\r\n'
377
 + 'c=IN IP4 0.0.0.0\r\n'
469
 + 'c=IN IP4 0.0.0.0\r\n'
443
 // A full sdp string representing a client doing simulcast
535
 // A full sdp string representing a client doing simulcast
444
 const simulcastSdpStr = baseSessionSdp + baseAudioMLineSdp + simulcastVideoMLineSdp + baseDataMLineSdp;
536
 const simulcastSdpStr = baseSessionSdp + baseAudioMLineSdp + simulcastVideoMLineSdp + baseDataMLineSdp;
445
 
537
 
538
+const simulcastDifferentSsrcSdpStr = baseSessionSdp + baseAudioMLineSdpDifferentSSRC + simulcastVideoMLineSdp + baseDataMLineSdp;
539
+
446
 // A full sdp string representing a remote client doing simucast when RTX is not negotiated with the jvb.
540
 // A full sdp string representing a remote client doing simucast when RTX is not negotiated with the jvb.
447
 const simulcastNoRtxSdpStr = baseSessionSdp + baseAudioMLineSdp + simulcastVideoMLineNoRtxSdp;
541
 const simulcastNoRtxSdpStr = baseSessionSdp + baseAudioMLineSdp + simulcastVideoMLineNoRtxSdp;
448
 
542
 
465
 const flexFecSdpStr = baseSessionSdp + baseAudioMLineSdp + flexFecVideoMLineSdp + baseDataMLineSdp;
559
 const flexFecSdpStr = baseSessionSdp + baseAudioMLineSdp + flexFecVideoMLineSdp + baseDataMLineSdp;
466
 
560
 
467
 // A full sdp string representing a client that doesn't have local sources added on Firefox.
561
 // A full sdp string representing a client that doesn't have local sources added on Firefox.
468
-const recvOnlySdpStr = baseSessionSdp + recvOnlyAudioMline + recvOnlyVideoMline;
562
+const recvOnlySdpStr = baseSessionSdp + recvOnlyAudioMline + recvOnlyVideoMline + baseDataMLineSdp;
563
+
564
+// A full sdp string representing a client that doesn't have local sources added on Chrome.
565
+const recvOnlySdpStrChrome = baseSessionSdp + recvOnlyAudioMlineChrome + recvOnlyVideoMlineChrome + baseDataMLineSdp;
469
 
566
 
470
 // A full sdp string representing a Firefox client with msid set to '-'.
567
 // A full sdp string representing a Firefox client with msid set to '-'.
471
 const sdpFirefoxStr = baseSessionSdp + baseAudioMLineSdp + videoMlineFF;
568
 const sdpFirefoxStr = baseSessionSdp + baseAudioMLineSdp + videoMlineFF;
474
 const sdpFirefoxP2pStr = baseSessionSdp + baseAudioMLineSdp + videoLineP2pFF;
571
 const sdpFirefoxP2pStr = baseSessionSdp + baseAudioMLineSdp + videoLineP2pFF;
475
 
572
 
476
 export default {
573
 export default {
574
+    get simulcastSdpStr() {
575
+        return simulcastSdpStr;
576
+    },
577
+
578
+    get simulcastDifferentSsrcSdpStr() {
579
+        return simulcastDifferentSsrcSdpStr;
580
+    },
581
+
477
     get simulcastSdp() {
582
     get simulcastSdp() {
478
         return transform.parse(simulcastSdpStr);
583
         return transform.parse(simulcastSdpStr);
479
     },
584
     },
506
         return transform.parse(flexFecSdpStr);
611
         return transform.parse(flexFecSdpStr);
507
     },
612
     },
508
 
613
 
614
+    get recvOnlySdpStr() {
615
+        return recvOnlySdpStr;
616
+    },
617
+
618
+    get recvOnlySdpStrChrome() {
619
+        return recvOnlySdpStrChrome;
620
+    },
621
+
509
     get recvOnlySdp() {
622
     get recvOnlySdp() {
510
         return transform.parse(recvOnlySdpStr);
623
         return transform.parse(recvOnlySdpStr);
511
     },
624
     },

+ 6
- 6
modules/xmpp/JingleSessionPC.js Visa fil

17
 import { SS_DEFAULT_FRAME_RATE } from '../RTC/ScreenObtainer';
17
 import { SS_DEFAULT_FRAME_RATE } from '../RTC/ScreenObtainer';
18
 import FeatureFlags from '../flags/FeatureFlags';
18
 import FeatureFlags from '../flags/FeatureFlags';
19
 import SDP from '../sdp/SDP';
19
 import SDP from '../sdp/SDP';
20
-import SDPDiffer from '../sdp/SDPDiffer';
20
+import { SDPDiffer } from '../sdp/SDPDiffer';
21
 import SDPUtil from '../sdp/SDPUtil';
21
 import SDPUtil from '../sdp/SDPUtil';
22
 import Statistics from '../statistics/statistics';
22
 import Statistics from '../statistics/statistics';
23
 import AsyncQueue, { ClearedQueueError } from '../util/AsyncQueue';
23
 import AsyncQueue, { ClearedQueueError } from '../util/AsyncQueue';
1009
             sid: this.sid
1009
             sid: this.sid
1010
         });
1010
         });
1011
 
1011
 
1012
-        new SDP(offerSdp).toJingle(
1012
+        new SDP(offerSdp, this.isP2P).toJingle(
1013
             init,
1013
             init,
1014
             this.isInitiator ? 'initiator' : 'responder');
1014
             this.isInitiator ? 'initiator' : 'responder');
1015
         init = init.tree();
1015
         init = init.tree();
1198
     sendSessionAccept(success, failure) {
1198
     sendSessionAccept(success, failure) {
1199
         // NOTE: since we're just reading from it, we don't need to be within
1199
         // NOTE: since we're just reading from it, we don't need to be within
1200
         //  the modification queue to access the local description
1200
         //  the modification queue to access the local description
1201
-        const localSDP = new SDP(this.peerconnection.localDescription.sdp);
1201
+        const localSDP = new SDP(this.peerconnection.localDescription.sdp, this.isP2P);
1202
         const accept = $iq({ to: this.remoteJid,
1202
         const accept = $iq({ to: this.remoteJid,
1203
             type: 'set' })
1203
             type: 'set' })
1204
             .c('jingle', { xmlns: 'urn:xmpp:jingle:1',
1204
             .c('jingle', { xmlns: 'urn:xmpp:jingle:1',
2334
             Object.keys(newMedia).forEach(mediaIndex => {
2334
             Object.keys(newMedia).forEach(mediaIndex => {
2335
                 const signaledSsrcs = Object.keys(newMedia[mediaIndex].ssrcs);
2335
                 const signaledSsrcs = Object.keys(newMedia[mediaIndex].ssrcs);
2336
 
2336
 
2337
-                mediaType = newMedia[mediaIndex].mid;
2337
+                mediaType = newMedia[mediaIndex].mediaType;
2338
                 if (signaledSsrcs?.length) {
2338
                 if (signaledSsrcs?.length) {
2339
                     ssrcs = ssrcs.concat(signaledSsrcs);
2339
                     ssrcs = ssrcs.concat(signaledSsrcs);
2340
                 }
2340
                 }
2347
         };
2347
         };
2348
 
2348
 
2349
         // send source-remove IQ.
2349
         // send source-remove IQ.
2350
-        let sdpDiffer = new SDPDiffer(newSDP, oldSDP);
2350
+        let sdpDiffer = new SDPDiffer(newSDP, oldSDP, this.isP2P);
2351
         const remove = $iq({ to: this.remoteJid,
2351
         const remove = $iq({ to: this.remoteJid,
2352
             type: 'set' })
2352
             type: 'set' })
2353
             .c('jingle', {
2353
             .c('jingle', {
2381
         }
2381
         }
2382
 
2382
 
2383
         // send source-add IQ.
2383
         // send source-add IQ.
2384
-        sdpDiffer = new SDPDiffer(oldSDP, newSDP);
2384
+        sdpDiffer = new SDPDiffer(oldSDP, newSDP, this.isP2P);
2385
         const add = $iq({ to: this.remoteJid,
2385
         const add = $iq({ to: this.remoteJid,
2386
             type: 'set' })
2386
             type: 'set' })
2387
             .c('jingle', {
2387
             .c('jingle', {

+ 0
- 4
service/RTC/RTCEvents.spec.ts Visa fil

14
         PERMISSIONS_CHANGED,
14
         PERMISSIONS_CHANGED,
15
         SENDER_VIDEO_CONSTRAINTS_CHANGED,
15
         SENDER_VIDEO_CONSTRAINTS_CHANGED,
16
         LASTN_VALUE_CHANGED,
16
         LASTN_VALUE_CHANGED,
17
-        LOCAL_TRACK_SSRC_UPDATED,
18
         LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED,
17
         LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED,
19
         TRACK_ATTACHED,
18
         TRACK_ATTACHED,
20
         REMOTE_TRACK_ADDED,
19
         REMOTE_TRACK_ADDED,
49
         expect( PERMISSIONS_CHANGED ).toBe( 'rtc.permissions_changed' );
48
         expect( PERMISSIONS_CHANGED ).toBe( 'rtc.permissions_changed' );
50
         expect( SENDER_VIDEO_CONSTRAINTS_CHANGED ).toBe( 'rtc.sender_video_constraints_changed' );
49
         expect( SENDER_VIDEO_CONSTRAINTS_CHANGED ).toBe( 'rtc.sender_video_constraints_changed' );
51
         expect( LASTN_VALUE_CHANGED ).toBe( 'rtc.lastn_value_changed' );
50
         expect( LASTN_VALUE_CHANGED ).toBe( 'rtc.lastn_value_changed' );
52
-        expect( LOCAL_TRACK_SSRC_UPDATED ).toBe( 'rtc.local_track_ssrc_updated' );
53
         expect( LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED ).toBe( 'rtc.local_track_max_enabled_resolution_changed' );
51
         expect( LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED ).toBe( 'rtc.local_track_max_enabled_resolution_changed' );
54
         expect( TRACK_ATTACHED ).toBe( 'rtc.track_attached' );
52
         expect( TRACK_ATTACHED ).toBe( 'rtc.track_attached' );
55
         expect( REMOTE_TRACK_ADDED ).toBe( 'rtc.remote_track_added' );
53
         expect( REMOTE_TRACK_ADDED ).toBe( 'rtc.remote_track_added' );
79
             expect( RTCEvents.PERMISSIONS_CHANGED ).toBe( 'rtc.permissions_changed' );
77
             expect( RTCEvents.PERMISSIONS_CHANGED ).toBe( 'rtc.permissions_changed' );
80
             expect( RTCEvents.SENDER_VIDEO_CONSTRAINTS_CHANGED ).toBe( 'rtc.sender_video_constraints_changed' );
78
             expect( RTCEvents.SENDER_VIDEO_CONSTRAINTS_CHANGED ).toBe( 'rtc.sender_video_constraints_changed' );
81
             expect( RTCEvents.LASTN_VALUE_CHANGED ).toBe( 'rtc.lastn_value_changed' );
79
             expect( RTCEvents.LASTN_VALUE_CHANGED ).toBe( 'rtc.lastn_value_changed' );
82
-            expect( RTCEvents.LOCAL_TRACK_SSRC_UPDATED ).toBe( 'rtc.local_track_ssrc_updated' );
83
             expect( RTCEvents.LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED ).toBe( 'rtc.local_track_max_enabled_resolution_changed' );
80
             expect( RTCEvents.LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED ).toBe( 'rtc.local_track_max_enabled_resolution_changed' );
84
             expect( RTCEvents.TRACK_ATTACHED ).toBe( 'rtc.track_attached' );
81
             expect( RTCEvents.TRACK_ATTACHED ).toBe( 'rtc.track_attached' );
85
             expect( RTCEvents.REMOTE_TRACK_ADDED ).toBe( 'rtc.remote_track_added' );
82
             expect( RTCEvents.REMOTE_TRACK_ADDED ).toBe( 'rtc.remote_track_added' );
110
             expect( RTCEventsDefault.PERMISSIONS_CHANGED ).toBe( 'rtc.permissions_changed' );
107
             expect( RTCEventsDefault.PERMISSIONS_CHANGED ).toBe( 'rtc.permissions_changed' );
111
             expect( RTCEventsDefault.SENDER_VIDEO_CONSTRAINTS_CHANGED ).toBe( 'rtc.sender_video_constraints_changed' );
108
             expect( RTCEventsDefault.SENDER_VIDEO_CONSTRAINTS_CHANGED ).toBe( 'rtc.sender_video_constraints_changed' );
112
             expect( RTCEventsDefault.LASTN_VALUE_CHANGED ).toBe( 'rtc.lastn_value_changed' );
109
             expect( RTCEventsDefault.LASTN_VALUE_CHANGED ).toBe( 'rtc.lastn_value_changed' );
113
-            expect( RTCEventsDefault.LOCAL_TRACK_SSRC_UPDATED ).toBe( 'rtc.local_track_ssrc_updated' );
114
             expect( RTCEventsDefault.LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED ).toBe( 'rtc.local_track_max_enabled_resolution_changed' );
110
             expect( RTCEventsDefault.LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED ).toBe( 'rtc.local_track_max_enabled_resolution_changed' );
115
             expect( RTCEventsDefault.TRACK_ATTACHED ).toBe( 'rtc.track_attached' );
111
             expect( RTCEventsDefault.TRACK_ATTACHED ).toBe( 'rtc.track_attached' );
116
             expect( RTCEventsDefault.REMOTE_TRACK_ADDED ).toBe( 'rtc.remote_track_added' );
112
             expect( RTCEventsDefault.REMOTE_TRACK_ADDED ).toBe( 'rtc.remote_track_added' );

+ 0
- 9
service/RTC/RTCEvents.ts Visa fil

30
      */
30
      */
31
     LASTN_VALUE_CHANGED = 'rtc.lastn_value_changed',
31
     LASTN_VALUE_CHANGED = 'rtc.lastn_value_changed',
32
 
32
 
33
-    /**
34
-     * Event emitted when ssrc for a local track is extracted and stored
35
-     * in {@link TraceablePeerConnection}.
36
-     * @param {JitsiLocalTrack} track which ssrc was updated
37
-     * @param {string} ssrc that was stored
38
-     */
39
-    LOCAL_TRACK_SSRC_UPDATED = 'rtc.local_track_ssrc_updated',
40
-
41
     /**
33
     /**
42
      * The max enabled resolution of a local video track was changed.
34
      * The max enabled resolution of a local video track was changed.
43
      */
35
      */
135
 export const PERMISSIONS_CHANGED = RTCEvents.PERMISSIONS_CHANGED;
127
 export const PERMISSIONS_CHANGED = RTCEvents.PERMISSIONS_CHANGED;
136
 export const SENDER_VIDEO_CONSTRAINTS_CHANGED = RTCEvents.SENDER_VIDEO_CONSTRAINTS_CHANGED;
128
 export const SENDER_VIDEO_CONSTRAINTS_CHANGED = RTCEvents.SENDER_VIDEO_CONSTRAINTS_CHANGED;
137
 export const LASTN_VALUE_CHANGED = RTCEvents.LASTN_VALUE_CHANGED;
129
 export const LASTN_VALUE_CHANGED = RTCEvents.LASTN_VALUE_CHANGED;
138
-export const LOCAL_TRACK_SSRC_UPDATED = RTCEvents.LOCAL_TRACK_SSRC_UPDATED;
139
 export const LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED = RTCEvents.LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED;
130
 export const LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED = RTCEvents.LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED;
140
 export const TRACK_ATTACHED = RTCEvents.TRACK_ATTACHED;
131
 export const TRACK_ATTACHED = RTCEvents.TRACK_ATTACHED;
141
 export const REMOTE_TRACK_ADDED = RTCEvents.REMOTE_TRACK_ADDED;
132
 export const REMOTE_TRACK_ADDED = RTCEvents.REMOTE_TRACK_ADDED;

+ 0
- 1
types/hand-crafted/service/RTC/RTCEvents.d.ts Visa fil

8
   PERMISSIONS_CHANGED = 'rtc.permissions_changed',
8
   PERMISSIONS_CHANGED = 'rtc.permissions_changed',
9
   SENDER_VIDEO_CONSTRAINTS_CHANGED = 'rtc.sender_video_constraints_changed',
9
   SENDER_VIDEO_CONSTRAINTS_CHANGED = 'rtc.sender_video_constraints_changed',
10
   LASTN_VALUE_CHANGED = 'rtc.lastn_value_changed',
10
   LASTN_VALUE_CHANGED = 'rtc.lastn_value_changed',
11
-  LOCAL_TRACK_SSRC_UPDATED = 'rtc.local_track_ssrc_updated',
12
   LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED = 'rtc.local_track_max_enabled_resolution_changed',
11
   LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED = 'rtc.local_track_max_enabled_resolution_changed',
13
   TRACK_ATTACHED = 'rtc.track_attached',
12
   TRACK_ATTACHED = 'rtc.track_attached',
14
   REMOTE_TRACK_ADDED = 'rtc.remote_track_added',
13
   REMOTE_TRACK_ADDED = 'rtc.remote_track_added',

Laddar…
Avbryt
Spara