From d51e557518f4aa1fca48365eea182bdc7650c412 Mon Sep 17 00:00:00 2001
From: Jaya Allamsetty <54324652+jallamsetty1@users.noreply.github.com>
Date: Tue, 29 Oct 2024 11:04:44 -0400
Subject: [PATCH] feat(JingleSession) Convert Jingle->SDP directly w/o interop
 layer. (#2590)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

* Update modules/RTC/TraceablePeerConnection.js

Co-authored-by: Saúl Ibarra Corretgé <s@saghul.net>

* ref(SDP) Introduce an enum for ssrc-group semantics.
Address review comments, fix test faiilures for ssrc-rewriting case and add unit tests.

* Update modules/RTC/TraceablePeerConnection.js

Co-authored-by: Saúl Ibarra Corretgé <s@saghul.net>

* squash: Address review comments

---------

Co-authored-by: Saúl Ibarra Corretgé <s@saghul.net>
---
 modules/RTC/MockClasses.js                    |  13 +
 modules/RTC/TraceablePeerConnection.js        | 116 ++---
 modules/sdp/RtxModifier.js                    |   7 +-
 modules/sdp/SDP.js                            | 219 +++++++--
 modules/sdp/SDP.spec.js                       | 314 +++++++++----
 modules/sdp/SDPUtil.js                        |   5 +-
 modules/sdp/SdpSimulcast.ts                   |  10 +-
 modules/sdp/SdpTransformUtil.js               |  10 +-
 modules/xmpp/JingleHelperFunctions.js         |   5 +-
 modules/xmpp/JingleSessionPC.js               | 431 ++++++------------
 modules/xmpp/JingleSessionPC.spec.js          | 172 ++++++-
 package-lock.json                             |  45 --
 package.json                                  |   1 -
 service/RTC/StandardVideoQualitySettings.ts   |  11 +
 .../modules/RTC/TraceablePeerConnection.d.ts  |   2 +-
 types/hand-crafted/modules/sdp/SDP.d.ts       |   1 -
 .../modules/xmpp/JingleSessionPC.d.ts         |   1 -
 17 files changed, 813 insertions(+), 550 deletions(-)

diff --git a/modules/RTC/MockClasses.js b/modules/RTC/MockClasses.js
index f1aadd5ebf..cc4cfee13f 100644
--- a/modules/RTC/MockClasses.js
+++ b/modules/RTC/MockClasses.js
@@ -183,6 +183,13 @@ export class MockPeerConnection {
         return Array.from(codecs);
     }
 
+    /**
+     * {@link TraceablePeerConnection.getDesiredMediaDirection}.
+     */
+    getDesiredMediaDirection() {
+        return 'sendrecv';
+    }
+
     /**
      * {@link TraceablePeerConnection.isSpatialScalabilityOn}.
      *
@@ -231,6 +238,12 @@ export class MockPeerConnection {
         return false;
     }
 
+    /**
+     * {@link TraceablePeerConnection.updateRemoteSources}.
+     */
+    updateRemoteSources() {
+    }
+
     /**
      * {@link TraceablePeerConnection.usesUnifiedPlan}.
      */
diff --git a/modules/RTC/TraceablePeerConnection.js b/modules/RTC/TraceablePeerConnection.js
index a5d861d91f..5510d23f1d 100644
--- a/modules/RTC/TraceablePeerConnection.js
+++ b/modules/RTC/TraceablePeerConnection.js
@@ -1,5 +1,5 @@
 import { getLogger } from '@jitsi/logger';
-import { Interop } from '@jitsi/sdp-interop';
+import { cloneDeep } from 'lodash-es';
 import transform from 'sdp-transform';
 
 import { CodecMimeType } from '../../service/RTC/CodecMimeType';
@@ -8,7 +8,7 @@ import { MediaType } from '../../service/RTC/MediaType';
 import RTCEvents from '../../service/RTC/RTCEvents';
 import * as SignalingEvents from '../../service/RTC/SignalingEvents';
 import { getSourceIndexFromSourceName } from '../../service/RTC/SignalingLayer';
-import { VIDEO_QUALITY_LEVELS } from '../../service/RTC/StandardVideoQualitySettings';
+import { SSRC_GROUP_SEMANTICS, VIDEO_QUALITY_LEVELS } from '../../service/RTC/StandardVideoQualitySettings';
 import { VideoType } from '../../service/RTC/VideoType';
 import { VIDEO_CODEC_CHANGED } from '../../service/statistics/AnalyticsEvents';
 import { SS_DEFAULT_FRAME_RATE } from '../RTC/ScreenObtainer';
@@ -289,8 +289,6 @@ export default function TraceablePeerConnection(
      */
     this.maxstats = options.maxstats;
 
-    this.interop = new Interop();
-
     this.simulcast = new SdpSimulcast();
 
     /**
@@ -328,12 +326,26 @@ export default function TraceablePeerConnection(
     this._localTrackTransceiverMids = new Map();
 
     /**
-     * Holds the SSRC map for the local tracks.
+     * Holds the SSRC map for the local tracks mapped by their source names.
      *
-     * @type {Map<string, TPCSSRCInfo>}
+     * @type {Map<string, TPCSourceInfo>}
+     * @property {string} msid - The track's MSID.
+     * @property {Array<string>} ssrcs - The SSRCs associated with the track.
+     * @property {Array<TPCGroupInfo>} groups - The SSRC groups associated with the track.
      */
     this._localSsrcMap = null;
 
+    /**
+     * Holds the SSRC map for the remote tracks mapped by their source names.
+     *
+     * @type {Map<string, TPCSourceInfo>}
+     * @property {string} mediaType - The media type of the track.
+     * @property {string} msid - The track's MSID.
+     * @property {Array<TPCGroupInfo>} groups - The SSRC groups associated with the track.
+     * @property {Array<string>} ssrcs - The SSRCs associated with the track.
+     */
+    this._remoteSsrcMap = new Map();
+
     // override as desired
     this.trace = (what, info) => {
         logger.trace(what, info);
@@ -680,7 +692,7 @@ TraceablePeerConnection.prototype.getLocalVideoSSRCs = function(localTrack) {
         return ssrcs;
     }
 
-    const ssrcGroup = this.isSpatialScalabilityOn() ? 'SIM' : 'FID';
+    const ssrcGroup = this.isSpatialScalabilityOn() ? SSRC_GROUP_SEMANTICS.SIM : SSRC_GROUP_SEMANTICS.FID;
 
     return this.localSSRCs.get(localTrack.rtcId)?.groups?.find(group => group.semantics === ssrcGroup)?.ssrcs || ssrcs;
 };
@@ -769,45 +781,35 @@ TraceablePeerConnection.prototype.getRemoteTracks = function(endpointId, mediaTy
 };
 
 /**
- * Parses the remote description and returns the sdp lines of the sources associated with a remote participant.
+ * Returns the remote sourceInfo for a given source name.
+ *
+ * @param {string} sourceName - The source name.
+ * @returns {TPCSourceInfo}
+ */
+TraceablePeerConnection.prototype.getRemoteSourceInfoBySourceName = function(sourceName) {
+    return cloneDeep(this._remoteSsrcMap.get(sourceName));
+};
+
+/**
+ * Returns a map of source names and their associated SSRCs for the remote participant.
  *
  * @param {string} id Endpoint id of the remote participant.
- * @returns {Array<string>} The sdp lines that have the ssrc information.
+ * @returns {Map<string, TPCSourceInfo>} The map of source names and their associated SSRCs.
  */
 TraceablePeerConnection.prototype.getRemoteSourceInfoByParticipant = function(id) {
-    const removeSsrcInfo = [];
+    const removeSsrcInfo = new Map();
     const remoteTracks = this.getRemoteTracks(id);
 
     if (!remoteTracks?.length) {
         return removeSsrcInfo;
     }
     const primarySsrcs = remoteTracks.map(track => track.getSSRC());
-    const sdp = new SDP(this.remoteDescription.sdp);
-
-    primarySsrcs.forEach((ssrc, idx) => {
-        for (const media of sdp.media) {
-            let lines = '';
-            let ssrcLines = SDPUtil.findLines(media, `a=ssrc:${ssrc}`);
-
-            if (ssrcLines.length) {
-                if (!removeSsrcInfo[idx]) {
-                    removeSsrcInfo[idx] = '';
-                }
 
-                // Check if there are any FID groups present for the primary ssrc.
-                const fidLines = SDPUtil.findLines(media, `a=ssrc-group:FID ${ssrc}`);
-
-                if (fidLines.length) {
-                    const secondarySsrc = fidLines[0].split(' ')[2];
-
-                    lines += `${fidLines[0]}\r\n`;
-                    ssrcLines = ssrcLines.concat(SDPUtil.findLines(media, `a=ssrc:${secondarySsrc}`));
-                }
-                removeSsrcInfo[idx] += `${ssrcLines.join('\r\n')}\r\n`;
-                removeSsrcInfo[idx] += lines;
-            }
+    for (const [ sourceName, sourceInfo ] of this._remoteSsrcMap) {
+        if (sourceInfo.ssrcList?.some(ssrc => primarySsrcs.includes(Number(ssrc)))) {
+            removeSsrcInfo.set(sourceName, sourceInfo);
         }
-    });
+    }
 
     return removeSsrcInfo;
 };
@@ -907,7 +909,7 @@ TraceablePeerConnection.prototype._remoteTrackAdded = function(stream, track, tr
         return;
     }
 
-    const remoteSDP = new SDP(this.peerconnection.remoteDescription.sdp);
+    const remoteSDP = new SDP(this.remoteDescription.sdp);
     let mediaLine;
 
     // Find the matching mline using 'mid' or the 'msid' attr of the stream.
@@ -1136,6 +1138,9 @@ TraceablePeerConnection.prototype._removeRemoteTrack = function(toBeRemoved) {
  * @returns {void}
  */
 TraceablePeerConnection.prototype._processAndExtractSourceInfo = function(localSDP) {
+    /**
+     * @type {Map<string, TPCSourceInfo>} The map of source names and their associated SSRCs.
+     */
     const ssrcMap = new Map();
 
     if (!localSDP || typeof localSDP !== 'string') {
@@ -1145,7 +1150,7 @@ TraceablePeerConnection.prototype._processAndExtractSourceInfo = function(localS
     const media = session.media.filter(mline => mline.direction === MediaDirection.SENDONLY
         || mline.direction === MediaDirection.SENDRECV);
 
-    if (!Array.isArray(media)) {
+    if (!media.length) {
         this._localSsrcMap = ssrcMap;
 
         return;
@@ -1178,14 +1183,14 @@ TraceablePeerConnection.prototype._processAndExtractSourceInfo = function(localS
                     ssrcInfo.groups.push(group);
                 }
 
-                const simGroup = ssrcGroups.find(group => group.semantics === 'SIM');
+                const simGroup = ssrcGroups.find(group => group.semantics === SSRC_GROUP_SEMANTICS.SIM);
 
                 // Add a SIM group if its missing in the description (happens on Firefox).
                 if (this.isSpatialScalabilityOn() && !simGroup) {
                     const groupSsrcs = ssrcGroups.map(group => group.ssrcs[0]);
 
                     ssrcInfo.groups.push({
-                        semantics: 'SIM',
+                        semantics: SSRC_GROUP_SEMANTICS.SIM,
                         ssrcs: groupSsrcs
                     });
                 }
@@ -1231,7 +1236,7 @@ TraceablePeerConnection.prototype._injectSsrcGroupForUnifiedSimulcast = function
 
     // Check if the browser supports RTX, add only the primary ssrcs to the SIM group if that is the case.
     video.ssrcGroups = video.ssrcGroups || [];
-    const fidGroups = video.ssrcGroups.filter(group => group.semantics === 'FID');
+    const fidGroups = video.ssrcGroups.filter(group => group.semantics === SSRC_GROUP_SEMANTICS.FID);
 
     if (video.simulcast || video.simulcast_03) {
         const ssrcs = [];
@@ -1247,7 +1252,7 @@ TraceablePeerConnection.prototype._injectSsrcGroupForUnifiedSimulcast = function
                 }
             });
         }
-        if (video.ssrcGroups.find(group => group.semantics === 'SIM')) {
+        if (video.ssrcGroups.find(group => group.semantics === SSRC_GROUP_SEMANTICS.SIM)) {
             // Group already exists, no need to do anything
             return desc;
         }
@@ -1257,7 +1262,7 @@ TraceablePeerConnection.prototype._injectSsrcGroupForUnifiedSimulcast = function
             const simSsrcs = ssrcs.slice(i, i + 3);
 
             video.ssrcGroups.push({
-                semantics: 'SIM',
+                semantics: SSRC_GROUP_SEMANTICS.SIM,
                 ssrcs: simSsrcs.join(' ')
             });
         }
@@ -1315,10 +1320,6 @@ const getters = {
         if (this.isP2P) {
             // Adjust the media direction for p2p based on whether a local source has been added.
             desc = this._adjustRemoteMediaDirection(desc);
-        } else {
-            // If this is a jvb connection, transform the SDP to Plan B first.
-            desc = this.interop.toPlanB(desc);
-            this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc));
         }
 
         return desc;
@@ -1591,7 +1592,7 @@ TraceablePeerConnection.prototype.getConfiguredVideoCodec = function(localTrack)
         return codecs[0].mimeType.split('/')[1].toLowerCase();
     }
 
-    const sdp = this.peerconnection.remoteDescription?.sdp;
+    const sdp = this.remoteDescription?.sdp;
     const defaultCodec = CodecMimeType.VP8;
 
     if (!sdp) {
@@ -1837,6 +1838,23 @@ TraceablePeerConnection.prototype.removeTrackFromPc = function(localTrack) {
     return this.tpcUtils.replaceTrack(localTrack, null).then(() => false);
 };
 
+/**
+ * Updates the remote source map with the given source map for adding or removing sources.
+ *
+ * @param {Map<string, TPCSourceInfo>} sourceMap - The map of source names to their corresponding SSRCs.
+ * @param {boolean} isAdd - Whether the sources are being added or removed.
+ * @returns {void}
+ */
+TraceablePeerConnection.prototype.updateRemoteSources = function(sourceMap, isAdd) {
+    for (const [ sourceName, ssrcInfo ] of sourceMap) {
+        if (isAdd) {
+            this._remoteSsrcMap.set(sourceName, ssrcInfo);
+        } else {
+            this._remoteSsrcMap.delete(sourceName);
+        }
+    }
+};
+
 /**
  * Returns true if the codec selection API is used for switching between codecs for the video sources.
  *
@@ -2217,12 +2235,6 @@ TraceablePeerConnection.prototype.setRemoteDescription = function(description) {
     // Munge stereo flag and opusMaxAverageBitrate based on config.js
     remoteDescription = this._mungeOpus(remoteDescription);
 
-    if (!this.isP2P) {
-        const currentDescription = this.peerconnection.remoteDescription;
-
-        remoteDescription = this.interop.toUnifiedPlan(remoteDescription, currentDescription);
-        this.trace('setRemoteDescription::postTransform (Unified)', dumpSDP(remoteDescription));
-    }
     if (this.isSpatialScalabilityOn()) {
         remoteDescription = this.tpcUtils.insertUnifiedPlanSimulcastReceive(remoteDescription);
         this.trace('setRemoteDescription::postTransform (sim receive)', dumpSDP(remoteDescription));
diff --git a/modules/sdp/RtxModifier.js b/modules/sdp/RtxModifier.js
index a9e0854100..6c25e894da 100644
--- a/modules/sdp/RtxModifier.js
+++ b/modules/sdp/RtxModifier.js
@@ -2,6 +2,7 @@ import { getLogger } from '@jitsi/logger';
 
 import { MediaDirection } from '../../service/RTC/MediaDirection';
 import { MediaType } from '../../service/RTC/MediaType';
+import { SSRC_GROUP_SEMANTICS } from '../../service/RTC/StandardVideoQualitySettings';
 
 import SDPUtil from './SDPUtil';
 import { SdpTransformWrap, parseSecondarySSRC } from './SdpTransformUtil';
@@ -48,7 +49,7 @@ function updateAssociatedRtxStream(mLine, primarySsrcInfo, rtxSsrc) {
         value: primarySsrcMsid
     });
     mLine.addSSRCGroup({
-        semantics: 'FID',
+        semantics: SSRC_GROUP_SEMANTICS.FID,
         ssrcs: `${primarySsrc} ${rtxSsrc}`
     });
 }
@@ -188,10 +189,10 @@ export default class RtxModifier {
             if (videoMLine.direction !== MediaDirection.RECVONLY
                 && videoMLine.getSSRCCount()
                 && videoMLine.containsAnySSRCGroups()) {
-                const fidGroups = videoMLine.findGroups('FID');
+                const fidGroups = videoMLine.findGroups(SSRC_GROUP_SEMANTICS.FID);
 
                 // Remove the fid groups from the mline
-                videoMLine.removeGroupsBySemantics('FID');
+                videoMLine.removeGroupsBySemantics(SSRC_GROUP_SEMANTICS.FID);
 
                 // Get the rtx ssrcs and remove them from the mline
                 for (const fidGroup of fidGroups) {
diff --git a/modules/sdp/SDP.js b/modules/sdp/SDP.js
index 3a442d0f0f..f7cb93b932 100644
--- a/modules/sdp/SDP.js
+++ b/modules/sdp/SDP.js
@@ -5,6 +5,7 @@ import { Strophe } from 'strophe.js';
 
 import { MediaDirection } from '../../service/RTC/MediaDirection';
 import { MediaType } from '../../service/RTC/MediaType';
+import { SSRC_GROUP_SEMANTICS } from '../../service/RTC/StandardVideoQualitySettings';
 import { XEP } from '../../service/xmpp/XMPPExtensioProtocols';
 import browser from '../browser';
 
@@ -24,7 +25,30 @@ export default class SDP {
      * @param {boolean} isP2P - Whether this SDP belongs to a p2p peerconnection.
      */
     constructor(sdp, isP2P = false) {
-        const media = sdp.split('\r\nm=');
+        this._updateSessionAndMediaSections(sdp);
+        this.isP2P = isP2P;
+        this.raw = this.session + this.media.join('');
+
+        // This flag will make {@link transportToJingle} and {@link jingle2media} replace ICE candidates IPs with
+        // invalid value of '1.1.1.1' which will cause ICE failure. The flag is used in the automated testing.
+        this.failICE = false;
+
+        // Whether or not to remove TCP ice candidates when translating from/to jingle.
+        this.removeTcpCandidates = false;
+
+        // Whether or not to remove UDP ice candidates when translating from/to jingle.
+        this.removeUdpCandidates = false;
+    }
+
+    /**
+     * Updates the media and session sections of the SDP based on the raw SDP string.
+     *
+     * @param {string} sdp - The SDP generated by the browser.
+     * @returns {void}
+     * @private
+     */
+    _updateSessionAndMediaSections(sdp) {
+        const media = typeof sdp === 'string' ? sdp.split('\r\nm=') : this.raw.split('\r\nm=');
 
         for (let i = 1, length = media.length; i < length; i++) {
             let mediaI = `m=${media[i]}`;
@@ -34,64 +58,104 @@ export default class SDP {
             }
             media[i] = mediaI;
         }
-        const session = `${media.shift()}\r\n`;
-
-        this.isP2P = isP2P;
+        this.session = `${media.shift()}\r\n`;
         this.media = media;
-        this.raw = session + media.join('');
-        this.session = session;
+    }
 
-        // This flag will make {@link transportToJingle} and {@link jingle2media} replace ICE candidates IPs with
-        // invalid value of '1.1.1.1' which will cause ICE failure. The flag is used in the automated testing.
-        this.failICE = false;
+    /**
+     * Adds or removes the sources from the SDP.
+     *
+     * @param {Object} sourceMap - The map of the sources that are being added/removed.
+     * @param {boolean} isAdd - Whether the sources are being added or removed.
+     * @returns {Array<number>} - The indices of the new m-lines that were added/modifed in the SDP.
+     */
+    updateRemoteSources(sourceMap, isAdd = true) {
+        const updatedMidIndices = [];
+
+        for (const source of sourceMap.values()) {
+            const { mediaType, msid, ssrcList, groups } = source;
+            let idx;
+
+            if (isAdd) {
+                // For P2P, check if there is an m-line with the matching mediaType that doesn't have any ssrc lines.
+                // Update the existing m-line if it exists, otherwise create a new m-line and add the sources.
+                idx = this.media.findIndex(mLine => mLine.includes(`m=${mediaType}`) && !mLine.includes('a=ssrc'));
+                if (!this.isP2P || idx === -1) {
+                    this.addMlineForNewSource(mediaType, true);
+                    idx = this.media.length - 1;
+                }
+            } else {
+                idx = this.media.findIndex(mLine => mLine.includes(`a=ssrc:${ssrcList[0]}`));
 
-        // Whether or not to remove TCP ice candidates when translating from/to jingle.
-        this.removeTcpCandidates = false;
+                if (idx === -1) {
+                    continue; // eslint-disable-line no-continue
+                }
+            }
 
-        // Whether or not to remove UDP ice candidates when translating from/to jingle.
-        this.removeUdpCandidates = false;
+            updatedMidIndices.push(idx);
+
+            if (isAdd) {
+                ssrcList.forEach(ssrc => {
+                    this.media[idx] += `a=ssrc:${ssrc} msid:${msid}\r\n`;
+                });
+                groups?.forEach(group => {
+                    this.media[idx] += `a=ssrc-group:${group.semantics} ${group.ssrcs.join(' ')}\r\n`;
+                });
+            } else {
+                ssrcList.forEach(ssrc => {
+                    this.media[idx] = this.media[idx].replace(new RegExp(`a=ssrc:${ssrc}.*\r\n`, 'g'), '');
+                });
+                groups?.forEach(group => {
+                    this.media[idx] = this.media[idx]
+                        .replace(new RegExp(`a=ssrc-group:${group.semantics}.*\r\n`, 'g'), '');
+                });
+
+                if (!this.isP2P) {
+                    // Reject the m-line so that the browser removes the associated transceiver from the list of
+                    // available transceivers. This will prevent the client from trying to re-use these inactive
+                    // transceivers when additional video sources are added to the peerconnection.
+                    const { media, port } = SDPUtil.parseMLine(this.media[idx].split('\r\n')[0]);
+
+                    this.media[idx] = this.media[idx]
+                        .replace(`a=${MediaDirection.SENDONLY}`, `a=${MediaDirection.INACTIVE}`);
+                    this.media[idx] = this.media[idx].replace(`m=${media} ${port}`, `m=${media} 0`);
+                }
+            }
+            this.raw = this.session + this.media.join('');
+        }
+
+        return updatedMidIndices;
     }
 
     /**
-     * Adds a new m-line to the description so that a new local source can then be attached to the transceiver that gets
-     * added after a reneogtiation cycle.
+     * Adds a new m-line to the description so that a new local or remote source can be added to the conference.
      *
      * @param {MediaType} mediaType media type of the new source that is being added.
      * @returns {void}
      */
-    addMlineForNewLocalSource(mediaType) {
+    addMlineForNewSource(mediaType, isRemote = false) {
         const mid = this.media.length;
         const sdp = transform.parse(this.raw);
         const mline = cloneDeep(sdp.media.find(m => m.type === mediaType));
 
         // Edit media direction, mid and remove the existing ssrc lines in the m-line.
         mline.mid = mid;
-        mline.direction = MediaDirection.RECVONLY;
+        mline.direction = isRemote ? MediaDirection.SENDONLY : MediaDirection.RECVONLY;
         mline.msid = undefined;
         mline.ssrcs = undefined;
         mline.ssrcGroups = undefined;
 
-        // We regenerate the BUNDLE group (since we added a new m-line).
         sdp.media = [ ...sdp.media, mline ];
 
+        // We regenerate the BUNDLE group (since we added a new m-line).
         sdp.groups.forEach(group => {
             if (group.type === 'BUNDLE') {
                 group.mids = [ ...group.mids.split(' '), mid ].join(' ');
             }
         });
-        this.raw = transform.write(sdp);
-    }
-
-    /**
-     * Checks if a given SSRC is present in the SDP.
-     *
-     * @param {string} ssrc
-     * @returns {boolean}
-     */
-    containsSSRC(ssrc) {
-        const sourceMap = this.getMediaSsrcMap();
 
-        return [ ...sourceMap.values() ].some(media => media.ssrcs[ssrc]);
+        this.raw = transform.write(sdp);
+        this._updateSessionAndMediaSections();
     }
 
     /**
@@ -111,7 +175,7 @@ export default class SDP {
 
         const groups = $(jingle).find(`>group[xmlns='${XEP.BUNDLE_MEDIA}']`);
 
-        if (groups.length) {
+        if (this.isP2P && groups.length) {
             groups.each((idx, group) => {
                 const contents = $(group)
                     .find('>content')
@@ -136,6 +200,97 @@ export default class SDP {
         });
 
         this.raw = this.session + this.media.join('');
+
+        if (this.isP2P) {
+            return;
+        }
+
+        // For offers from Jicofo, a new m-line needs to be created for each new remote source that is added to the
+        // conference.
+        const newSession = transform.parse(this.raw);
+        const newMedia = [];
+
+        newSession.media.forEach(mLine => {
+            const type = mLine.type;
+
+            if (type === MediaType.APPLICATION) {
+                const newMline = cloneDeep(mLine);
+
+                newMline.mid = newMedia.length.toString();
+                newMedia.push(newMline);
+
+                return;
+            }
+
+            if (!mLine.ssrcs?.length) {
+                const newMline = cloneDeep(mLine);
+
+                newMline.mid = newMedia.length.toString();
+                newMedia.push(newMline);
+
+                return;
+            }
+
+            mLine.ssrcs.forEach((ssrc, idx) => {
+                // Do nothing if the m-line with the given SSRC already exists.
+                if (newMedia.some(mline => mline.ssrcs?.some(source => source.id === ssrc.id))) {
+                    return;
+                }
+                const newMline = cloneDeep(mLine);
+
+                newMline.ssrcs = [];
+                newMline.ssrcGroups = [];
+                newMline.mid = newMedia.length.toString();
+                newMline.bundleOnly = undefined;
+                newMline.direction = idx ? 'sendonly' : 'sendrecv';
+
+                // Add the sources and the related FID source group to the new m-line.
+                const ssrcId = ssrc.id.toString();
+                const group = mLine.ssrcGroups?.find(g => g.ssrcs.includes(ssrcId));
+
+                if (group) {
+                    newMline.ssrcs.push(ssrc);
+                    const otherSsrc = group.ssrcs.split(' ').find(s => s !== ssrcId);
+
+                    if (otherSsrc) {
+                        const otherSource = mLine.ssrcs.find(source => source.id.toString() === otherSsrc);
+
+                        newMline.ssrcs.push(otherSource);
+                    }
+                    newMline.ssrcGroups.push(group);
+                } else {
+                    newMline.ssrcs.push(ssrc);
+                }
+                newMedia.push(newMline);
+            });
+        });
+
+        newSession.media = newMedia;
+        const mids = [];
+
+        newMedia.forEach(mLine => {
+            mids.push(mLine.mid);
+        });
+
+        if (groups.length) {
+            // We regenerate the BUNDLE group (since we regenerated the mids)
+            newSession.groups = [ {
+                type: 'BUNDLE',
+                mids: mids.join(' ')
+            } ];
+        }
+
+        // msid semantic
+        newSession.msidSemantic = {
+            semantic: 'WMS',
+            token: '*'
+        };
+
+        // Increment the session version every time.
+        newSession.origin.sessionVersion++;
+
+        this.raw = transform.write(newSession);
+        this._updateSessionAndMediaSections();
     }
 
     /**
@@ -634,7 +789,7 @@ export default class SDP {
 
                     if (unifiedSimulcast) {
                         elem.c('rid-group', {
-                            semantics: 'SIM',
+                            semantics: SSRC_GROUP_SEMANTICS.SIM,
                             xmlns: XEP.SOURCE_ATTRIBUTES
                         });
                         rids.forEach(rid => elem.c('source', { rid }).up());
diff --git a/modules/sdp/SDP.spec.js b/modules/sdp/SDP.spec.js
index bb857e0767..cf864156e3 100644
--- a/modules/sdp/SDP.spec.js
+++ b/modules/sdp/SDP.spec.js
@@ -6,7 +6,7 @@ import { expandSourcesFromJson } from '../xmpp/JingleHelperFunctions';
 
 import SDP from './SDP';
 
-/* eslint-disable max-len*/
+/* eslint-disable max-len */
 
 /**
  * @param {string} xml - raw xml of the stanza
@@ -51,8 +51,6 @@ describe('SDP', () => {
             'a=rtpmap:100 VP8/90000\r\n',
             'a=rtpmap:99 rtx/90000\r\n',
             'a=rtpmap:96 rtx/90000\r\n',
-            'a=fmtp:107 x-google-start-bitrate=800\r\n',
-            'a=fmtp:100 x-google-start-bitrate=800\r\n',
             'a=fmtp:99 apt=107\r\n',
             'a=fmtp:96 apt=100\r\n',
             'a=rtcp:9 IN IP4 0.0.0.0\r\n',
@@ -998,7 +996,28 @@ describe('SDP', () => {
     });
 
     describe('fromJingle', () => {
-        const stanza = `<iq>
+        let sdp;
+
+        beforeEach(() => {
+            sdp = new SDP('');
+        });
+
+        it('should handle no sources', () => {
+            const jingle = $(
+                `<jingle xmlns='urn:xmpp:jingle:1'>
+                    <content name='audio'>
+                        <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>
+                    </content>
+                </jingle>`
+            );
+
+            sdp.fromJingle(jingle);
+
+            expect(sdp.raw).toContain('m=audio');
+        });
+
+        it('gets converted to SDP', () => {
+            const stanza = `<iq>
 <jingle action='session-initiate' initiator='focus' sid='123' xmlns='urn:xmpp:jingle:1'>
     <content creator='initiator' name='audio' senders='both'>
         <description media='audio' maxptime='60' xmlns='urn:xmpp:jingle:apps:rtp:1'>
@@ -1037,7 +1056,6 @@ describe('SDP', () => {
                 <rtcp-fb subtype='pli' type='nack' xmlns='urn:xmpp:jingle:apps:rtp:rtcp-fb:0'/>
                 <rtcp-fb type='goog-remb' xmlns='urn:xmpp:jingle:apps:rtp:rtcp-fb:0'/>
                 <rtcp-fb type='transport-cc' xmlns='urn:xmpp:jingle:apps:rtp:rtcp-fb:0'/>
-                <parameter name='x-google-start-bitrate' value='800'/>
             </payload-type>
             <payload-type clockrate='90000' name='rtx' id='96'>
                 <rtcp-fb subtype='fir' type='ccm' xmlns='urn:xmpp:jingle:apps:rtp:rtcp-fb:0'/>
@@ -1067,83 +1085,73 @@ describe('SDP', () => {
         <content name='video'/>
     </group>
 </jingle></iq>`;
-        const expectedSDP = `v=0
-o=- 123 2 IN IP4 0.0.0.0
+            const expectedSDP = `v=0
+o=- 123 3 IN IP4 0.0.0.0
 s=-
 t=0 0
-a=group:BUNDLE audio video
+a=msid-semantic: WMS *
+a=group:BUNDLE 0 1
 m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 126
 c=IN IP4 0.0.0.0
-a=rtcp:1 IN IP4 0.0.0.0
-a=ice-ufrag:someufrag
-a=ice-pwd:somepwd
-a=fingerprint:sha-256 09:B1:51:0F:85:4C:80:19:A1:AF:81:73:47:EE:ED:3D:00:3A:84:C7:76:C1:4E:34:BE:56:F6:42:AD:15:D5:D7
-a=setup:actpass
-a=candidate:1 1 udp 2130706431 10.0.0.1 10000 typ host generation 0
-a=candidate:2 1 udp 1694498815 10.0.0.2 10000 typ srflx raddr 10.0.0.1 rport 10000 generation 0
-a=sendrecv
-a=mid:audio
-a=rtcp-mux
 a=rtpmap:111 opus/48000/2
-a=fmtp:111 minptime=10;useinbandfec=1
-a=rtcp-fb:111 transport-cc
 a=rtpmap:103 ISAC/16000
 a=rtpmap:104 ISAC/32000
 a=rtpmap:126 telephone-event/8000
+a=fmtp:111 minptime=10;useinbandfec=1
 a=fmtp:126 0-15
+a=rtcp:1 IN IP4 0.0.0.0
+a=rtcp-fb:111 transport-cc
 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
 a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
-a=extmap-allow-mixed
-a=ssrc:4039389863 cname:mixed
-a=ssrc:4039389863 label:mixedlabelaudio0
-a=ssrc:4039389863 msid:mixedmslabel mixedlabelaudio0
-a=ssrc:4039389863 mslabel:mixedmslabel
-m=video 9 UDP/TLS/RTP/SAVPF 100 96
-c=IN IP4 0.0.0.0
-a=rtcp:1 IN IP4 0.0.0.0
+a=setup:actpass
+a=mid:0
+a=sendrecv
 a=ice-ufrag:someufrag
 a=ice-pwd:somepwd
 a=fingerprint:sha-256 09:B1:51:0F:85:4C:80:19:A1:AF:81:73:47:EE:ED:3D:00:3A:84:C7:76:C1:4E:34:BE:56:F6:42:AD:15:D5:D7
-a=setup:actpass
 a=candidate:1 1 udp 2130706431 10.0.0.1 10000 typ host generation 0
 a=candidate:2 1 udp 1694498815 10.0.0.2 10000 typ srflx raddr 10.0.0.1 rport 10000 generation 0
-a=sendrecv
-a=mid:video
+a=ssrc:4039389863 cname:mixed
 a=rtcp-mux
+a=extmap-allow-mixed
+m=video 9 UDP/TLS/RTP/SAVPF 100 96
+c=IN IP4 0.0.0.0
 a=rtpmap:100 VP8/90000
-a=fmtp:100 x-google-start-bitrate=800
+a=rtpmap:96 rtx/90000
+a=fmtp:96 apt=100
+a=rtcp:1 IN IP4 0.0.0.0
 a=rtcp-fb:100 ccm fir
 a=rtcp-fb:100 nack
 a=rtcp-fb:100 nack pli
 a=rtcp-fb:100 goog-remb
 a=rtcp-fb:100 transport-cc
-a=rtpmap:96 rtx/90000
-a=fmtp:96 apt=100
 a=rtcp-fb:96 ccm fir
 a=rtcp-fb:96 nack
 a=rtcp-fb:96 nack pli
 a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
 a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
-a=extmap-allow-mixed
+a=setup:actpass
+a=mid:1
+a=sendrecv
+a=ice-ufrag:someufrag
+a=ice-pwd:somepwd
+a=fingerprint:sha-256 09:B1:51:0F:85:4C:80:19:A1:AF:81:73:47:EE:ED:3D:00:3A:84:C7:76:C1:4E:34:BE:56:F6:42:AD:15:D5:D7
+a=candidate:1 1 udp 2130706431 10.0.0.1 10000 typ host generation 0
+a=candidate:2 1 udp 1694498815 10.0.0.2 10000 typ srflx raddr 10.0.0.1 rport 10000 generation 0
 a=ssrc:3758540092 cname:mixed
-a=ssrc:3758540092 label:mixedlabelvideo0
-a=ssrc:3758540092 msid:mixedmslabel mixedlabelvideo0
-a=ssrc:3758540092 mslabel:mixedmslabel
+a=rtcp-mux
+a=extmap-allow-mixed
 `.split('\n').join('\r\n');
-
-        it('gets converted to SDP', () => {
             const offer = createStanzaElement(stanza);
-            const sdp = new SDP('');
 
             sdp.fromJingle($(offer).find('>jingle'));
             const rawSDP = sdp.raw.replace(/o=- \d+/, 'o=- 123'); // replace generated o= timestamp.
 
             expect(rawSDP).toEqual(expectedSDP);
         });
-    });
 
-    describe('fromJingleWithJSONFormat', () => {
-        const stanza = `
+        it('fromJingleWithJSONFormat gets converted to SDP', () => {
+            const stanza = `
     <iq>
         <jingle xmlns="urn:xmpp:jingle:1" action="session-initiate" initiator="focus" sid="123">
             <content name="audio" creator="initiator" senders="both">
@@ -1172,7 +1180,6 @@ a=ssrc:3758540092 mslabel:mixedmslabel
                         <rtcp-fb xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0" subtype="fir" type="ccm"/>
                         <rtcp-fb xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0" type="nack"/>
                         <rtcp-fb xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0" subtype="pli" type="nack"/>
-                        <parameter value="800" name="x-google-start-bitrate"/>
                         <rtcp-fb xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0" type="transport-cc"/>
                     </payload-type>
                     <payload-type name="rtx" clockrate="90000" id="96">
@@ -1195,95 +1202,143 @@ a=ssrc:3758540092 mslabel:mixedmslabel
                 <content name="audio"/>
                 <content name="video"/>
             </group>
-            <json-message xmlns="http://jitsi.org/jitmeet">{"sources":{"831de82b":[[{"s":257838819,"m":"831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1"},{"s":865670341,"m":"831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1"},{"s":3041289080,"m":"831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1"},{"s":6437989,"m":"831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1"},{"s":2417192010,"m":"831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1"},{"s":3368859313,"m":"831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1"}],[["f",257838819,865670341],["s",257838819,3041289080,6437989],["f",3041289080,2417192010],["f",6437989,3368859313]],[]],"07af8d49":[[{"s":110279275,"m":"07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2"},{"s":527738645,"m":"07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2"},{"s":1201074111,"m":"07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2"},{"s":1635907749,"m":"07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2"},{"s":3873826414,"m":"07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2"},{"s":4101906340,"m":"07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2"}],[["f",3873826414,110279275],["s",3873826414,1201074111,1635907749],["f",1201074111,4101906340],["f",1635907749,527738645]],[]],"95edea8d":[[{"s":620660772,"m":"95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1"},{"s":2212130687,"m":"95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1"},{"s":2306112481,"m":"95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1"},{"s":3334993162,"m":"95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1"},{"s":3473290740,"m":"95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1"},{"s":4085804879,"m":"95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1"}],[["f",2306112481,620660772],["s",2306112481,3334993162,4085804879],["f",3334993162,3473290740],["f",4085804879,2212130687]],[]],"jvb":[[{"s":1427774514,"m":"mixedmslabel mixedlabelvideo0","c":"mixed"}],[],[{"s":3659539811,"m":"mixedmslabel mixedlabelaudio0","c":"mixed"}]]}}</json-message>
+            <json-message xmlns="http://jitsi.org/jitmeet">{"sources":{"831de82b":[[{"s":257838819,"m":"831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1"},{"s":865670341,"m":"831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1"}],[["f",257838819,865670341]],[]],"07af8d49":[[{"s":110279275,"m":"07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2"},{"s":3873826414,"m":"07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2"}],[["f",3873826414,110279275]],[]],"95edea8d":[[{"s":620660772,"m":"95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1"},{"s":2306112481,"m":"95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1"}],[["f",2306112481,620660772]],[]],"jvb":[[{"s":1427774514,"m":"mixedmslabel mixedlabelvideo0","c":"mixed"}],[],[{"s":3659539811,"m":"mixedmslabel mixedlabelaudio0","c":"mixed"}]]}}</json-message>
         </jingle>
     </iq>`;
-        const expectedSDP = `v=0
-o=- 123 2 IN IP4 0.0.0.0
+            const expectedSDP = `v=0
+o=- 123 3 IN IP4 0.0.0.0
 s=-
 t=0 0
-a=group:BUNDLE audio video
+a=msid-semantic: WMS *
+a=group:BUNDLE 0 1 2 3 4
 m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 126
 c=IN IP4 0.0.0.0
-a=rtcp:1 IN IP4 0.0.0.0
-a=ice-ufrag:someufrag
-a=ice-pwd:somepwd
-a=fingerprint:sha-256 09:B1:51:0F:85:4C:80:19:A1:AF:81:73:47:EE:ED:3D:00:3A:84:C7:76:C1:4E:34:BE:56:F6:42:AD:15:D5:D7
-a=setup:actpass
-a=candidate:1 1 udp 2130706431 10.0.0.1 10000 typ host generation 0
-a=candidate:2 1 udp 1694498815 10.0.0.2 10000 typ srflx raddr 10.0.0.1 rport 10000 generation 0
-a=sendrecv
-a=mid:audio
-a=rtcp-mux
 a=rtpmap:111 opus/48000/2
-a=fmtp:111 minptime=10;useinbandfec=1
-a=rtcp-fb:111 transport-cc
 a=rtpmap:103 ISAC/16000
 a=rtpmap:104 ISAC/32000
 a=rtpmap:126 telephone-event/8000
+a=fmtp:111 minptime=10;useinbandfec=1
+a=rtcp:1 IN IP4 0.0.0.0
+a=rtcp-fb:111 transport-cc
 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
 a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
+a=setup:actpass
+a=mid:0
+a=sendrecv
+a=ice-ufrag:someufrag
+a=ice-pwd:somepwd
+a=fingerprint:sha-256 09:B1:51:0F:85:4C:80:19:A1:AF:81:73:47:EE:ED:3D:00:3A:84:C7:76:C1:4E:34:BE:56:F6:42:AD:15:D5:D7
+a=candidate:1 1 udp 2130706431 10.0.0.1 10000 typ host generation 0
+a=candidate:2 1 udp 1694498815 10.0.0.2 10000 typ srflx raddr 10.0.0.1 rport 10000 generation 0
 a=ssrc:3659539811 msid:mixedmslabel mixedlabelaudio0
+a=rtcp-mux
 m=video 9 UDP/TLS/RTP/SAVPF 100 96
 c=IN IP4 0.0.0.0
+a=rtpmap:100 VP8/90000
+a=rtpmap:96 rtx/90000
+a=fmtp:96 apt=100
 a=rtcp:1 IN IP4 0.0.0.0
+a=rtcp-fb:100 ccm fir
+a=rtcp-fb:100 nack
+a=rtcp-fb:100 nack pli
+a=rtcp-fb:100 transport-cc
+a=rtcp-fb:96 ccm fir
+a=rtcp-fb:96 nack
+a=rtcp-fb:96 nack pli
+a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
+a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
+a=setup:actpass
+a=mid:1
+a=sendrecv
 a=ice-ufrag:someufrag
 a=ice-pwd:somepwd
 a=fingerprint:sha-256 09:B1:51:0F:85:4C:80:19:A1:AF:81:73:47:EE:ED:3D:00:3A:84:C7:76:C1:4E:34:BE:56:F6:42:AD:15:D5:D7
-a=setup:actpass
 a=candidate:1 1 udp 2130706431 10.0.0.1 10000 typ host generation 0
 a=candidate:2 1 udp 1694498815 10.0.0.2 10000 typ srflx raddr 10.0.0.1 rport 10000 generation 0
-a=sendrecv
-a=mid:video
+a=ssrc:1427774514 msid:mixedmslabel mixedlabelvideo0
 a=rtcp-mux
+m=video 9 UDP/TLS/RTP/SAVPF 100 96
+c=IN IP4 0.0.0.0
 a=rtpmap:100 VP8/90000
-a=fmtp:100 x-google-start-bitrate=800
+a=rtpmap:96 rtx/90000
+a=fmtp:96 apt=100
+a=rtcp:1 IN IP4 0.0.0.0
 a=rtcp-fb:100 ccm fir
 a=rtcp-fb:100 nack
 a=rtcp-fb:100 nack pli
 a=rtcp-fb:100 transport-cc
-a=rtpmap:96 rtx/90000
-a=fmtp:96 apt=100
 a=rtcp-fb:96 ccm fir
 a=rtcp-fb:96 nack
 a=rtcp-fb:96 nack pli
 a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
 a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
-a=ssrc-group:FID 257838819 865670341
-a=ssrc-group:SIM 257838819 3041289080 6437989
-a=ssrc-group:FID 3041289080 2417192010
-a=ssrc-group:FID 6437989 3368859313
-a=ssrc-group:FID 3873826414 110279275
-a=ssrc-group:SIM 3873826414 1201074111 1635907749
-a=ssrc-group:FID 1201074111 4101906340
-a=ssrc-group:FID 1635907749 527738645
-a=ssrc-group:FID 2306112481 620660772
-a=ssrc-group:SIM 2306112481 3334993162 4085804879
-a=ssrc-group:FID 3334993162 3473290740
-a=ssrc-group:FID 4085804879 2212130687
-a=ssrc:1427774514 msid:mixedmslabel mixedlabelvideo0
+a=setup:actpass
+a=mid:2
+a=sendonly
+a=ice-ufrag:someufrag
+a=ice-pwd:somepwd
+a=fingerprint:sha-256 09:B1:51:0F:85:4C:80:19:A1:AF:81:73:47:EE:ED:3D:00:3A:84:C7:76:C1:4E:34:BE:56:F6:42:AD:15:D5:D7
+a=candidate:1 1 udp 2130706431 10.0.0.1 10000 typ host generation 0
+a=candidate:2 1 udp 1694498815 10.0.0.2 10000 typ srflx raddr 10.0.0.1 rport 10000 generation 0
 a=ssrc:257838819 msid:831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1
 a=ssrc:865670341 msid:831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1
-a=ssrc:3041289080 msid:831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1
-a=ssrc:6437989 msid:831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1
-a=ssrc:2417192010 msid:831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1
-a=ssrc:3368859313 msid:831de82b-video-1 9bb949d4-5abd-4498-98e9-2be1222b8d3e-1
+a=ssrc-group:FID 257838819 865670341
+a=rtcp-mux
+m=video 9 UDP/TLS/RTP/SAVPF 100 96
+c=IN IP4 0.0.0.0
+a=rtpmap:100 VP8/90000
+a=rtpmap:96 rtx/90000
+a=fmtp:96 apt=100
+a=rtcp:1 IN IP4 0.0.0.0
+a=rtcp-fb:100 ccm fir
+a=rtcp-fb:100 nack
+a=rtcp-fb:100 nack pli
+a=rtcp-fb:100 transport-cc
+a=rtcp-fb:96 ccm fir
+a=rtcp-fb:96 nack
+a=rtcp-fb:96 nack pli
+a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
+a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
+a=setup:actpass
+a=mid:3
+a=sendonly
+a=ice-ufrag:someufrag
+a=ice-pwd:somepwd
+a=fingerprint:sha-256 09:B1:51:0F:85:4C:80:19:A1:AF:81:73:47:EE:ED:3D:00:3A:84:C7:76:C1:4E:34:BE:56:F6:42:AD:15:D5:D7
+a=candidate:1 1 udp 2130706431 10.0.0.1 10000 typ host generation 0
+a=candidate:2 1 udp 1694498815 10.0.0.2 10000 typ srflx raddr 10.0.0.1 rport 10000 generation 0
 a=ssrc:110279275 msid:07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2
-a=ssrc:527738645 msid:07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2
-a=ssrc:1201074111 msid:07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2
-a=ssrc:1635907749 msid:07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2
 a=ssrc:3873826414 msid:07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2
-a=ssrc:4101906340 msid:07af8d49-video-2 f685aa25-0318-442e-bd00-cd2a911236da-2
+a=ssrc-group:FID 3873826414 110279275
+a=rtcp-mux
+m=video 9 UDP/TLS/RTP/SAVPF 100 96
+c=IN IP4 0.0.0.0
+a=rtpmap:100 VP8/90000
+a=rtpmap:96 rtx/90000
+a=fmtp:96 apt=100
+a=rtcp:1 IN IP4 0.0.0.0
+a=rtcp-fb:100 ccm fir
+a=rtcp-fb:100 nack
+a=rtcp-fb:100 nack pli
+a=rtcp-fb:100 transport-cc
+a=rtcp-fb:96 ccm fir
+a=rtcp-fb:96 nack
+a=rtcp-fb:96 nack pli
+a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
+a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
+a=setup:actpass
+a=mid:4
+a=sendonly
+a=ice-ufrag:someufrag
+a=ice-pwd:somepwd
+a=fingerprint:sha-256 09:B1:51:0F:85:4C:80:19:A1:AF:81:73:47:EE:ED:3D:00:3A:84:C7:76:C1:4E:34:BE:56:F6:42:AD:15:D5:D7
+a=candidate:1 1 udp 2130706431 10.0.0.1 10000 typ host generation 0
+a=candidate:2 1 udp 1694498815 10.0.0.2 10000 typ srflx raddr 10.0.0.1 rport 10000 generation 0
 a=ssrc:620660772 msid:95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1
-a=ssrc:2212130687 msid:95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1
 a=ssrc:2306112481 msid:95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1
-a=ssrc:3334993162 msid:95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1
-a=ssrc:3473290740 msid:95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1
-a=ssrc:4085804879 msid:95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1
+a=ssrc-group:FID 2306112481 620660772
+a=rtcp-mux
 `.split('\n').join('\r\n');
-        /* eslint-enable max-len*/
-
-        it('gets converted to SDP', () => {
             const offer = createStanzaElement(stanza);
             const jsonMessages = $(offer).find('jingle>json-message');
 
@@ -1291,12 +1346,77 @@ a=ssrc:4085804879 msid:95edea8d-video-1 0c5d94d1-1902-4fb7-bf6a-76517d065d02-1
                 expandSourcesFromJson(offer, jsonMessages[i]);
             }
 
-            const sdp = new SDP('');
-
             sdp.fromJingle($(offer).find('>jingle'));
             const rawSDP = sdp.raw.replace(/o=- \d+/, 'o=- 123'); // replace generated o= timestamp.
 
             expect(rawSDP).toEqual(expectedSDP);
         });
     });
+
+    /* eslint-disable max-len */
+    describe('jingle2media', () => {
+        it('should convert basic Jingle content to SDP', () => {
+            const jingleContent = createStanzaElement(`
+                <content name="audio">
+                    <description media="audio" xmlns="urn:xmpp:jingle:apps:rtp:1">
+                        <payload-type id="111" name="opus" clockrate="48000" channels="2"/>
+                    </description>
+                    <transport xmlns="urn:xmpp:jingle:transports:ice-udp:1">
+                        <candidate foundation="1" component="1" protocol="udp" priority="2130706431" ip="192.168.1.1" port="10000" type="host"/>
+                    </transport>
+                </content>
+            `);
+
+            const sdp = new SDP('');
+            const media = sdp.jingle2media($(jingleContent));
+
+            expect(media).toContain('m=audio 9 UDP/TLS/RTP/SAVPF 111');
+            expect(media).toContain('a=rtpmap:111 opus/48000/2');
+            expect(media).toContain('c=IN IP4 0.0.0.0');
+            expect(media).toContain('a=candidate:1 1 udp 2130706431 192.168.1.1 10000 typ host');
+        });
+
+        it('should convert Jingle content with multiple payload types to SDP', () => {
+            const jingleContent = createStanzaElement(`
+                <content name="video">
+                    <description media="video" xmlns="urn:xmpp:jingle:apps:rtp:1">
+                        <payload-type id="100" name="VP8" clockrate="90000"/>
+                        <payload-type id="101" name="VP9" clockrate="90000"/>
+                    </description>
+                    <transport xmlns="urn:xmpp:jingle:transports:ice-udp:1">
+                        <candidate foundation="1" component="1" protocol="udp" priority="2130706431" ip="192.168.1.1" port="10000" type="host"/>
+                    </transport>
+                </content>
+            `);
+
+            const sdp = new SDP('');
+            const media = sdp.jingle2media($(jingleContent));
+
+            expect(media).toContain('m=video 9 UDP/TLS/RTP/SAVPF 100 101');
+            expect(media).toContain('a=rtpmap:100 VP8/90000');
+            expect(media).toContain('a=rtpmap:101 VP9/90000');
+            expect(media).toContain('c=IN IP4 0.0.0.0');
+            expect(media).toContain('a=candidate:1 1 udp 2130706431 192.168.1.1 10000 typ host');
+        });
+
+        it('should convert Jingle content with ICE candidates to SDP', () => {
+            const jingleContent = createStanzaElement(`
+                <content name="audio">
+                    <description media="audio" xmlns="urn:xmpp:jingle:apps:rtp:1">
+                        <payload-type id="111" name="opus" clockrate="48000" channels="2"/>
+                    </description>
+                    <transport xmlns="urn:xmpp:jingle:transports:ice-udp:1">
+                        <candidate foundation="1" component="1" protocol="udp" priority="2130706431" ip="192.168.1.1" port="10000" type="host"/>
+                        <candidate foundation="2" component="1" protocol="tcp" priority="2130706430" ip="192.168.1.2" port="10001" type="host"/>
+                    </transport>
+                </content>
+            `);
+
+            const sdp = new SDP('');
+            const media = sdp.jingle2media($(jingleContent));
+
+            expect(media).toContain('a=candidate:1 1 udp 2130706431 192.168.1.1 10000 typ host');
+            expect(media).toContain('a=candidate:2 1 tcp 2130706430 192.168.1.2 10001 typ host');
+        });
+    });
 });
diff --git a/modules/sdp/SDPUtil.js b/modules/sdp/SDPUtil.js
index 271b98b34c..c3e6f1a374 100644
--- a/modules/sdp/SDPUtil.js
+++ b/modules/sdp/SDPUtil.js
@@ -3,6 +3,7 @@ const logger = getLogger(__filename);
 
 import { CodecMimeType } from '../../service/RTC/CodecMimeType';
 import { MediaDirection } from '../../service/RTC/MediaDirection';
+import { SSRC_GROUP_SEMANTICS } from '../../service/RTC/StandardVideoQualitySettings';
 import browser from '../browser';
 import RandomUtil from '../util/RandomUtil';
 
@@ -543,7 +544,7 @@ const SDPUtil = {
             // Can figure it out if there's an FID group
             const fidGroup
                 = videoMLine.ssrcGroups.find(
-                    group => group.semantics === 'FID');
+                    group => group.semantics === SSRC_GROUP_SEMANTICS.FID);
 
             if (fidGroup) {
                 primarySsrc = fidGroup.ssrcs.split(' ')[0];
@@ -552,7 +553,7 @@ const SDPUtil = {
             // Can figure it out if there's a sim group
             const simGroup
                 = videoMLine.ssrcGroups.find(
-                    group => group.semantics === 'SIM');
+                    group => group.semantics === SSRC_GROUP_SEMANTICS.SIM);
 
             if (simGroup) {
                 primarySsrc = simGroup.ssrcs.split(' ')[0];
diff --git a/modules/sdp/SdpSimulcast.ts b/modules/sdp/SdpSimulcast.ts
index 2dd4b593fa..2811ac3c41 100644
--- a/modules/sdp/SdpSimulcast.ts
+++ b/modules/sdp/SdpSimulcast.ts
@@ -1,6 +1,6 @@
 import { MediaDirection } from '../../service/RTC/MediaDirection';
 import { MediaType } from '../../service/RTC/MediaType';
-import { SIM_LAYERS } from '../../service/RTC/StandardVideoQualitySettings';
+import { SIM_LAYERS, SSRC_GROUP_SEMANTICS } from '../../service/RTC/StandardVideoQualitySettings';
 
 import * as transform from 'sdp-transform';
 
@@ -61,7 +61,7 @@ export default class SdpSimulcast {
         }
 
         mLine.ssrcGroups.push({
-            semantics: 'SIM',
+            semantics: SSRC_GROUP_SEMANTICS.SIM,
             ssrcs: cachedSsrcs.join(' ')
         });
 
@@ -120,7 +120,7 @@ export default class SdpSimulcast {
 
         mLine.ssrcGroups = mLine.ssrcGroups || [];
         mLine.ssrcGroups.push({
-            semantics: 'SIM',
+            semantics: SSRC_GROUP_SEMANTICS.SIM,
             ssrcs: primarySsrc + ' ' + simSsrcs.join(' ')
         });
 
@@ -159,7 +159,7 @@ export default class SdpSimulcast {
      * @returns
      */
     _parseSimLayers(mLine: transform.MediaDescription) : Array<number> | null {
-        const simGroup = mLine.ssrcGroups?.find(group => group.semantics === 'SIM');
+        const simGroup = mLine.ssrcGroups?.find(group => group.semantics === SSRC_GROUP_SEMANTICS.SIM);
 
         if (simGroup) {
             return simGroup.ssrcs.split(' ').map(ssrc => Number(ssrc));
@@ -209,7 +209,7 @@ export default class SdpSimulcast {
             if (numSsrcs.size === 1) {
                 primarySsrc = Number(media.ssrcs[0]?.id);
             } else {
-                const fidGroup = media.ssrcGroups.find(group => group.semantics === 'FID');
+                const fidGroup = media.ssrcGroups.find(group => group.semantics === SSRC_GROUP_SEMANTICS.FID);
 
                 if (fidGroup) {
                     primarySsrc = Number(fidGroup.ssrcs.split(' ')[0]);
diff --git a/modules/sdp/SdpTransformUtil.js b/modules/sdp/SdpTransformUtil.js
index 65e3bc8452..14e72ac014 100644
--- a/modules/sdp/SdpTransformUtil.js
+++ b/modules/sdp/SdpTransformUtil.js
@@ -1,5 +1,7 @@
 import * as transform from 'sdp-transform';
 
+import { SSRC_GROUP_SEMANTICS } from '../../service/RTC/StandardVideoQualitySettings';
+
 /**
  * Parses the primary SSRC of given SSRC group.
  * @param {object} group the SSRC group object as defined by the 'sdp-transform'
@@ -232,12 +234,12 @@ class MLineWrap {
 
         // Look for a SIM, FID, or FEC-FR group
         if (this.mLine.ssrcGroups) {
-            const simGroup = this.findGroup('SIM');
+            const simGroup = this.findGroup(SSRC_GROUP_SEMANTICS.SIM);
 
             if (simGroup) {
                 return parsePrimarySSRC(simGroup);
             }
-            const fidGroup = this.findGroup('FID');
+            const fidGroup = this.findGroup(SSRC_GROUP_SEMANTICS.FID);
 
             if (fidGroup) {
                 return parsePrimarySSRC(fidGroup);
@@ -260,7 +262,7 @@ class MLineWrap {
      * one)
      */
     getRtxSSRC(primarySsrc) {
-        const fidGroup = this.findGroupByPrimarySSRC('FID', primarySsrc);
+        const fidGroup = this.findGroupByPrimarySSRC(SSRC_GROUP_SEMANTICS.FID, primarySsrc);
 
 
         return fidGroup && parseSecondarySSRC(fidGroup);
@@ -295,7 +297,7 @@ class MLineWrap {
             // Right now, FID and FEC-FR groups are the only ones we parse to
             // disqualify streams.  If/when others arise we'll
             // need to add support for them here
-            if (ssrcGroupInfo.semantics === 'FID'
+            if (ssrcGroupInfo.semantics === SSRC_GROUP_SEMANTICS.FID
                     || ssrcGroupInfo.semantics === 'FEC-FR') {
                 // secondary streams should be filtered out
                 const secondarySsrc = parseSecondarySSRC(ssrcGroupInfo);
diff --git a/modules/xmpp/JingleHelperFunctions.js b/modules/xmpp/JingleHelperFunctions.js
index ef0790385e..769869d041 100644
--- a/modules/xmpp/JingleHelperFunctions.js
+++ b/modules/xmpp/JingleHelperFunctions.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
 import { $build } from 'strophe.js';
 
 import { MediaType } from '../../service/RTC/MediaType';
+import { SSRC_GROUP_SEMANTICS } from '../../service/RTC/StandardVideoQualitySettings';
 import { XEP } from '../../service/xmpp/XMPPExtensioProtocols';
 
 const logger = getLogger(__filename);
@@ -101,9 +102,9 @@ function _getOrCreateRtpDescription(iq, mediaType) {
  */
 function _getSemantics(str) {
     if (str === 'f') {
-        return 'FID';
+        return SSRC_GROUP_SEMANTICS.FID;
     } else if (str === 's') {
-        return 'SIM';
+        return SSRC_GROUP_SEMANTICS.SIM;
     }
 
     return null;
diff --git a/modules/xmpp/JingleSessionPC.js b/modules/xmpp/JingleSessionPC.js
index eff72edae8..fcf87fb5b7 100644
--- a/modules/xmpp/JingleSessionPC.js
+++ b/modules/xmpp/JingleSessionPC.js
@@ -1,11 +1,13 @@
 import { getLogger } from '@jitsi/logger';
 import $ from 'jquery';
+import { isEqual } from 'lodash-es';
 import { $build, $iq, Strophe } from 'strophe.js';
 
 import { JitsiTrackEvents } from '../../JitsiTrackEvents';
 import { CodecMimeType } from '../../service/RTC/CodecMimeType';
 import { MediaDirection } from '../../service/RTC/MediaDirection';
 import { MediaType } from '../../service/RTC/MediaType';
+import { SSRC_GROUP_SEMANTICS } from '../../service/RTC/StandardVideoQualitySettings';
 import { VideoType } from '../../service/RTC/VideoType';
 import {
     ICE_DURATION,
@@ -370,15 +372,9 @@ export default class JingleSessionPC extends JingleSession {
      */
     _addOrRemoveRemoteStream(isAdd, elem) {
         const logPrefix = isAdd ? 'addRemoteStream' : 'removeRemoteStream';
-
-        if (isAdd) {
-            this.readSsrcInfo(elem);
-        }
-
         const workFunction = finishedCallback => {
-            if (!this.peerconnection.localDescription
-                || !this.peerconnection.localDescription.sdp) {
-                const errMsg = `${logPrefix} - localDescription not ready yet`;
+            if (!this.peerconnection.remoteDescription?.sdp) {
+                const errMsg = `${logPrefix} - received before remoteDescription is set, ignoring!!`;
 
                 logger.error(errMsg);
                 finishedCallback(errMsg);
@@ -388,17 +384,33 @@ export default class JingleSessionPC extends JingleSession {
 
             logger.log(`${this} Processing ${logPrefix}`);
 
-            const sdp = new SDP(this.peerconnection.remoteDescription.sdp);
-            const addOrRemoveSsrcInfo
-                = isAdd
-                    ? this._parseSsrcInfoFromSourceAdd(elem, sdp)
-                    : this._parseSsrcInfoFromSourceRemove(elem, sdp);
-            const newRemoteSdp
-                = isAdd
-                    ? this._processRemoteAddSource(addOrRemoveSsrcInfo)
-                    : this._processRemoteRemoveSource(addOrRemoveSsrcInfo);
-
-            this._renegotiate(newRemoteSdp.raw).then(() => {
+            const currentRemoteSdp = new SDP(this.peerconnection.remoteDescription.sdp, this.isP2P);
+            const sourceDescription = this._processSourceMapFromJingle(elem, isAdd);
+
+            if (!sourceDescription.size) {
+                logger.debug(`${this} ${logPrefix} - no sources to ${isAdd ? 'add' : 'remove'}`);
+                finishedCallback();
+            }
+
+            logger.debug(`${isAdd ? 'adding' : 'removing'} sources=${Array.from(sourceDescription.keys())}`);
+
+            // Update the remote description.
+            const modifiedMids = currentRemoteSdp.updateRemoteSources(sourceDescription, isAdd);
+
+            for (const mid of modifiedMids) {
+                if (this.isP2P) {
+                    const { media } = SDPUtil.parseMLine(currentRemoteSdp.media[mid].split('\r\n')[0]);
+                    const desiredDirection = this.peerconnection.getDesiredMediaDirection(media, true);
+
+                    [ MediaDirection.RECVONLY, MediaDirection.INACTIVE ].forEach(direction => {
+                        currentRemoteSdp.media[mid] = currentRemoteSdp.media[mid]
+                            .replace(`a=${direction}`, `a=${desiredDirection}`);
+                    });
+                    currentRemoteSdp.raw = currentRemoteSdp.session + currentRemoteSdp.media.join('');
+                }
+            }
+
+            this._renegotiate(currentRemoteSdp.raw).then(() => {
                 logger.log(`${this} ${logPrefix} - OK`);
                 finishedCallback();
             }, error => {
@@ -484,163 +496,6 @@ export default class JingleSessionPC extends JingleSession {
         return this.state !== JingleSessionState.ENDED;
     }
 
-    /**
-     * Parse the information from the xml sourceAddElem and translate it into sdp lines.
-     *
-     * @param {jquery xml element} sourceAddElem the source-add element from jingle.
-     * @param {SDP object} currentRemoteSdp the current remote sdp (as of this new source-add).
-     * @returns {list} a list of SDP line strings that should be added to the remote SDP.
-     * @private
-     */
-    _parseSsrcInfoFromSourceAdd(sourceAddElem, currentRemoteSdp) {
-        const addSsrcInfo = [];
-        const self = this;
-
-        $(sourceAddElem).each((i1, content) => {
-            const name = $(content).attr('name');
-            let lines = '';
-
-            $(content)
-                .find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]')
-                .each(function() {
-                    // eslint-disable-next-line no-invalid-this
-                    const semantics = this.getAttribute('semantics');
-                    const ssrcs
-                        = $(this) // eslint-disable-line no-invalid-this
-                            .find('>source')
-                            .map(function() {
-                                // eslint-disable-next-line no-invalid-this
-                                return this.getAttribute('ssrc');
-                            })
-                            .get();
-
-                    if (ssrcs.length) {
-                        lines += `a=ssrc-group:${semantics} ${ssrcs.join(' ')}\r\n`;
-                    }
-                });
-
-            // handles both >source and >description>source
-            const tmp
-                = $(content).find(
-                    'source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
-
-            /* eslint-disable no-invalid-this */
-            tmp.each(function() {
-                const ssrc = $(this).attr('ssrc');
-
-                if (currentRemoteSdp.containsSSRC(ssrc)) {
-
-                    // Do not print the warning for unified plan p2p case since ssrcs are never removed from the SDP.
-                    !self.isP2P && logger.warn(`${self} Source-add request for existing SSRC: ${ssrc}`);
-
-                    return;
-                }
-
-                // eslint-disable-next-line newline-per-chained-call
-                $(this).find('>parameter').each(function() {
-                    lines += `a=ssrc:${ssrc} ${$(this).attr('name')}`;
-                    if ($(this).attr('value') && $(this).attr('value').length) {
-                        lines += `:${$(this).attr('value')}`;
-                    }
-                    lines += '\r\n';
-                });
-            });
-
-            let midFound = false;
-
-            /* eslint-enable no-invalid-this */
-            currentRemoteSdp.media.forEach((media, i2) => {
-                if (!SDPUtil.findLine(media, `a=mid:${name}`)) {
-                    return;
-                }
-                if (!addSsrcInfo[i2]) {
-                    addSsrcInfo[i2] = '';
-                }
-                addSsrcInfo[i2] += lines;
-                midFound = true;
-            });
-
-            // In p2p unified mode with multi-stream enabled, the new sources will have content name that doesn't exist
-            // in the current remote description. Add a new m-line for this newly signaled source.
-            if (!midFound && this.isP2P) {
-                addSsrcInfo[name] = lines;
-            }
-        });
-
-        return addSsrcInfo;
-    }
-
-    /**
-     * Parse the information from the xml sourceRemoveElem and translate it into sdp lines.
-     *
-     * @param {jquery xml element} sourceRemoveElem the source-remove element from jingle.
-     * @param {SDP object} currentRemoteSdp the current remote sdp (as of this new source-remove).
-     * @returns {list} a list of SDP line strings that should be removed from the remote SDP.
-     * @private
-     */
-    _parseSsrcInfoFromSourceRemove(sourceRemoveElem, currentRemoteSdp) {
-        const removeSsrcInfo = [];
-
-        $(sourceRemoveElem).each((i1, content) => {
-            const name = $(content).attr('name');
-            let lines = '';
-
-            $(content)
-                .find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]')
-                .each(function() {
-                    /* eslint-disable no-invalid-this */
-                    const semantics = this.getAttribute('semantics');
-                    const ssrcs
-                        = $(this)
-                            .find('>source')
-                            .map(function() {
-                                return this.getAttribute('ssrc');
-                            })
-                            .get();
-
-                    if (ssrcs.length) {
-                        lines
-                            += `a=ssrc-group:${semantics} ${
-                                ssrcs.join(' ')}\r\n`;
-                    }
-
-                    /* eslint-enable no-invalid-this */
-                });
-            const ssrcs = [];
-
-            // handles both >source and >description>source versions
-            const tmp
-                = $(content).find(
-                    'source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
-
-            tmp.each(function() {
-                // eslint-disable-next-line no-invalid-this
-                const ssrc = $(this).attr('ssrc');
-
-                ssrcs.push(ssrc);
-            });
-            currentRemoteSdp.media.forEach((media, i2) => {
-                if (!SDPUtil.findLine(media, `a=mid:${name}`)) {
-                    return;
-                }
-                if (!removeSsrcInfo[i2]) {
-                    removeSsrcInfo[i2] = '';
-                }
-                ssrcs.forEach(ssrc => {
-                    const ssrcLines
-                        = SDPUtil.findLines(media, `a=ssrc:${ssrc}`);
-
-                    if (ssrcLines.length) {
-                        removeSsrcInfo[i2] += `${ssrcLines.join('\r\n')}\r\n`;
-                    }
-                });
-                removeSsrcInfo[i2] += lines;
-            });
-        });
-
-        return removeSsrcInfo;
-    }
-
     /**
      * Takes in a jingle offer iq, returns the new sdp offer that can be set as remote description in the
      * peerconnection.
@@ -650,7 +505,7 @@ export default class JingleSessionPC extends JingleSession {
      * @private
      */
     _processNewJingleOfferIq(offerIq) {
-        const remoteSdp = new SDP('');
+        const remoteSdp = new SDP('', this.isP2P);
 
         if (this.webrtcIceTcpDisable) {
             remoteSdp.removeTcpCandidates = true;
@@ -663,100 +518,107 @@ export default class JingleSessionPC extends JingleSession {
         }
 
         remoteSdp.fromJingle(offerIq);
-        this.readSsrcInfo($(offerIq).find('>content'));
+        this._processSourceMapFromJingle($(offerIq).find('>content'));
 
         return remoteSdp;
     }
 
     /**
-     * Adds the given ssrc lines to the current remote sdp.
+     * Parses the SSRC information from the source-add/source-remove element passed and updates the SSRC owners.
      *
-     * @param {list} addSsrcInfo a list of SDP line strings that should be added to the remote SDP.
-     * @returns type {SDP Object} the new remote SDP (after removing the lines in removeSsrcInfo.
-     * @private
+     * @param {jquery xml element} sourceElement the source-add/source-remove element from jingle.
+     * @param {boolean} isAdd true if the sources are being added, false if they are to be removed.
+     * @returns {Map<string, Object>} - The map of source name to ssrcs, msid and groups.
      */
-    _processRemoteAddSource(addSsrcInfo) {
-        let remoteSdp = new SDP(this.peerconnection.remoteDescription.sdp);
-
-        // Add a new m-line in the remote description if the source info for a secondary video source is recceived from
-        // the remote p2p peer when multi-stream support is enabled.
-        if (addSsrcInfo.length > remoteSdp.media.length && this.isP2P) {
-            remoteSdp.addMlineForNewLocalSource(MediaType.VIDEO);
-            remoteSdp = new SDP(remoteSdp.raw);
-        }
-        addSsrcInfo.forEach((lines, idx) => {
-            remoteSdp.media[idx] += lines;
+    _processSourceMapFromJingle(sourceElement, isAdd = true) {
+        /**
+         * Map of source name to ssrcs, mediaType, msid and groups.
+         * @type {Map<string,
+         *  {
+         *      mediaType: string,
+         *      msid: string,
+         *      ssrcList: Array<number>,
+         *      groups: {semantics: string, ssrcs: Array<number>}
+         *  }>}
+         */
+        const sourceDescription = new Map();
+        const sourceElementArray = Array.isArray(sourceElement) ? sourceElement : [ sourceElement ];
+
+        for (const content of sourceElementArray) {
+            const descriptionsWithSources = $(content).find('>description')
+                .filter((_, el) => $(el).find('>source').length);
+
+            for (const description of descriptionsWithSources) {
+                const mediaType = $(description).attr('media');
+                const sources = $(description).find('>source');
+                const removeSsrcs = [];
+
+                for (const source of sources) {
+                    const ssrc = $(source).attr('ssrc');
+                    const sourceName = $(source).attr('name');
+                    const msid = $(source)
+                        .find('>parameter[name="msid"]')
+                        .attr('value');
+
+                    if (sourceDescription.has(sourceName)) {
+                        sourceDescription.get(sourceName).ssrcList?.push(ssrc);
+                    } else {
+                        sourceDescription.set(sourceName, {
+                            groups: [],
+                            mediaType,
+                            msid,
+                            ssrcList: [ ssrc ]
+                        });
+                    }
 
-            // Make sure to change the direction to 'sendrecv/sendonly' only for p2p connections. For jvb connections,
-            // a new m-line is added for the new remote sources.
-            if (this.isP2P) {
-                const mediaType = SDPUtil.parseMLine(remoteSdp.media[idx].split('\r\n')[0])?.media;
-                const desiredDirection = this.peerconnection.getDesiredMediaDirection(mediaType, true);
+                    // Update the source owner and source name.
+                    const owner = $(source)
+                        .find('>ssrc-info[xmlns="http://jitsi.org/jitmeet"]')
+                        .attr('owner');
+
+                    if (owner && isAdd) {
+                        // JVB source-add.
+                        this._signalingLayer.setSSRCOwner(Number(ssrc), getEndpointId(owner), sourceName);
+                    } else if (isAdd) {
+                        // P2P source-add.
+                        this._signalingLayer.setSSRCOwner(Number(ssrc),
+                            Strophe.getResourceFromJid(this.remoteJid), sourceName);
+                    } else {
+                        removeSsrcs.push(Number(ssrc));
+                    }
+                }
 
-                [ MediaDirection.RECVONLY, MediaDirection.INACTIVE ].forEach(direction => {
-                    remoteSdp.media[idx] = remoteSdp.media[idx]
-                        .replace(`a=${direction}`, `a=${desiredDirection}`);
-                });
-            }
-        });
-        remoteSdp.raw = remoteSdp.session + remoteSdp.media.join('');
+                // 'source-remove' from remote peer.
+                removeSsrcs.length && this._signalingLayer.removeSSRCOwners(removeSsrcs);
+                const groups = $(description).find('>ssrc-group');
 
-        return remoteSdp;
-    }
+                if (!groups.length) {
+                    continue; // eslint-disable-line no-continue
+                }
 
-    /**
-     * Removes the given ssrc lines from the current remote sdp.
-     *
-     * @param {list} removeSsrcInfo a list of SDP line strings that should be removed from the remote SDP.
-     * @returns type {SDP Object} the new remote SDP (after removing the lines in removeSsrcInfo.
-     * @private
-     */
-    _processRemoteRemoveSource(removeSsrcInfo) {
-        const remoteSdp = new SDP(this.peerconnection.peerconnection.remoteDescription.sdp);
-        let ssrcs;
-
-        removeSsrcInfo.forEach(lines => {
-            // eslint-disable-next-line no-param-reassign
-            lines = lines.split('\r\n');
-            lines.pop(); // remove empty last element;
-            ssrcs = lines.map(line => Number(line.split('a=ssrc:')[1]?.split(' ')[0]));
-
-            let mid;
-
-            lines.forEach(line => {
-                mid = remoteSdp.media.findIndex(mLine => mLine.includes(line));
-                if (mid > -1) {
-                    remoteSdp.media[mid] = remoteSdp.media[mid].replace(`${line}\r\n`, '');
-                    if (this.isP2P) {
-                        const mediaType = SDPUtil.parseMLine(remoteSdp.media[mid].split('\r\n')[0])?.media;
-                        const desiredDirection = this.peerconnection.getDesiredMediaDirection(mediaType, false);
-
-                        [ MediaDirection.SENDRECV, MediaDirection.SENDONLY ].forEach(direction => {
-                            remoteSdp.media[mid] = remoteSdp.media[mid]
-                                .replace(`a=${direction}`, `a=${desiredDirection}`);
-                        });
-                    } else {
-                        // Jvb connections will have direction set to 'sendonly' for the remote sources.
-                        remoteSdp.media[mid] = remoteSdp.media[mid]
-                            .replace(`a=${MediaDirection.SENDONLY}`, `a=${MediaDirection.INACTIVE}`);
+                for (const group of groups) {
+                    const semantics = $(group).attr('semantics');
+                    const groupSsrcs = [];
 
-                        // Reject the m-line so that the browser removes the associated transceiver from the list
-                        // of available transceivers. This will prevent the client from trying to re-use these
-                        // inactive transceivers when additional video sources are added to the peerconnection.
-                        const { media, port } = SDPUtil.parseMLine(remoteSdp.media[mid].split('\r\n')[0]);
+                    for (const source of $(group).find('>source')) {
+                        groupSsrcs.push($(source).attr('ssrc'));
+                    }
 
-                        remoteSdp.media[mid] = remoteSdp.media[mid].replace(`m=${media} ${port}`, `m=${media} 0`);
+                    for (const [ sourceName, { ssrcList } ] of sourceDescription) {
+                        if (isEqual(ssrcList.slice().sort(), groupSsrcs.slice().sort())) {
+                            sourceDescription.get(sourceName).groups.push({
+                                semantics,
+                                ssrcs: groupSsrcs
+                            });
+                        }
                     }
                 }
-            });
-        });
-
-        // Update the ssrc owners list.
-        ssrcs?.length && this._signalingLayer.removeSSRCOwners(ssrcs);
+            }
+        }
 
-        remoteSdp.raw = remoteSdp.session + remoteSdp.media.join('');
+        sourceDescription.size && this.peerconnection.updateRemoteSources(sourceDescription, isAdd);
 
-        return remoteSdp;
+        return sourceDescription;
     }
 
     /**
@@ -1232,7 +1094,7 @@ export default class JingleSessionPC extends JingleSession {
 
         const replaceTracks = [];
         const workFunction = finishedCallback => {
-            const remoteSdp = new SDP(this.peerconnection.peerconnection.remoteDescription.sdp);
+            const remoteSdp = new SDP(this.peerconnection.remoteDescription.sdp, this.isP2P);
             const recvOnlyTransceiver = this.peerconnection.peerconnection.getTransceivers()
                     .find(t => t.receiver.track.kind === MediaType.VIDEO
                         && t.direction === MediaDirection.RECVONLY
@@ -1243,7 +1105,7 @@ export default class JingleSessionPC extends JingleSession {
             // existing one in that case.
             for (const track of localTracks) {
                 if (!this.isP2P || !recvOnlyTransceiver) {
-                    remoteSdp.addMlineForNewLocalSource(track.getType());
+                    remoteSdp.addMlineForNewSource(track.getType());
                 }
             }
 
@@ -1995,6 +1857,14 @@ export default class JingleSessionPC extends JingleSession {
                 logger.debug(`Existing SSRC re-mapped ${ssrc}: new owner=${owner}, source-name=${source}`);
 
                 this._signalingLayer.setSSRCOwner(ssrc, owner, source);
+                const oldSourceName = track.getSourceName();
+                const sourceInfo = this.peerconnection.getRemoteSourceInfoBySourceName(oldSourceName);
+
+                // Update the SSRC map on the peerconnection.
+                if (sourceInfo) {
+                    this.peerconnection.updateRemoteSources(new Map([ [ oldSourceName, sourceInfo ] ]), false);
+                    this.peerconnection.updateRemoteSources(new Map([ [ source, sourceInfo ] ]), true /* isAdd */);
+                }
 
                 // Update the muted state and the video type on the track since the presence for this track could have
                 // been received before the updated source map is received on the bridge channel.
@@ -2028,7 +1898,7 @@ export default class JingleSessionPC extends JingleSession {
                         _addSourceElement(node, src, rtx, msid);
                         node.c('ssrc-group', {
                             xmlns: XEP.SOURCE_ATTRIBUTES,
-                            semantics: 'FID'
+                            semantics: SSRC_GROUP_SEMANTICS.FID
                         })
                             .c('source', {
                                 xmlns: XEP.SOURCE_ATTRIBUTES,
@@ -2055,45 +1925,6 @@ export default class JingleSessionPC extends JingleSession {
         }
     }
 
-    /**
-     * Processes the Jingle message received from the peer and updates the SSRC owners for all the sources signaled
-     * in the Jingle message.
-     *
-     * @param {Element} contents - The content element of the jingle message.
-     * @returns {void}
-     */
-    readSsrcInfo(contents) {
-        const ssrcs = $(contents).find('>description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
-
-        ssrcs.each((i, ssrcElement) => {
-            const ssrc = Number(ssrcElement.getAttribute('ssrc'));
-            let sourceName;
-
-            if (ssrcElement.hasAttribute('name')) {
-                sourceName = ssrcElement.getAttribute('name');
-            }
-
-            if (this.isP2P) {
-                // In P2P all SSRCs are owner by the remote peer
-                this._signalingLayer.setSSRCOwner(ssrc, Strophe.getResourceFromJid(this.remoteJid), sourceName);
-            } else {
-                $(ssrcElement)
-                    .find('>ssrc-info[xmlns="http://jitsi.org/jitmeet"]')
-                    .each((i3, ssrcInfoElement) => {
-                        const owner = ssrcInfoElement.getAttribute('owner');
-
-                        if (owner?.length) {
-                            if (isNaN(ssrc) || ssrc < 0) {
-                                logger.warn(`${this} Invalid SSRC ${ssrc} value received for ${owner}`);
-                            } else {
-                                this._signalingLayer.setSSRCOwner(ssrc, getEndpointId(owner), sourceName);
-                            }
-                        }
-                    });
-            }
-        });
-    }
-
     /**
      * Handles a Jingle source-remove message for this Jingle session.
      *
@@ -2114,8 +1945,12 @@ export default class JingleSessionPC extends JingleSession {
         const workFunction = finishCallback => {
             const removeSsrcInfo = this.peerconnection.getRemoteSourceInfoByParticipant(id);
 
-            if (removeSsrcInfo.length) {
-                const newRemoteSdp = this._processRemoteRemoveSource(removeSsrcInfo);
+            if (removeSsrcInfo.size) {
+                logger.debug(`${this} Removing SSRCs for user ${id}, sources=${Array.from(removeSsrcInfo.keys())}`);
+                const newRemoteSdp = new SDP(this.peerconnection.remoteDescription.sdp, this.isP2P);
+
+                newRemoteSdp.updateRemoteSources(removeSsrcInfo, false /* isAdd */);
+                this.peerconnection.updateRemoteSources(removeSsrcInfo, false /* isAdd */);
 
                 this._renegotiate(newRemoteSdp.raw)
                     .then(() => finishCallback(), error => finishCallback(error));
diff --git a/modules/xmpp/JingleSessionPC.spec.js b/modules/xmpp/JingleSessionPC.spec.js
index 595f456dae..13f8a43abc 100644
--- a/modules/xmpp/JingleSessionPC.spec.js
+++ b/modules/xmpp/JingleSessionPC.spec.js
@@ -1,7 +1,6 @@
 import $ from 'jquery';
 
 import { MockRTC } from '../RTC/MockClasses';
-import FeatureFlags from '../flags/FeatureFlags';
 
 import JingleSessionPC from './JingleSessionPC';
 import * as JingleSessionState from './JingleSessionState';
@@ -56,7 +55,10 @@ describe('JingleSessionPC', () => {
         jingleSession.initialize(
             /* ChatRoom */ new MockChatRoom(),
             /* RTC */ rtc,
-            /* Signaling layer */ { },
+            /* Signaling layer */ {
+                setSSRCOwner: () => { }, // eslint-disable-line no-empty-function,
+                removeSSRCOwners: () => { } // eslint-disable-line no-empty-function
+            },
             /* options */ { });
 
         // eslint-disable-next-line no-empty-function
@@ -64,10 +66,6 @@ describe('JingleSessionPC', () => {
     });
 
     describe('send/receive video constraints w/ source-name', () => {
-        beforeEach(() => {
-            FeatureFlags.init({ });
-        });
-
         it('sends content-modify with recv frame size', () => {
             const sendIQSpy = spyOn(connection, 'sendIQ').and.callThrough();
             const sourceConstraints = new Map();
@@ -130,4 +128,166 @@ describe('JingleSessionPC', () => {
             });
         });
     });
+
+    describe('_processSourceAddOrRemove', () => {
+        let peerconnection, removeSsrcOwnersSpy, setSsrcOwnerSpy, sourceInfo, updateRemoteSourcesSpy;
+
+        beforeEach(() => {
+            peerconnection = jingleSession.peerconnection;
+            setSsrcOwnerSpy = spyOn(jingleSession._signalingLayer, 'setSSRCOwner');
+            removeSsrcOwnersSpy = spyOn(jingleSession._signalingLayer, 'removeSSRCOwners');
+            updateRemoteSourcesSpy = spyOn(peerconnection, 'updateRemoteSources');
+        });
+        it('should handle no sources', () => {
+            const jingle = $.parseXML(
+                    `<jingle xmlns='urn:xmpp:jingle:1'>
+                        <content name='audio'>
+                            <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>
+                        </content>
+                        <content name='video'>
+                            <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='video'/>
+                        </content>
+                    </jingle>`
+            );
+            const sourceAddElem = $(jingle).find('>jingle>content');
+
+            sourceInfo = jingleSession._processSourceMapFromJingle(sourceAddElem, true);
+
+            expect(sourceInfo.size).toBe(0);
+            expect(setSsrcOwnerSpy).not.toHaveBeenCalled();
+            expect(removeSsrcOwnersSpy).not.toHaveBeenCalled();
+            expect(updateRemoteSourcesSpy).not.toHaveBeenCalled();
+        });
+
+        it('should handle a single source', () => {
+            const jingle = $.parseXML(
+                    `<jingle xmlns='urn:xmpp:jingle:1'>
+                        <content name='audio'>
+                            <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>
+                                <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='1234' name='source1' owner='peer'>
+                                    <parameter name='msid' value='stream1'/>
+                                </source>
+                            </description>
+                        </content>
+                    </jingle>`
+            );
+            const sourceAddElem = $(jingle).find('>jingle>content');
+
+            sourceInfo = jingleSession._processSourceMapFromJingle(sourceAddElem, true);
+            expect(sourceInfo.size).toBe(1);
+            expect(sourceInfo.get('source1').ssrcList).toEqual([ '1234' ]);
+            expect(sourceInfo.get('source1').msid).toBe('stream1');
+            expect(setSsrcOwnerSpy).toHaveBeenCalledWith(1234, null, 'source1');
+            expect(updateRemoteSourcesSpy).toHaveBeenCalledWith(sourceInfo, true);
+
+            sourceInfo = jingleSession._processSourceMapFromJingle(sourceAddElem, false);
+
+            expect(removeSsrcOwnersSpy).toHaveBeenCalledWith([ 1234 ]);
+            expect(updateRemoteSourcesSpy).toHaveBeenCalledWith(sourceInfo, false);
+        });
+
+        it('should handle multiple ssrcs belonging to the same source', () => {
+            const jingle = $.parseXML(
+                    `<jingle xmlns='urn:xmpp:jingle:1'>
+                        <content name='audio'>
+                                <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>
+                        </content>
+                        <content name='video'>
+                            <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='video'>
+                                <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='1234' name='source1' owner='peer'>
+                                    <parameter name='msid' value='stream1'/>
+                                </source>
+                                <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='5678' name='source1' owner='peer'>
+                                    <parameter name='msid' value='stream1'/>
+                                </source>
+                                <ssrc-group xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' semantics='FID'>
+                                    <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='1234'/>
+                                    <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='5678'/>
+                                </ssrc-group>
+                            </description>
+                        </content>
+                    </jingle>`
+            );
+            const sourceAddElem = $(jingle).find('>jingle>content');
+
+            sourceInfo = jingleSession._processSourceMapFromJingle(sourceAddElem, true);
+
+            expect(sourceInfo.size).toBe(1);
+            expect(sourceInfo.get('source1').ssrcList).toEqual([ '1234', '5678' ]);
+            expect(sourceInfo.get('source1').msid).toBe('stream1');
+            expect(sourceInfo.get('source1').mediaType).toBe('video');
+            expect(sourceInfo.get('source1').groups).toEqual([ {
+                semantics: 'FID',
+                ssrcs: [ '1234', '5678' ] } ]);
+            expect(setSsrcOwnerSpy).toHaveBeenCalledWith(1234, null, 'source1');
+            expect(setSsrcOwnerSpy).toHaveBeenCalledWith(5678, null, 'source1');
+            expect(updateRemoteSourcesSpy).toHaveBeenCalledWith(sourceInfo, true);
+
+            sourceInfo = jingleSession._processSourceMapFromJingle(sourceAddElem, false);
+
+            expect(removeSsrcOwnersSpy).toHaveBeenCalledWith([ 1234, 5678 ]);
+            expect(updateRemoteSourcesSpy).toHaveBeenCalledWith(sourceInfo, false);
+        });
+
+        it('should handle multiple ssrcs belonging to different sources', () => {
+            const jingle = $.parseXML(
+                    `<jingle xmlns='urn:xmpp:jingle:1'>
+                        <content name='audio'>
+                                <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>
+                        </content>
+                        <content name='video'>
+                            <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='video'>
+                                <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='1234' name='source1' owner='peer'>
+                                    <parameter name='msid' value='stream1'/>
+                                </source>
+                                <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='5678' name='source1' owner='peer'>
+                                    <parameter name='msid' value='stream1'/>
+                                </source>
+                                <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='4321' name='source2' owner='peer'>
+                                    <parameter name='msid' value='stream2'/>
+                                </source>
+                                <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='8765' name='source2' owner='peer'>
+                                    <parameter name='msid' value='stream2'/>
+                                </source>
+                                <ssrc-group xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' semantics='FID'>
+                                    <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='1234'/>
+                                    <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='5678'/>
+                                </ssrc-group>
+                                <ssrc-group xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' semantics='FID'>
+                                    <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='4321'/>
+                                    <source xmlns='urn:xmpp:jingle:apps:rtp:ssma:0' ssrc='8765'/>
+                                </ssrc-group>
+                            </description>
+                        </content>
+                    </jingle>`
+            );
+            const sourceAddElem = $(jingle).find('>jingle>content');
+
+            sourceInfo = jingleSession._processSourceMapFromJingle(sourceAddElem, true);
+
+            expect(sourceInfo.size).toBe(2);
+            expect(sourceInfo.get('source1').ssrcList).toEqual([ '1234', '5678' ]);
+            expect(sourceInfo.get('source1').msid).toBe('stream1');
+            expect(sourceInfo.get('source1').groups).toEqual([ {
+                semantics: 'FID',
+                ssrcs: [ '1234', '5678' ] } ]);
+            expect(sourceInfo.get('source1').mediaType).toBe('video');
+            expect(sourceInfo.get('source2').ssrcList).toEqual([ '4321', '8765' ]);
+            expect(sourceInfo.get('source2').msid).toBe('stream2');
+            expect(sourceInfo.get('source2').groups).toEqual([ {
+                semantics: 'FID',
+                ssrcs: [ '4321', '8765' ] } ]);
+            expect(sourceInfo.get('source2').mediaType).toBe('video');
+            expect(setSsrcOwnerSpy).toHaveBeenCalledWith(1234, null, 'source1');
+            expect(setSsrcOwnerSpy).toHaveBeenCalledWith(5678, null, 'source1');
+            expect(setSsrcOwnerSpy).toHaveBeenCalledWith(4321, null, 'source2');
+            expect(setSsrcOwnerSpy).toHaveBeenCalledWith(8765, null, 'source2');
+            expect(updateRemoteSourcesSpy).toHaveBeenCalledWith(sourceInfo, true);
+
+            sourceInfo = jingleSession._processSourceMapFromJingle(sourceAddElem, false);
+
+            expect(removeSsrcOwnersSpy).toHaveBeenCalledWith([ 1234, 5678, 4321, 8765 ]);
+            expect(updateRemoteSourcesSpy).toHaveBeenCalledWith(sourceInfo, false);
+        });
+    });
 });
diff --git a/package-lock.json b/package-lock.json
index 62276958cd..8fc66cb534 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,6 @@
         "@jitsi/logger": "2.0.2",
         "@jitsi/precall-test": "1.0.6",
         "@jitsi/rtcstats": "9.7.0",
-        "@jitsi/sdp-interop": "git+https://github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
         "@testrtc/watchrtc-sdk": "1.38.2",
         "async-es": "3.2.4",
         "base64-js": "1.3.1",
@@ -2048,24 +2047,6 @@
         "uuid": "dist/bin/uuid"
       }
     },
-    "node_modules/@jitsi/sdp-interop": {
-      "version": "1.0.5",
-      "resolved": "git+https://git@github.com/jitsi/sdp-interop.git#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
-      "integrity": "sha512-80u69QNTBArnCd1CGbTTrl/8AsZOOMF82dQhrgXBQAnrimdpomX1fMZ82ZkxyWyYvRMPG167u43Tp8y1g2DLNA==",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "lodash.clonedeep": "4.5.0",
-        "sdp-transform": "2.14.1"
-      }
-    },
-    "node_modules/@jitsi/sdp-interop/node_modules/sdp-transform": {
-      "version": "2.14.1",
-      "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
-      "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==",
-      "bin": {
-        "sdp-verify": "checker.js"
-      }
-    },
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -4952,11 +4933,6 @@
       "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
       "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
     },
-    "node_modules/lodash.clonedeep": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
-      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
-    },
     "node_modules/lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -8714,22 +8690,6 @@
         }
       }
     },
-    "@jitsi/sdp-interop": {
-      "version": "git+https://git@github.com/jitsi/sdp-interop.git#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
-      "integrity": "sha512-80u69QNTBArnCd1CGbTTrl/8AsZOOMF82dQhrgXBQAnrimdpomX1fMZ82ZkxyWyYvRMPG167u43Tp8y1g2DLNA==",
-      "from": "@jitsi/sdp-interop@git+https://github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
-      "requires": {
-        "lodash.clonedeep": "4.5.0",
-        "sdp-transform": "2.14.1"
-      },
-      "dependencies": {
-        "sdp-transform": {
-          "version": "2.14.1",
-          "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
-          "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw=="
-        }
-      }
-    },
     "@jridgewell/gen-mapping": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -10961,11 +10921,6 @@
       "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
       "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
     },
-    "lodash.clonedeep": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
-      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
-    },
     "lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
diff --git a/package.json b/package.json
index 47c36a0dc3..81ec7e3552 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,6 @@
     "@jitsi/logger": "2.0.2",
     "@jitsi/precall-test": "1.0.6",
     "@jitsi/rtcstats": "9.7.0",
-    "@jitsi/sdp-interop": "git+https://github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
     "@testrtc/watchrtc-sdk": "1.38.2",
     "async-es": "3.2.4",
     "base64-js": "1.3.1",
diff --git a/service/RTC/StandardVideoQualitySettings.ts b/service/RTC/StandardVideoQualitySettings.ts
index db332d6faf..93649ecf37 100644
--- a/service/RTC/StandardVideoQualitySettings.ts
+++ b/service/RTC/StandardVideoQualitySettings.ts
@@ -29,6 +29,17 @@ export const SIM_LAYERS = [
     }
 ];
 
+/**
+ * The ssrc-group semantics for SSRCs related to the video streams.
+ */
+export enum SSRC_GROUP_SEMANTICS {
+    // The semantics for group of SSRCs belonging to the same stream, primary and RTX.
+    FID = 'FID',
+
+    // The semantics for group with primary SSRCs for each of the simulcast streams.
+    SIM = 'SIM'
+}
+
 /**
  * Standard scalability mode settings for different video codecs and the default bitrates.
  */
diff --git a/types/hand-crafted/modules/RTC/TraceablePeerConnection.d.ts b/types/hand-crafted/modules/RTC/TraceablePeerConnection.d.ts
index fd714a9d71..61835b80bc 100644
--- a/types/hand-crafted/modules/RTC/TraceablePeerConnection.d.ts
+++ b/types/hand-crafted/modules/RTC/TraceablePeerConnection.d.ts
@@ -75,7 +75,7 @@ export default class TraceablePeerConnection {
   getLocalVideoTrack: () => JitsiLocalTrack | undefined;
   hasAnyTracksOfType: ( mediaType: MediaType ) => boolean;
   getRemoteTracks: ( endpointId: string, mediaType: MediaType ) => JitsiRemoteTrack[];
-  getRemoteSourceInfoByParticipant: ( id: string ) => string[]; // TODO:
+  getRemoteSourceInfoByParticipant: ( id: string ) => Map<string, TPCSourceInfo>; // TODO:
   getTargetVideoBitrates: () => unknown; // TODO:
   getTrackBySSRC: ( ssrc: number ) => JitsiTrack | null;
   getSsrcByTrackId: ( id: string ) => number | null;
diff --git a/types/hand-crafted/modules/sdp/SDP.d.ts b/types/hand-crafted/modules/sdp/SDP.d.ts
index f3c29d2563..1748b4250f 100644
--- a/types/hand-crafted/modules/sdp/SDP.d.ts
+++ b/types/hand-crafted/modules/sdp/SDP.d.ts
@@ -6,7 +6,6 @@ export default class SDP {
   removeTcpCandidates: boolean;
   removeUdpCandidates: boolean;
   getMediaSsrcMap: () => unknown; // TODO:
-  containsSSRC: ( ssrc: unknown ) => boolean; // TODO:
   toJingle: ( elem: unknown, thecreator: unknown ) => unknown; // TODO:
   transportToJingle: ( mediaindex: unknown, elem: unknown ) => unknown; // TODO:
   rtcpFbToJingle: ( mediaindex: unknown, elem: unknown, payloadtype: unknown ) => unknown; // TODO:
diff --git a/types/hand-crafted/modules/xmpp/JingleSessionPC.d.ts b/types/hand-crafted/modules/xmpp/JingleSessionPC.d.ts
index dc113a2460..ded7272718 100644
--- a/types/hand-crafted/modules/xmpp/JingleSessionPC.d.ts
+++ b/types/hand-crafted/modules/xmpp/JingleSessionPC.d.ts
@@ -11,7 +11,6 @@ export default class JingleSessionPC extends JingleSession {
   sendIceCandidate: ( candidate: RTCIceCandidate ) => void;
   sendIceCandidates: ( candidates: RTCIceCandidate[] ) => void;
   addIceCandidates: ( elem: unknown ) => void; // TODO:
-  readSsrcInfo: ( contents: unknown ) => void; // TODO:
   getConfiguredVideoCodec: () => CodecMimeType;
   acceptOffer: ( jingleOffer: JQuery, success: ( params: unknown ) => unknown, failure: ( params: unknown ) => unknown, localTracks?: JitsiLocalTrack[] ) => void; // TODO:
   invite: ( localTracks?: JitsiLocalTrack[] ) => void;