From a7476b126c89e1ccce9a2ed6e8e2f656d8dce978 Mon Sep 17 00:00:00 2001 From: Jaya Allamsetty <54324652+jallamsetty1@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:37:55 -0400 Subject: [PATCH] feat(SDP) Convert SDP->Jingle directly w/o sdp-interop layer. * ref(SDPDiffer) Convert to ES6 class. Make it work directly with unified plan SDP that has multiple m-lines and add more unit tests. * ref(xmpp) translate unified-plan SDP->Jingle directly. Without having to run it through the SDPInterop.toPlanB cycle. * ref(SDP) Always generate the MSID for signaling it to Jicofo. * fix(SDPDiffer) Check explicitly for ssrc changes * fix(SDP): Fix comments and cleanup. Remove LOCAL_TRACK_SSRC_UPDATED event as the application ignores the event and no additional action needs to be taken when that event is fired. * ref(SDP) Add a helper function for parsing the 'a=ssrc-group' line. * squash: Address review comments --- modules/RTC/TPCUtils.js | 13 +- modules/RTC/TraceablePeerConnection.js | 263 ++---- modules/sdp/LocalSdpMunger.js | 236 ++--- modules/sdp/LocalSdpMunger.spec.js | 58 +- modules/sdp/SDP.js | 121 ++- modules/sdp/SDP.spec.js | 873 +++++++++++++++++- modules/sdp/SDPDiffer.js | 287 ++---- modules/sdp/SDPDiffer.spec.js | 328 ++++++- modules/sdp/SDPUtil.js | 15 + modules/sdp/SampleSdpStrings.js | 115 ++- modules/xmpp/JingleSessionPC.js | 12 +- service/RTC/RTCEvents.spec.ts | 4 - service/RTC/RTCEvents.ts | 9 - types/hand-crafted/service/RTC/RTCEvents.d.ts | 1 - 14 files changed, 1706 insertions(+), 629 deletions(-) diff --git a/modules/RTC/TPCUtils.js b/modules/RTC/TPCUtils.js index 90aa2a6a14..ada4e85a23 100644 --- a/modules/RTC/TPCUtils.js +++ b/modules/RTC/TPCUtils.js @@ -360,12 +360,14 @@ export class TPCUtils { /** * Adds {@link JitsiLocalTrack} to the WebRTC peerconnection for the first time. + * * @param {JitsiLocalTrack} track - track to be added to the peerconnection. * @param {boolean} isInitiator - boolean that indicates if the endpoint is offerer in a p2p connection. - * @returns {void} + * @returns {RTCRtpTransceiver} - the transceiver that the track was added to. */ addTrack(localTrack, isInitiator) { const track = localTrack.getTrack(); + let transceiver; if (isInitiator) { const streams = []; @@ -385,13 +387,18 @@ export class TPCUtils { if (!browser.isFirefox()) { transceiverInit.sendEncodings = this._getStreamEncodings(localTrack); } - this.pc.peerconnection.addTransceiver(track, transceiverInit); + transceiver = this.pc.peerconnection.addTransceiver(track, transceiverInit); } else { // Use pc.addTrack() for responder case so that we can re-use the m-lines that were created // when setRemoteDescription was called. pc.addTrack() automatically attaches to any existing // unused "recv-only" transceiver. - this.pc.peerconnection.addTrack(track); + const sender = this.pc.peerconnection.addTrack(track); + + // Find the corresponding transceiver that the track was attached to. + transceiver = this.pc.peerconnection.getTransceivers().find(t => t.sender === sender); } + + return transceiver; } /** diff --git a/modules/RTC/TraceablePeerConnection.js b/modules/RTC/TraceablePeerConnection.js index 236d4db699..a5d861d91f 100644 --- a/modules/RTC/TraceablePeerConnection.js +++ b/modules/RTC/TraceablePeerConnection.js @@ -327,6 +327,13 @@ export default function TraceablePeerConnection( */ this._localTrackTransceiverMids = new Map(); + /** + * Holds the SSRC map for the local tracks. + * + * @type {Map} + */ + this._localSsrcMap = null; + // override as desired this.trace = (what, info) => { logger.trace(what, info); @@ -1123,117 +1130,81 @@ TraceablePeerConnection.prototype._removeRemoteTrack = function(toBeRemoved) { }; /** - * Returns a map with keys msid/mediaType and TrackSSRCInfo values. - * @param {RTCSessionDescription} desc the local description. - * @return {Map} + * Processes the local SDP and creates an SSRC map for every local track. + * + * @param {string} localSDP - SDP from the local description. + * @returns {void} */ -TraceablePeerConnection.prototype._extractSSRCMap = function(desc) { - /** - * Track SSRC infos mapped by stream ID (msid) or mediaType (unified-plan) - * @type {Map} - */ +TraceablePeerConnection.prototype._processAndExtractSourceInfo = function(localSDP) { const ssrcMap = new Map(); - /** - * Groups mapped by primary SSRC number - * @type {Map>} - */ - const groupsMap = new Map(); - - if (typeof desc !== 'object' || desc === null - || typeof desc.sdp !== 'string') { - logger.warn('An empty description was passed as an argument'); - - return ssrcMap; + if (!localSDP || typeof localSDP !== 'string') { + throw new Error('Local SDP must be a valid string, aborting!!'); } + const session = transform.parse(localSDP); + const media = session.media.filter(mline => mline.direction === MediaDirection.SENDONLY + || mline.direction === MediaDirection.SENDRECV); - const session = transform.parse(desc.sdp); + if (!Array.isArray(media)) { + this._localSsrcMap = ssrcMap; - if (!Array.isArray(session.media)) { - return ssrcMap; + return; } - let media = session.media; - - media = media.filter(mline => mline.direction === MediaDirection.SENDONLY - || mline.direction === MediaDirection.SENDRECV); + for (const localTrack of this.localTracks.values()) { + const sourceName = localTrack.getSourceName(); + const trackIndex = getSourceIndexFromSourceName(sourceName); + const mediaType = localTrack.getType(); + const mLines = media.filter(m => m.type === mediaType); + const ssrcGroups = mLines[trackIndex].ssrcGroups; + let ssrcs = mLines[trackIndex].ssrcs; + + if (ssrcs?.length) { + // Filter the ssrcs with 'cname' attribute. + ssrcs = ssrcs.filter(s => s.attribute === 'cname'); + + const msid = `${this.rtc.getLocalEndpointId()}-${mediaType}-${trackIndex}`; + const ssrcInfo = { + ssrcs: [], + groups: [], + msid + }; - let index = 0; + ssrcs.forEach(ssrc => ssrcInfo.ssrcs.push(ssrc.id)); - for (const mLine of media) { - if (!Array.isArray(mLine.ssrcs)) { - continue; // eslint-disable-line no-continue - } + if (ssrcGroups?.length) { + for (const group of ssrcGroups) { + group.ssrcs = group.ssrcs.split(' ').map(ssrcStr => parseInt(ssrcStr, 10)); + ssrcInfo.groups.push(group); + } - if (Array.isArray(mLine.ssrcGroups)) { - for (const group of mLine.ssrcGroups) { - if (typeof group.semantics !== 'undefined' && typeof group.ssrcs !== 'undefined') { - // Parse SSRCs and store as numbers - const groupSSRCs = group.ssrcs.split(' ').map(ssrcStr => parseInt(ssrcStr, 10)); - const primarySSRC = groupSSRCs[0]; + const simGroup = ssrcGroups.find(group => group.semantics === 'SIM'); - // Note that group.semantics is already present - group.ssrcs = groupSSRCs; + // 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]); - // eslint-disable-next-line max-depth - if (!groupsMap.has(primarySSRC)) { - groupsMap.set(primarySSRC, []); - } - groupsMap.get(primarySSRC).push(group); + ssrcInfo.groups.push({ + semantics: 'SIM', + ssrcs: groupSsrcs + }); } } - const simGroup = mLine.ssrcGroups.find(group => group.semantics === 'SIM'); + ssrcMap.set(sourceName, ssrcInfo); - // Add a SIM group if its missing in the description (happens on Firefox). - if (!simGroup) { - const groupSsrcs = mLine.ssrcGroups.map(group => group.ssrcs[0]); + const oldSsrcInfo = this.localSSRCs.get(localTrack.rtcId); + const oldSsrc = this._extractPrimarySSRC(oldSsrcInfo); + const newSsrc = this._extractPrimarySSRC(ssrcInfo); - groupsMap.get(groupSsrcs[0]).push({ - semantics: 'SIM', - ssrcs: groupSsrcs - }); + if (oldSsrc !== newSsrc) { + oldSsrc && logger.debug(`${this} Overwriting SSRC for track=${localTrack}] with ssrc=${newSsrc}`); + this.localSSRCs.set(localTrack.rtcId, ssrcInfo); + localTrack.setSsrc(newSsrc); } } - - let ssrcs = mLine.ssrcs; - - // Filter the ssrcs with 'cname' attribute. - ssrcs = ssrcs.filter(s => s.attribute === 'cname'); - - for (const ssrc of ssrcs) { - // Use the mediaType as key for the source map for unified plan clients since msids are not part of - // the standard and the unified plan SDPs do not have a proper msid attribute for the sources. - // Also the ssrcs for sources do not change for Unified plan clients since RTCRtpSender#replaceTrack is - // used for switching the tracks so it is safe to use the mediaType as the key for the TrackSSRCInfo map. - const key = `${mLine.type}-${index}`; - const ssrcNumber = ssrc.id; - let ssrcInfo = ssrcMap.get(key); - - if (!ssrcInfo) { - ssrcInfo = { - ssrcs: [], - groups: [], - msid: key - }; - ssrcMap.set(key, ssrcInfo); - } - ssrcInfo.ssrcs.push(ssrcNumber); - - if (groupsMap.has(ssrcNumber)) { - const ssrcGroups = groupsMap.get(ssrcNumber); - - for (const group of ssrcGroups) { - ssrcInfo.groups.push(group); - } - } - } - - // Currently multi-stream is supported for video only. - mLine.type === MediaType.VIDEO && index++; } - - return ssrcMap; + this._localSsrcMap = ssrcMap; }; /** @@ -1322,15 +1293,12 @@ const getters = { // For a jvb connection, transform the SDP to Plan B first. if (!this.isP2P) { - desc = this.interop.toPlanB(desc); - this.trace('getLocalDescription::postTransform (Plan B)', dumpSDP(desc)); - desc = this._injectSsrcGroupForUnifiedSimulcast(desc); this.trace('getLocalDescription::postTransform (inject ssrc group)', dumpSDP(desc)); } // See the method's doc for more info about this transformation. - desc = this.localSdpMunger.transformStreamIdentifiers(desc); + desc = this.localSdpMunger.transformStreamIdentifiers(desc, this._localSsrcMap); return desc; }, @@ -1505,6 +1473,7 @@ TraceablePeerConnection.prototype._updateAv1DdHeaders = function(description) { */ TraceablePeerConnection.prototype.addTrack = function(track, isInitiator = false) { const rtcId = track.rtcId; + let transceiver; logger.info(`${this} adding ${track}`); if (this.localTracks.has(rtcId)) { @@ -1516,20 +1485,25 @@ TraceablePeerConnection.prototype.addTrack = function(track, isInitiator = false const webrtcStream = track.getOriginalStream(); try { - this.tpcUtils.addTrack(track, isInitiator); - if (track) { - if (track.isAudioTrack()) { - this._hasHadAudioTrack = true; - } else { - this._hasHadVideoTrack = true; - } - } + transceiver = this.tpcUtils.addTrack(track, isInitiator); } catch (error) { logger.error(`${this} Adding track=${track} failed: ${error?.message}`); return Promise.reject(error); } + if (transceiver?.mid) { + this._localTrackTransceiverMids.set(track.rtcId, transceiver.mid.toString()); + } + + if (track) { + if (track.isAudioTrack()) { + this._hasHadAudioTrack = true; + } else { + this._hasHadVideoTrack = true; + } + } + let promiseChain = Promise.resolve(); // On Firefox, the encodings have to be configured on the sender only after the transceiver is created. @@ -1643,7 +1617,7 @@ TraceablePeerConnection.prototype.getConfiguredVideoCodec = function(localTrack) * @returns {Array} */ TraceablePeerConnection.prototype.getConfiguredVideoCodecs = function(description) { - const currentSdp = description?.sdp ?? this.peerconnection.localDescription?.sdp; + const currentSdp = description?.sdp ?? this.localDescription?.sdp; if (!currentSdp) { return []; @@ -1746,7 +1720,7 @@ TraceablePeerConnection.prototype.findSenderForTrack = function(track) { * @returns {void} */ TraceablePeerConnection.prototype.processLocalSdpForTransceiverInfo = function(localTracks) { - const localSdp = this.peerconnection.localDescription?.sdp; + const localSdp = this.localDescription?.sdp; if (!localSdp) { return; @@ -1817,23 +1791,21 @@ TraceablePeerConnection.prototype.replaceTrack = function(oldTrack, newTrack) { } } - if (transceiver) { - // In the scenario where we remove the oldTrack (oldTrack is not null and newTrack is null) on FF - // if we change the direction to RECVONLY, create answer will generate SDP with only 1 receive - // only ssrc instead of keeping all 6 ssrcs that we currently have. Stopping the screen sharing - // and then starting it again will trigger 2 rounds of source-remove and source-add replacing - // the 6 ssrcs for the screen sharing with 1 receive only ssrc and then removing the receive - // only ssrc and adding the same 6 ssrcs. On the remote participant's side the same ssrcs will - // be reused on a new m-line and if the remote participant is FF due to - // https://bugzilla.mozilla.org/show_bug.cgi?id=1768729 the video stream won't be rendered. - // That's why we need keep the direction to SENDRECV for FF. - // - // NOTE: If we return back to the approach of not removing the track for FF and instead using the - // enabled property for mute or stopping screensharing we may need to change the direction to - // RECVONLY if FF still sends the media even though the enabled flag is set to false. - transceiver.direction - = newTrack || browser.isFirefox() ? MediaDirection.SENDRECV : MediaDirection.RECVONLY; - } + // In the scenario where we remove the oldTrack (oldTrack is not null and newTrack is null) on FF + // if we change the direction to RECVONLY, create answer will generate SDP with only 1 receive + // only ssrc instead of keeping all 6 ssrcs that we currently have. Stopping the screen sharing + // and then starting it again will trigger 2 rounds of source-remove and source-add replacing + // the 6 ssrcs for the screen sharing with 1 receive only ssrc and then removing the receive + // only ssrc and adding the same 6 ssrcs. On the remote participant's side the same ssrcs will + // be reused on a new m-line and if the remote participant is FF due to + // https://bugzilla.mozilla.org/show_bug.cgi?id=1768729 the video stream won't be rendered. + // That's why we need keep the direction to SENDRECV for FF. + // + // NOTE: If we return back to the approach of not removing the track for FF and instead using the + // enabled property for mute or stopping screensharing we may need to change the direction to + // RECVONLY if FF still sends the media even though the enabled flag is set to false. + transceiver.direction + = newTrack || browser.isFirefox() ? MediaDirection.SENDRECV : MediaDirection.RECVONLY; // Avoid re-configuring the encodings on Chromium/Safari, this is needed only on Firefox. const configureEncodingsPromise @@ -2599,9 +2571,7 @@ TraceablePeerConnection.prototype.createOffer = function(constraints) { return this._createOfferOrAnswer(true /* offer */, constraints); }; -TraceablePeerConnection.prototype._createOfferOrAnswer = function( - isOffer, - constraints) { +TraceablePeerConnection.prototype._createOfferOrAnswer = function(isOffer, constraints) { const logName = isOffer ? 'Offer' : 'Answer'; this.trace(`create${logName}`, JSON.stringify(constraints, null, ' ')); @@ -2631,9 +2601,7 @@ TraceablePeerConnection.prototype._createOfferOrAnswer = function( dumpSDP(resultSdp)); } - const ssrcMap = this._extractSSRCMap(resultSdp); - - this._processLocalSSRCsMap(ssrcMap); + this._processAndExtractSourceInfo(resultSdp.sdp); resolveFn(resultSdp); } catch (e) { @@ -2720,47 +2688,6 @@ TraceablePeerConnection.prototype._extractPrimarySSRC = function(ssrcObj) { return null; }; -/** - * Goes over the SSRC map extracted from the latest local description and tries - * to match them with the local tracks (by MSID). Will update the values - * currently stored in the {@link TraceablePeerConnection.localSSRCs} map. - * @param {Map} ssrcMap - * @private - */ -TraceablePeerConnection.prototype._processLocalSSRCsMap = function(ssrcMap) { - for (const track of this.localTracks.values()) { - const sourceName = track.getSourceName(); - const sourceIndex = getSourceIndexFromSourceName(sourceName); - const sourceIdentifier = `${track.getType()}-${sourceIndex}`; - - if (ssrcMap.has(sourceIdentifier)) { - const newSSRC = ssrcMap.get(sourceIdentifier); - - if (!newSSRC) { - logger.error(`${this} No SSRC found for stream=${sourceIdentifier}`); - - return; - } - const oldSSRC = this.localSSRCs.get(track.rtcId); - const newSSRCNum = this._extractPrimarySSRC(newSSRC); - const oldSSRCNum = this._extractPrimarySSRC(oldSSRC); - - // eslint-disable-next-line no-negated-condition - if (newSSRCNum !== oldSSRCNum) { - oldSSRCNum && logger.error(`${this} Overwriting SSRC for track=${track}] with ssrc=${newSSRC}`); - this.localSSRCs.set(track.rtcId, newSSRC); - track.setSsrc(newSSRCNum); - this.eventEmitter.emit(RTCEvents.LOCAL_TRACK_SSRC_UPDATED, track, newSSRCNum); - } - } else if (!track.isVideoTrack() && !track.isMuted()) { - // It is normal to find no SSRCs for a muted video track in - // the local SDP as the recv-only SSRC is no longer munged in. - // So log the warning only if it's not a muted video track. - logger.warn(`${this} No SSRCs found in the local SDP for track=${track}, stream=${sourceIdentifier}`); - } - } -}; - /** * Track the SSRCs seen so far. * @param {number} ssrc - SSRC. diff --git a/modules/sdp/LocalSdpMunger.js b/modules/sdp/LocalSdpMunger.js index 1690e3d062..3da390b7c5 100644 --- a/modules/sdp/LocalSdpMunger.js +++ b/modules/sdp/LocalSdpMunger.js @@ -1,21 +1,15 @@ -import { getLogger } from '@jitsi/logger'; +import { isEqual } from 'lodash-es'; import { MediaDirection } from '../../service/RTC/MediaDirection'; import { MediaType } from '../../service/RTC/MediaType'; -import { getSourceNameForJitsiTrack } from '../../service/RTC/SignalingLayer'; import browser from '../browser'; import { SdpTransformWrap } from './SdpTransformUtil'; -const logger = getLogger(__filename); - /** - * Fakes local SDP exposed to {@link JingleSessionPC} through the local - * description getter. Modifies the SDP, so that it will contain muted local - * video tracks description, even though their underlying {MediaStreamTrack}s - * are no longer in the WebRTC peerconnection. That prevents from SSRC updates - * being sent to Jicofo/remote peer and prevents sRD/sLD cycle on the remote - * side. + * Fakes local SDP exposed to {@link JingleSessionPC} through the local description getter. Modifies the SDP, so that + * the stream identifiers are unique across all of the local PeerConnections and that the source names and video types + * are injected so that Jicofo can use them to identify the sources. */ export default class LocalSdpMunger { @@ -28,77 +22,78 @@ export default class LocalSdpMunger { constructor(tpc, localEndpointId) { this.tpc = tpc; this.localEndpointId = localEndpointId; - this.audioSourcesToMsidMap = new Map(); - this.videoSourcesToMsidMap = new Map(); - } - - /** - * Returns a string that can be set as the MSID attribute for a source. - * - * @param {string} mediaType - Media type of the source. - * @param {string} trackId - Id of the MediaStreamTrack associated with the source. - * @param {string} streamId - Id of the MediaStream associated with the source. - * @returns {string|null} - */ - _generateMsidAttribute(mediaType, trackId, streamId) { - if (!(mediaType && trackId)) { - logger.error(`Unable to munge local MSID - track id=${trackId} or media type=${mediaType} is missing`); - - return null; - } - const pcId = this.tpc.id; - - return `${streamId}-${pcId} ${trackId}-${pcId}`; } /** - * Updates or adds a 'msid' attribute in the format '---' - * example - d8ff91-video-0-1 - * All other attributes like 'cname', 'label' and 'mslabel' are removed since these are not processed by Jicofo. + * Updates or adds a 'msid' attribute for the local sources in the SDP. Also adds 'sourceName' and 'videoType' + * (if applicable) attributes. All other source attributes like 'cname', 'label' and 'mslabel' are removed since + * these are not processed by Jicofo. * * @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be * modified in place. * @returns {void} * @private */ - _transformMediaIdentifiers(mediaSection) { - const mediaType = mediaSection.mLine?.type; - const mediaDirection = mediaSection.mLine?.direction; - const msidLine = mediaSection.mLine?.msid; - const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ]; - const streamId = `${this.localEndpointId}-${mediaType}`; - let trackId = msidLine ? msidLine.split(' ')[1] : `${this.localEndpointId}-${mediaSection.mLine.mid}`; - - // Always overwrite msid since we want the msid to be in this format even if the browser generates one. - for (const source of sources) { - const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid'); - - if (msid) { - trackId = msid.value.split(' ')[1]; + _transformMediaIdentifiers(mediaSection, ssrcMap) { + const mediaType = mediaSection.mLine.type; + const mediaDirection = mediaSection.mLine.direction; + const sources = [ ...new Set(mediaSection.mLine.ssrcs?.map(s => s.id)) ]; + let sourceName; + + if (ssrcMap.size) { + const sortedSources = sources.slice().sort(); + + for (const [ id, trackSsrcs ] of ssrcMap.entries()) { + if (isEqual(sortedSources, [ ...trackSsrcs.ssrcs ].sort())) { + sourceName = id; + } } - this._updateSourcesToMsidMap(mediaType, streamId, trackId); - const storedStreamId = mediaType === MediaType.VIDEO - ? this.videoSourcesToMsidMap.get(trackId) - : this.audioSourcesToMsidMap.get(trackId); - - const generatedMsid = this._generateMsidAttribute(mediaType, trackId, storedStreamId); - - // Update the msid if the 'msid' attribute exists. - if (msid) { - msid.value = generatedMsid; - - // Generate the 'msid' attribute if there is a local source. - } else if (mediaDirection === MediaDirection.SENDONLY || mediaDirection === MediaDirection.SENDRECV) { - mediaSection.ssrcs.push({ - id: source, - attribute: 'msid', - value: generatedMsid - }); + for (const source of sources) { + if ((mediaDirection === MediaDirection.SENDONLY || mediaDirection === MediaDirection.SENDRECV) + && sourceName) { + const msid = ssrcMap.get(sourceName).msid; + const generatedMsid = `${msid}-${this.tpc.id}`; + const existingMsid = mediaSection.ssrcs + .find(ssrc => ssrc.id === source && ssrc.attribute === 'msid'); + + // Always overwrite msid since we want the msid to be in this format even if the browser generates + // one. '---' example - d8ff91-video-0-1 + if (existingMsid) { + existingMsid.value = generatedMsid; + } else { + mediaSection.ssrcs.push({ + id: source, + attribute: 'msid', + value: generatedMsid + }); + } + + // Inject source names as a=ssrc:3124985624 name:endpointA-v0 + mediaSection.ssrcs.push({ + id: source, + attribute: 'name', + value: sourceName + }); + + const videoType = this.tpc.getLocalVideoTracks() + .find(track => track.getSourceName() === sourceName) + ?.getVideoType(); + + if (mediaType === MediaType.VIDEO && videoType) { + // Inject videoType as a=ssrc:1234 videoType:desktop. + mediaSection.ssrcs.push({ + id: source, + attribute: 'videoType', + value: videoType + }); + } + } } } - // Ignore the 'cname', 'label' and 'mslabel' attributes and only have the 'msid' attribute. - mediaSection.ssrcs = mediaSection.ssrcs.filter(ssrc => ssrc.attribute === 'msid'); + // Ignore the 'cname', 'label' and 'mslabel' attributes. + mediaSection.ssrcs = mediaSection.ssrcs + .filter(ssrc => ssrc.attribute === 'msid' || ssrc.attribute === 'name' || ssrc.attribute === 'videoType'); // On FF when the user has started muted create answer will generate a recv only SSRC. We don't want to signal // this SSRC in order to reduce the load of the xmpp server for large calls. Therefore the SSRC needs to be @@ -122,43 +117,16 @@ export default class LocalSdpMunger { } /** - * Updates the MSID map. + * This transformation will make sure that stream identifiers are unique across all of the local PeerConnections + * even if the same stream is used by multiple instances at the same time. It also injects 'sourceName' and + * 'videoType' attribute. * - * @param {string} mediaType The media type. - * @param {string} streamId The stream id. - * @param {string} trackId The track id. - * @returns {void} - */ - _updateSourcesToMsidMap(mediaType, streamId, trackId) { - if (mediaType === MediaType.VIDEO) { - if (!this.videoSourcesToMsidMap.has(trackId)) { - const generatedStreamId = `${streamId}-${this.videoSourcesToMsidMap.size}`; - - this.videoSourcesToMsidMap.set(trackId, generatedStreamId); - } - } else if (!this.audioSourcesToMsidMap.has(trackId)) { - const generatedStreamId = `${streamId}-${this.audioSourcesToMsidMap.size}`; - - this.audioSourcesToMsidMap.set(trackId, generatedStreamId); - } - } - - /** - * This transformation will make sure that stream identifiers are unique - * across all of the local PeerConnections even if the same stream is used - * by multiple instances at the same time. - * Each PeerConnection assigns different SSRCs to the same local - * MediaStream, but the MSID remains the same as it's used to identify - * the stream by the WebRTC backend. The transformation will append - * {@link TraceablePeerConnection#id} at the end of each stream's identifier - * ("cname", "msid", "label" and "mslabel"). - * - * @param {RTCSessionDescription} sessionDesc - The local session - * description (this instance remains unchanged). + * @param {RTCSessionDescription} sessionDesc - The local session description (this instance remains unchanged). + * @param {Map} ssrcMap - The SSRC and source map for the local tracks. * @return {RTCSessionDescription} - Transformed local session description * (a modified copy of the one given as the input). */ - transformStreamIdentifiers(sessionDesc) { + transformStreamIdentifiers(sessionDesc, ssrcMap) { if (!sessionDesc || !sessionDesc.sdp || !sessionDesc.type) { return sessionDesc; } @@ -167,80 +135,18 @@ export default class LocalSdpMunger { const audioMLine = transformer.selectMedia(MediaType.AUDIO)?.[0]; if (audioMLine) { - this._transformMediaIdentifiers(audioMLine); - this._injectSourceNames(audioMLine); + this._transformMediaIdentifiers(audioMLine, ssrcMap); } const videoMlines = transformer.selectMedia(MediaType.VIDEO); for (const videoMLine of videoMlines) { - this._transformMediaIdentifiers(videoMLine); - this._injectSourceNames(videoMLine); + this._transformMediaIdentifiers(videoMLine, ssrcMap); } - // Reset the local tracks based maps for msid after every transformation since Chrome 122 is generating - // a new set of SSRCs for the same source when the direction of transceiver changes because of a remote - // source getting added on the p2p connection. - this.audioSourcesToMsidMap.clear(); - this.videoSourcesToMsidMap.clear(); - return new RTCSessionDescription({ type: sessionDesc.type, sdp: transformer.toRawSDP() }); } - - /** - * Injects source names. Source names are need to for multiple streams per endpoint support. The final plan is to - * use the "mid" attribute for source names, but because the SDP to Jingle conversion still operates in the Plan-B - * semantics (one source name per media), a custom "name" attribute is injected into SSRC lines.. - * - * @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be - * modified in place. - * @returns {void} - * @private - */ - _injectSourceNames(mediaSection) { - const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ]; - const mediaType = mediaSection.mLine?.type; - - if (!mediaType) { - throw new Error('_transformMediaIdentifiers - no media type in mediaSection'); - } - - for (const source of sources) { - const nameExists = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'name'); - const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid').value; - const streamId = msid.split(' ')[0]; - - // Example stream id: d8ff91-video-8-1 - // In the example above 8 is the track index - const trackIndexParts = streamId.split('-'); - const trackIndex = trackIndexParts[trackIndexParts.length - 2]; - const sourceName = getSourceNameForJitsiTrack(this.localEndpointId, mediaType, trackIndex); - - if (!nameExists) { - // Inject source names as a=ssrc:3124985624 name:endpointA-v0 - mediaSection.ssrcs.push({ - id: source, - attribute: 'name', - value: sourceName - }); - } - - if (mediaType === MediaType.VIDEO) { - const videoType = this.tpc.getLocalVideoTracks().find(track => track.getSourceName() === sourceName) - ?.getVideoType(); - - if (videoType) { - // Inject videoType as a=ssrc:1234 videoType:desktop. - mediaSection.ssrcs.push({ - id: source, - attribute: 'videoType', - value: videoType - }); - } - } - } - } } diff --git a/modules/sdp/LocalSdpMunger.spec.js b/modules/sdp/LocalSdpMunger.spec.js index 5f1df8196e..8dbafee664 100644 --- a/modules/sdp/LocalSdpMunger.spec.js +++ b/modules/sdp/LocalSdpMunger.spec.js @@ -2,7 +2,6 @@ import * as transform from 'sdp-transform'; import { MockPeerConnection } from '../RTC/MockClasses'; -import FeatureFlags from '../flags/FeatureFlags'; import LocalSdpMunger from './LocalSdpMunger'; import { default as SampleSdpStrings } from './SampleSdpStrings.js'; @@ -26,7 +25,6 @@ describe('TransformSdpsForUnifiedPlan', () => { const localEndpointId = 'sRdpsdg'; beforeEach(() => { - FeatureFlags.init({ }); localSdpMunger = new LocalSdpMunger(tpc, localEndpointId); }); describe('StripSsrcs', () => { @@ -38,7 +36,7 @@ describe('TransformSdpsForUnifiedPlan', () => { type: 'offer', sdp: sdpStr }); - const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc); + const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, {}); const newSdp = transform.parse(transformedDesc.sdp); const audioSsrcs = getSsrcLines(newSdp, 'audio'); const videoSsrcs = getSsrcLines(newSdp, 'video'); @@ -56,7 +54,20 @@ describe('TransformSdpsForUnifiedPlan', () => { type: 'offer', sdp: sdpStr }); - const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc); + const ssrcMap = new Map(); + + ssrcMap.set('sRdpsdg-v0', { + ssrcs: [ 1757014965, 1479742055, 1089111804 ], + msid: 'sRdpsdg-video-0', + groups: [ { + semantics: 'SIM', + ssrcs: [ 1757014965, 1479742055, 1089111804 ] } ] + }); + ssrcMap.set('sRdpsdg-a0', { + ssrcs: [ 124723944 ], + msid: 'sRdpsdg-audio-0' + }); + const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap); const newSdp = transform.parse(transformedDesc.sdp); audioSsrcs = getSsrcLines(newSdp, 'audio'); @@ -79,14 +90,24 @@ describe('TransformSdpsForUnifiedPlan', () => { type: 'offer', sdp: sdpStr }); - const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc); + const ssrcMap = new Map(); + + ssrcMap.set('sRdpsdg-v0', { + ssrcs: [ 984899560 ], + msid: 'sRdpsdg-video-0' + }); + ssrcMap.set('sRdpsdg-a0', { + ssrcs: [ 124723944 ], + msid: 'sRdpsdg-audio-0' + }); + const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap); const newSdp = transform.parse(transformedDesc.sdp); const videoSsrcs = getSsrcLines(newSdp, 'video'); for (const ssrcLine of videoSsrcs) { if (ssrcLine.attribute === 'msid') { - const msid = ssrcLine.value.split(' ')[0]; + const msid = ssrcLine.value; expect(msid).toBe(`${localEndpointId}-video-0-${tpc.id}`); } @@ -102,7 +123,17 @@ describe('TransformSdpsForUnifiedPlan', () => { type: 'offer', sdp: sdpStr }); - const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc); + const ssrcMap = new Map(); + + ssrcMap.set('sRdpsdg-v0', { + ssrcs: [ 984899560 ], + msid: 'sRdpsdg-video-0' + }); + ssrcMap.set('sRdpsdg-a0', { + ssrcs: [ 124723944 ], + msid: 'sRdpsdg-audio-0' + }); + const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap); const newSdp = transform.parse(transformedDesc.sdp); const videoSsrcs = getSsrcLines(newSdp, 'video'); const msidExists = videoSsrcs.find(s => s.attribute === 'msid'); @@ -124,7 +155,17 @@ describe('Transform msids for source-name signaling', () => { type: 'offer', sdp: sdpStr }); - const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc); + const ssrcMap = new Map(); + + ssrcMap.set('sRdpsdg-v0', { + ssrcs: [ 1757014965, 984899560, 1479742055, 855213044, 1089111804, 2963867077 ], + msid: 'sRdpsdg-video-0' + }); + ssrcMap.set('sRdpsdg-a0', { + ssrcs: [ 124723944 ], + msid: 'sRdpsdg-audio-0' + }); + const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap); const newSdp = transform.parse(transformedDesc.sdp); audioMsidLine = getSsrcLines(newSdp, 'audio').find(ssrc => ssrc.attribute === 'msid')?.value; @@ -134,7 +175,6 @@ describe('Transform msids for source-name signaling', () => { }; it('should transform', () => { - FeatureFlags.init({ }); transformStreamIdentifiers(); expect(audioMsid).toBe('sRdpsdg-audio-0-1'); diff --git a/modules/sdp/SDP.js b/modules/sdp/SDP.js index 80066832b8..3a442d0f0f 100644 --- a/modules/sdp/SDP.js +++ b/modules/sdp/SDP.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import { cloneDeep } from 'lodash-es'; import transform from 'sdp-transform'; +import { Strophe } from 'strophe.js'; import { MediaDirection } from '../../service/RTC/MediaDirection'; import { MediaType } from '../../service/RTC/MediaType'; @@ -20,8 +21,9 @@ export default class SDP { * * @param {string} sdp - The SDP generated by the browser when SDP->Jingle conversion is needed, an empty string * when Jingle->SDP conversion is needed. + * @param {boolean} isP2P - Whether this SDP belongs to a p2p peerconnection. */ - constructor(sdp) { + constructor(sdp, isP2P = false) { const media = sdp.split('\r\nm='); for (let i = 1, length = media.length; i < length; i++) { @@ -34,6 +36,7 @@ export default class SDP { } const session = `${media.shift()}\r\n`; + this.isP2P = isP2P; this.media = media; this.raw = session + media.join(''); this.session = session; @@ -86,9 +89,9 @@ export default class SDP { * @returns {boolean} */ containsSSRC(ssrc) { - const souceMap = this.getMediaSsrcMap(); + const sourceMap = this.getMediaSsrcMap(); - return Object.values(souceMap).some(media => media.ssrcs[ssrc]); + return [ ...sourceMap.values() ].some(media => media.ssrcs[ssrc]); } /** @@ -141,19 +144,19 @@ export default class SDP { * @returns {*} */ getMediaSsrcMap() { - const mediaSSRCs = {}; + const sourceInfo = new Map(); this.media.forEach((mediaItem, mediaindex) => { const mid = SDPUtil.parseMID(SDPUtil.findLine(mediaItem, 'a=mid:')); + const mline = SDPUtil.parseMLine(mediaItem.split('\r\n')[0]); const media = { mediaindex, + mediaType: mline.media, mid, ssrcs: {}, ssrcGroups: [] }; - mediaSSRCs[mediaindex] = media; - SDPUtil.findLines(mediaItem, 'a=ssrc:').forEach(line => { const linessrc = line.substring(7).split(' ')[0]; @@ -179,9 +182,11 @@ export default class SDP { }); } }); + + sourceInfo.set(mediaindex, media); }); - return mediaSSRCs; + return sourceInfo; } /** @@ -444,16 +449,21 @@ export default class SDP { xmlns: XEP.BUNDLE_MEDIA, semantics }); - parts.forEach(part => elem.c('content', { name: part }).up()); + + // Bundle all the media types. Jicofo expects the 'application' media type to be signaled as 'data'. + let mediaTypes = [ MediaType.AUDIO, MediaType.VIDEO, 'data' ]; + + // For p2p connection, 'mid' will be used in the bundle group. + if (this.isP2P) { + mediaTypes = this.media.map(mediaItem => SDPUtil.parseMID(SDPUtil.findLine(mediaItem, 'a=mid:'))); + } + mediaTypes.forEach(type => elem.c('content', { name: type }).up()); elem.up(); }); this.media.forEach((mediaItem, i) => { const mline = SDPUtil.parseMLine(mediaItem.split('\r\n')[0]); - - if (![ MediaType.AUDIO, MediaType.VIDEO, MediaType.APPLICATION ].includes(mline.media)) { - return; - } + const mediaType = mline.media === MediaType.APPLICATION ? 'data' : mline.media; let ssrc = false; const assrcline = SDPUtil.findLine(mediaItem, 'a=ssrc:'); @@ -462,32 +472,89 @@ export default class SDP { ssrc = assrcline.substring(7).split(' ')[0]; } + const contents = $(elem.tree()).find(`content[name='${mediaType}']`); + + // Append source groups from the new m-lines to the existing media description. The SDP will have multiple + // m-lines for audio and video including the recv-only ones for remote sources but there needs to be only + // one media description for a given media type that should include all the sources, i.e., both the camera + // and screenshare sources should be added to the 'video' description. + for (const content of contents) { + if (!content.hasAttribute('creator')) { + // eslint-disable-next-line no-continue + continue; + } + + if (ssrc) { + const description = $(content).find('description'); + const ssrcMap = SDPUtil.parseSSRC(mediaItem); + + for (const [ availableSsrc, ssrcParameters ] of ssrcMap) { + const sourceName = SDPUtil.parseSourceNameLine(ssrcParameters); + const videoType = SDPUtil.parseVideoTypeLine(ssrcParameters); + const source = Strophe.xmlElement('source', { + ssrc: availableSsrc, + name: sourceName, + videoType, + xmlns: XEP.SOURCE_ATTRIBUTES + }); + + const msid = SDPUtil.parseMSIDAttribute(ssrcParameters); + + if (msid) { + const param = Strophe.xmlElement('parameter', { + name: 'msid', + value: msid + }); + + source.append(param); + } + description.append(source); + } + + const ssrcGroupLines = SDPUtil.findLines(mediaItem, 'a=ssrc-group:'); + + ssrcGroupLines.forEach(line => { + const { semantics, ssrcs } = SDPUtil.parseSSRCGroupLine(line); + + if (ssrcs.length) { + const group = Strophe.xmlElement('ssrc-group', { + semantics, + xmlns: XEP.SOURCE_ATTRIBUTES + }); + + for (const val of ssrcs) { + const src = Strophe.xmlElement('source', { + ssrc: val + }); + + group.append(src); + } + description.append(group); + } + }); + } + + return; + } + const mid = SDPUtil.parseMID(SDPUtil.findLine(mediaItem, 'a=mid:')); + elem.c('content', { creator: thecreator, - name: mline.media + name: this.isP2P ? mid : mediaType }); - const amidline = SDPUtil.findLine(mediaItem, 'a=mid:'); - if (amidline) { - // Prefer identifier from a=mid if present. - elem.attrs({ name: SDPUtil.parseMID(amidline) }); - } - - if (mline.media === MediaType.VIDEO && typeof this.initialLastN === 'number') { + if (mediaType === MediaType.VIDEO && typeof this.initialLastN === 'number') { elem.c('initial-last-n', { xmlns: 'jitsi:colibri2', value: this.initialLastN }).up(); } - if ([ MediaType.AUDIO, MediaType.VIDEO ].includes(mline.media)) { + if ([ MediaType.AUDIO, MediaType.VIDEO ].includes(mediaType)) { elem.c('description', { xmlns: XEP.RTP_MEDIA, - media: mline.media + media: mediaType }); - if (ssrc) { - elem.attrs({ ssrc }); - } mline.fmt.forEach(format => { const rtpmap = SDPUtil.findLine(mediaItem, `a=rtpmap:${format}`); @@ -536,9 +603,7 @@ export default class SDP { const ssrcGroupLines = SDPUtil.findLines(mediaItem, 'a=ssrc-group:'); ssrcGroupLines.forEach(line => { - const idx = line.indexOf(' '); - const semantics = line.substr(0, idx).substr(13); - const ssrcs = line.substr(14 + semantics.length).split(' '); + const { semantics, ssrcs } = SDPUtil.parseSSRCGroupLine(line); if (ssrcs.length) { elem.c('ssrc-group', { diff --git a/modules/sdp/SDP.spec.js b/modules/sdp/SDP.spec.js index 8384620d66..bb857e0767 100644 --- a/modules/sdp/SDP.spec.js +++ b/modules/sdp/SDP.spec.js @@ -6,6 +6,8 @@ import { expandSourcesFromJson } from '../xmpp/JingleHelperFunctions'; import SDP from './SDP'; +/* eslint-disable max-len*/ + /** * @param {string} xml - raw xml of the stanza */ @@ -18,7 +20,6 @@ describe('SDP', () => { FeatureFlags.init({ }); }); describe('toJingle', () => { - /* eslint-disable max-len*/ const testSdp = [ 'v=0\r\n', 'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n', @@ -79,7 +80,6 @@ describe('SDP', () => { 'a=ssrc-group:FID 4004 4005\r\n', 'a=rtcp-mux\r\n' ].join(''); - /* eslint-enable max-len*/ it('correctly groups ssrcs lines that are not in order', () => { const sdp = new SDP(testSdp); @@ -95,7 +95,7 @@ describe('SDP', () => { sid: 'temp-sid' }); - sdp.toJingle(accept, false); + sdp.toJingle(accept, 'responder'); const { nodeTree } = accept; const videoSources = nodeTree.querySelectorAll('description[media=\'video\']>source'); @@ -118,7 +118,7 @@ describe('SDP', () => { sid: 'temp-sid' }); - sdp.toJingle(accept, false); + sdp.toJingle(accept, 'responder'); const { nodeTree } = accept; @@ -135,8 +135,869 @@ describe('SDP', () => { }); }); + describe('toJingle for multiple m-lines', () => { + const testSdp = [ + 'v=0\r\n', + 'o=- 6251210045590020951 2 IN IP4 127.0.0.1\r\n', + 's=-\r\n', + 't=0 0\r\n', + 'a=msid-semantic: WMS\r\n', + 'a=group:BUNDLE 0 1 2\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:111 opus/48000/2\r\n', + 'a=rtpmap:126 telephone-event/8000\r\n', + 'a=fmtp:111 minptime=10;useinbandfec=1\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:111 transport-cc\r\n', + 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:0\r\n', + 'a=msid:- 5caf9eeb-f846-43cf-8868-78ed2e0fea74\r\n', + 'a=sendrecv\r\n', + 'a=ice-ufrag:gi+W\r\n', + 'a=ice-pwd:NmFZJ6NWoC2gjagIudLFWI8Q\r\n', + 'a=fingerprint:sha-256 41:1D:49:50:40:0D:68:9F:C6:AB:B2:14:98:67:E7:06:70:F0:B2:4A:5C:AB:03:F3:89:AF:B0:11:AF:05:2D:D6\r\n', + 'a=ice-options:trickle\r\n', + 'a=ssrc:3134174615 cname:Ypjacq/wapOqDJKy\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:101 VP9/90000\r\n', + 'a=rtpmap:97 rtx/90000\r\n', + 'a=rtpmap:100 VP8/90000\r\n', + 'a=rtpmap:96 rtx/90000\r\n', + 'a=rtpmap:107 H264/90000\r\n', + 'a=rtpmap:99 rtx/90000\r\n', + 'a=rtpmap:41 AV1/90000\r\n', + 'a=rtpmap:42 rtx/90000\r\n', + 'a=fmtp:101 profile-id=0\r\n', + 'a=fmtp:97 apt=101\r\n', + 'a=fmtp:96 apt=100\r\n', + 'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n', + 'a=fmtp:99 apt=107\r\n', + 'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n', + 'a=fmtp:42 apt=41\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:101 ccm fir\r\n', + 'a=rtcp-fb:101 nack\r\n', + 'a=rtcp-fb:101 nack pli\r\n', + 'a=rtcp-fb:101 transport-cc\r\n', + 'a=rtcp-fb:97 ccm fir\r\n', + 'a=rtcp-fb:97 nack\r\n', + 'a=rtcp-fb:97 nack pli\r\n', + 'a=rtcp-fb:100 ccm fir\r\n', + 'a=rtcp-fb:100 nack\r\n', + 'a=rtcp-fb:100 nack pli\r\n', + 'a=rtcp-fb:100 transport-cc\r\n', + 'a=rtcp-fb:96 ccm fir\r\n', + 'a=rtcp-fb:96 nack\r\n', + 'a=rtcp-fb:96 nack pli\r\n', + 'a=rtcp-fb:107 ccm fir\r\n', + 'a=rtcp-fb:107 nack\r\n', + 'a=rtcp-fb:107 nack pli\r\n', + 'a=rtcp-fb:107 transport-cc\r\n', + 'a=rtcp-fb:41 ccm fir\r\n', + 'a=rtcp-fb:41 nack\r\n', + 'a=rtcp-fb:41 nack pli\r\n', + 'a=rtcp-fb:41 transport-cc\r\n', + 'a=rtcp-fb:42 ccm fir\r\n', + 'a=rtcp-fb:42 nack\r\n', + 'a=rtcp-fb:42 nack pli\r\n', + 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:1\r\n', + 'a=msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n', + 'a=sendrecv\r\n', + 'a=ice-ufrag:gi+W\r\n', + 'a=ice-pwd:NmFZJ6NWoC2gjagIudLFWI8Q\r\n', + 'a=fingerprint:sha-256 41:1D:49:50:40:0D:68:9F:C6:AB:B2:14:98:67:E7:06:70:F0:B2:4A:5C:AB:03:F3:89:AF:B0:11:AF:05:2D:D6\r\n', + 'a=ice-options:trickle\r\n', + 'a=ssrc:691901703 cname:Ypjacq/wapOqDJKy\r\n', + 'a=ssrc:3967743536 cname:Ypjacq/wapOqDJKy\r\n', + 'a=ssrc:691901703 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n', + 'a=ssrc:3967743536 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n', + 'a=ssrc:4098097822 cname:Ypjacq/wapOqDJKy\r\n', + 'a=ssrc:4098097822 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n', + 'a=ssrc:731566086 cname:Ypjacq/wapOqDJKy\r\n', + 'a=ssrc:731566086 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n', + 'a=ssrc:2374965413 cname:Ypjacq/wapOqDJKy\r\n', + 'a=ssrc:2374965413 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n', + 'a=ssrc:3680614139 cname:Ypjacq/wapOqDJKy\r\n', + 'a=ssrc:3680614139 msid:- 84615c77-2441-4d1f-801d-591a4bc1beaa\r\n', + 'a=ssrc-group:FID 691901703 3967743536\r\n', + 'a=ssrc-group:SIM 691901703 4098097822 731566086\r\n', + 'a=ssrc-group:FID 4098097822 2374965413\r\n', + 'a=ssrc-group:FID 731566086 3680614139\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=setup:active\r\n', + 'a=mid:2\r\n', + 'a=ice-ufrag:gi+W\r\n', + 'a=ice-pwd:NmFZJ6NWoC2gjagIudLFWI8Q\r\n', + 'a=fingerprint:sha-256 41:1D:49:50:40:0D:68:9F:C6:AB:B2:14:98:67:E7:06:70:F0:B2:4A:5C:AB:03:F3:89:AF:B0:11:AF:05:2D:D6\r\n', + 'a=ice-options:trickle\r\n', + 'a=sctp-port:5000\r\n', + 'a=max-message-size:262144\r\n' + ].join(''); + + it('correctly groups ssrcs lines', () => { + const sdp = new SDP(testSdp); + const accept = $iq({ + to: 'peerjid', + type: 'set' + }) + .c('jingle', { + xmlns: 'urn:xmpp:jingle:1', + action: 'session-accept', + initiator: false, + responder: true, + sid: 'temp-sid' + }); + + sdp.toJingle(accept, 'responder'); + const { nodeTree } = accept; + const content = nodeTree.querySelectorAll('jingle>content'); + + expect(content.length).toBe(3); + const videoSources = nodeTree.querySelectorAll('description[media=\'video\']>source'); + + expect(videoSources.length).toBe(6); + const audioSources = nodeTree.querySelectorAll('description[media=\'audio\']>source'); + + expect(audioSources.length).toBe(1); + const videoSourceGroups = nodeTree.querySelectorAll('description[media=\'video\']>ssrc-group'); + + expect(videoSourceGroups.length).toBe(4); + const data = nodeTree.querySelectorAll('jingle>content[name=\'data\']'); + + expect(data.length).toBe(1); + }); + }); + + describe('toJingle for multiple m-lines with recv-only', () => { + const testSdp = [ + 'v=0\r\n', + 'o=- 8014175770430016012 6 IN IP4 127.0.0.1\r\n', + 's=-\r\n', + 't=0 0\r\n', + 'a=msid-semantic: WMS\r\n', + 'a=group:BUNDLE 0 1 2 3 4 5 6 7\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:111 opus/48000/2\r\n', + 'a=rtpmap:126 telephone-event/8000\r\n', + 'a=fmtp:111 minptime=10;useinbandfec=1\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:111 transport-cc\r\n', + 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:0\r\n', + 'a=msid:- 836692af-4ea9-432f-811c-fef6ec7ee612\r\n', + 'a=sendrecv\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=candidate:4240059272 1 UDP 2122260223 x.x.x.x 54192 typ host\r\n', + 'a=ice-options:trickle\r\n', + 'a=ssrc:2833013218 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:2833013218 msid:- 836692af-4ea9-432f-811c-fef6ec7ee612\r\n', + 'a=ssrc:2833013218 name:abcd-a0\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:101 VP9/90000\r\n', + 'a=rtpmap:97 rtx/90000\r\n', + 'a=rtpmap:100 VP8/90000\r\n', + 'a=rtpmap:96 rtx/90000\r\n', + 'a=rtpmap:107 H264/90000\r\n', + 'a=rtpmap:99 rtx/90000\r\n', + 'a=rtpmap:41 AV1/90000\r\n', + 'a=rtpmap:42 rtx/90000\r\n', + 'a=fmtp:101 profile-id=0\r\n', + 'a=fmtp:97 apt=101\r\n', + 'a=fmtp:96 apt=100\r\n', + 'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n', + 'a=fmtp:99 apt=107\r\n', + 'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n', + 'a=fmtp:42 apt=41\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:101 ccm fir\r\n', + 'a=rtcp-fb:101 nack\r\n', + 'a=rtcp-fb:101 nack pli\r\n', + 'a=rtcp-fb:101 transport-cc\r\n', + 'a=rtcp-fb:97 ccm fir\r\n', + 'a=rtcp-fb:97 nack\r\n', + 'a=rtcp-fb:97 nack pli\r\n', + 'a=rtcp-fb:100 ccm fir\r\n', + 'a=rtcp-fb:100 nack\r\n', + 'a=rtcp-fb:100 nack pli\r\n', + 'a=rtcp-fb:100 transport-cc\r\n', + 'a=rtcp-fb:96 ccm fir\r\n', + 'a=rtcp-fb:96 nack\r\n', + 'a=rtcp-fb:96 nack pli\r\n', + 'a=rtcp-fb:107 ccm fir\r\n', + 'a=rtcp-fb:107 nack\r\n', + 'a=rtcp-fb:107 nack pli\r\n', + 'a=rtcp-fb:107 transport-cc\r\n', + 'a=rtcp-fb:41 ccm fir\r\n', + 'a=rtcp-fb:41 nack\r\n', + 'a=rtcp-fb:41 nack pli\r\n', + 'a=rtcp-fb:41 transport-cc\r\n', + 'a=rtcp-fb:42 ccm fir\r\n', + 'a=rtcp-fb:42 nack\r\n', + 'a=rtcp-fb:42 nack pli\r\n', + 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:1\r\n', + 'a=msid:- 72254a21-ae73-4c0e-bd47-e84a2d3b9474\r\n', + 'a=sendrecv\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=ssrc:1261622218 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:1261622218 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n', + 'a=ssrc:1261622218 videoType:camera\r\n', + 'a=ssrc:1261622218 name:abcd-v0\r\n', + 'a=ssrc:2809057491 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:2809057491 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n', + 'a=ssrc:2809057491 videoType:camera\r\n', + 'a=ssrc:2809057491 name:abcd-v0\r\n', + 'a=ssrc:4223705690 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:4223705690 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n', + 'a=ssrc:4223705690 videoType:camera\r\n', + 'a=ssrc:4223705690 name:abcd-v0\r\n', + 'a=ssrc:44482421 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:44482421 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n', + 'a=ssrc:44482421 videoType:camera\r\n', + 'a=ssrc:44482421 name:abcd-v0\r\n', + 'a=ssrc:1408200021 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:1408200021 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n', + 'a=ssrc:1408200021 videoType:camera\r\n', + 'a=ssrc:1408200021 name:abcd-v0\r\n', + 'a=ssrc:712505591 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:712505591 msid:- 7c3aee52-697e-446e-a898-9ea470a19b27\r\n', + 'a=ssrc:712505591 videoType:camera\r\n', + 'a=ssrc:712505591 name:abcd-v0\r\n', + 'a=ssrc-group:FID 1261622218 2809057491\r\n', + 'a=ssrc-group:SIM 1261622218 4223705690 44482421\r\n', + 'a=ssrc-group:FID 4223705690 1408200021\r\n', + 'a=ssrc-group:FID 44482421 712505591\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=setup:active\r\n', + 'a=mid:2\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=sctp-port:5000\r\n', + 'a=max-message-size:262144\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:101 VP9/90000\r\n', + 'a=rtpmap:97 rtx/90000\r\n', + 'a=rtpmap:100 VP8/90000\r\n', + 'a=rtpmap:96 rtx/90000\r\n', + 'a=rtpmap:107 H264/90000\r\n', + 'a=rtpmap:99 rtx/90000\r\n', + 'a=rtpmap:41 AV1/90000\r\n', + 'a=rtpmap:42 rtx/90000\r\n', + 'a=fmtp:101 profile-id=0\r\n', + 'a=fmtp:97 apt=101\r\n', + 'a=fmtp:96 apt=100\r\n', + 'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n', + 'a=fmtp:99 apt=107\r\n', + 'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n', + 'a=fmtp:42 apt=41\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:101 ccm fir\r\n', + 'a=rtcp-fb:101 nack\r\n', + 'a=rtcp-fb:101 nack pli\r\n', + 'a=rtcp-fb:101 transport-cc\r\n', + 'a=rtcp-fb:97 ccm fir\r\n', + 'a=rtcp-fb:97 nack\r\n', + 'a=rtcp-fb:97 nack pli\r\n', + 'a=rtcp-fb:100 ccm fir\r\n', + 'a=rtcp-fb:100 nack\r\n', + 'a=rtcp-fb:100 nack pli\r\n', + 'a=rtcp-fb:100 transport-cc\r\n', + 'a=rtcp-fb:96 ccm fir\r\n', + 'a=rtcp-fb:96 nack\r\n', + 'a=rtcp-fb:96 nack pli\r\n', + 'a=rtcp-fb:107 ccm fir\r\n', + 'a=rtcp-fb:107 nack\r\n', + 'a=rtcp-fb:107 nack pli\r\n', + 'a=rtcp-fb:107 transport-cc\r\n', + 'a=rtcp-fb:41 ccm fir\r\n', + 'a=rtcp-fb:41 nack\r\n', + 'a=rtcp-fb:41 nack pli\r\n', + 'a=rtcp-fb:41 transport-cc\r\n', + 'a=rtcp-fb:42 ccm fir\r\n', + 'a=rtcp-fb:42 nack\r\n', + 'a=rtcp-fb:42 nack pli\r\n', + 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=extmap:11 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension\r\n', + 'a=setup:active\r\n', + 'a=mid:3\r\n', + 'a=recvonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:101 VP9/90000\r\n', + 'a=rtpmap:97 rtx/90000\r\n', + 'a=rtpmap:100 VP8/90000\r\n', + 'a=rtpmap:96 rtx/90000\r\n', + 'a=rtpmap:107 H264/90000\r\n', + 'a=rtpmap:99 rtx/90000\r\n', + 'a=rtpmap:41 AV1/90000\r\n', + 'a=rtpmap:42 rtx/90000\r\n', + 'a=fmtp:101 profile-id=0\r\n', + 'a=fmtp:97 apt=101\r\n', + 'a=fmtp:96 apt=100\r\n', + 'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n', + 'a=fmtp:99 apt=107\r\n', + 'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n', + 'a=fmtp:42 apt=41\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:101 ccm fir\r\n', + 'a=rtcp-fb:101 nack\r\n', + 'a=rtcp-fb:101 nack pli\r\n', + 'a=rtcp-fb:101 transport-cc\r\n', + 'a=rtcp-fb:97 ccm fir\r\n', + 'a=rtcp-fb:97 nack\r\n', + 'a=rtcp-fb:97 nack pli\r\n', + 'a=rtcp-fb:100 ccm fir\r\n', + 'a=rtcp-fb:100 nack\r\n', + 'a=rtcp-fb:100 nack pli\r\n', + 'a=rtcp-fb:100 transport-cc\r\n', + 'a=rtcp-fb:96 ccm fir\r\n', + 'a=rtcp-fb:96 nack\r\n', + 'a=rtcp-fb:96 nack pli\r\n', + 'a=rtcp-fb:107 ccm fir\r\n', + 'a=rtcp-fb:107 nack\r\n', + 'a=rtcp-fb:107 nack pli\r\n', + 'a=rtcp-fb:107 transport-cc\r\n', + 'a=rtcp-fb:41 ccm fir\r\n', + 'a=rtcp-fb:41 nack\r\n', + 'a=rtcp-fb:41 nack pli\r\n', + 'a=rtcp-fb:41 transport-cc\r\n', + 'a=rtcp-fb:42 ccm fir\r\n', + 'a=rtcp-fb:42 nack\r\n', + 'a=rtcp-fb:42 nack pli\r\n', + 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=extmap:11 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension\r\n', + 'a=setup:active\r\n', + 'a=mid:4\r\n', + 'a=recvonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:111 opus/48000/2\r\n', + 'a=rtpmap:126 telephone-event/8000\r\n', + 'a=fmtp:111 minptime=10;useinbandfec=1\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:111 transport-cc\r\n', + 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:5\r\n', + 'a=recvonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:111 opus/48000/2\r\n', + 'a=rtpmap:126 telephone-event/8000\r\n', + 'a=fmtp:111 minptime=10;useinbandfec=1\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:111 transport-cc\r\n', + 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:6\r\n', + 'a=recvonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:101 VP9/90000\r\n', + 'a=rtpmap:97 rtx/90000\r\n', + 'a=rtpmap:100 VP8/90000\r\n', + 'a=rtpmap:96 rtx/90000\r\n', + 'a=rtpmap:107 H264/90000\r\n', + 'a=rtpmap:99 rtx/90000\r\n', + 'a=rtpmap:41 AV1/90000\r\n', + 'a=rtpmap:42 rtx/90000\r\n', + 'a=fmtp:101 profile-id=0\r\n', + 'a=fmtp:97 apt=101\r\n', + 'a=fmtp:96 apt=100\r\n', + 'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n', + 'a=fmtp:99 apt=107\r\n', + 'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n', + 'a=fmtp:42 apt=41\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:101 ccm fir\r\n', + 'a=rtcp-fb:101 nack\r\n', + 'a=rtcp-fb:101 nack pli\r\n', + 'a=rtcp-fb:101 transport-cc\r\n', + 'a=rtcp-fb:97 ccm fir\r\n', + 'a=rtcp-fb:97 nack\r\n', + 'a=rtcp-fb:97 nack pli\r\n', + 'a=rtcp-fb:100 ccm fir\r\n', + 'a=rtcp-fb:100 nack\r\n', + 'a=rtcp-fb:100 nack pli\r\n', + 'a=rtcp-fb:100 transport-cc\r\n', + 'a=rtcp-fb:96 ccm fir\r\n', + 'a=rtcp-fb:96 nack\r\n', + 'a=rtcp-fb:96 nack pli\r\n', + 'a=rtcp-fb:107 ccm fir\r\n', + 'a=rtcp-fb:107 nack\r\n', + 'a=rtcp-fb:107 nack pli\r\n', + 'a=rtcp-fb:107 transport-cc\r\n', + 'a=rtcp-fb:41 ccm fir\r\n', + 'a=rtcp-fb:41 nack\r\n', + 'a=rtcp-fb:41 nack pli\r\n', + 'a=rtcp-fb:41 transport-cc\r\n', + 'a=rtcp-fb:42 ccm fir\r\n', + 'a=rtcp-fb:42 nack\r\n', + 'a=rtcp-fb:42 nack pli\r\n', + 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:7\r\n', + 'a=msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n', + 'a=sendonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=ssrc:4074534577 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:4074534577 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n', + 'a=ssrc:4074534577 videoType:desktop\r\n', + 'a=ssrc:4074534577 name:abcd-v1\r\n', + 'a=ssrc:3122913012 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:3122913012 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n', + 'a=ssrc:3122913012 videoType:desktop\r\n', + 'a=ssrc:3122913012 name:abcd-v1\r\n', + 'a=ssrc:3145321104 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:3145321104 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n', + 'a=ssrc:3145321104 videoType:desktop\r\n', + 'a=ssrc:3145321104 name:abcd-v1\r\n', + 'a=ssrc:2686550307 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:2686550307 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n', + 'a=ssrc:2686550307 videoType:desktop\r\n', + 'a=ssrc:2686550307 name:abcd-v1\r\n', + 'a=ssrc:2960588630 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:2960588630 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n', + 'a=ssrc:2960588630 videoType:desktop\r\n', + 'a=ssrc:2960588630 name:abcd-v1\r\n', + 'a=ssrc:3495860096 cname:0T+Z3AzTbva5NoHF\r\n', + 'a=ssrc:3495860096 msid:- 7c3aee52-697e-446e-a898-9ea470a19b26\r\n', + 'a=ssrc:3495860096 videoType:desktop\r\n', + 'a=ssrc:3495860096 name:abcd-v1\r\n', + 'a=ssrc-group:FID 4074534577 3122913012\r\n', + 'a=ssrc-group:SIM 4074534577 3145321104 2686550307\r\n', + 'a=ssrc-group:FID 3145321104 2960588630\r\n', + 'a=ssrc-group:FID 2686550307 3495860096\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n' + ].join(''); + + it('correctly groups ssrcs lines', () => { + const sdp = new SDP(testSdp); + const accept = $iq({ + to: 'peerjid', + type: 'set' + }) + .c('jingle', { + xmlns: 'urn:xmpp:jingle:1', + action: 'session-accept', + initiator: false, + responder: true, + sid: 'temp-sid' + }); + + sdp.toJingle(accept, 'responder'); + const { nodeTree } = accept; + const content = nodeTree.querySelectorAll('jingle>content'); + + expect(content.length).toBe(3); + const videoSources = nodeTree.querySelectorAll('description[media=\'video\']>source'); + + expect(videoSources.length).toBe(12); + const audioSources = nodeTree.querySelectorAll('description[media=\'audio\']>source'); + + expect(audioSources.length).toBe(1); + const videoSourceGroups = nodeTree.querySelectorAll('description[media=\'video\']>ssrc-group'); + + expect(videoSourceGroups.length).toBe(8); + const data = nodeTree.querySelectorAll('jingle>content[name=\'data\']'); + + expect(data.length).toBe(1); + }); + }); + + describe('toJingle for multiple m-lines with only recv-only', () => { + const testSdp = [ + 'v=0\r\n', + 'o=- 8014175770430016012 6 IN IP4 127.0.0.1\r\n', + 's=-\r\n', + 't=0 0\r\n', + 'a=msid-semantic: WMS\r\n', + 'a=group:BUNDLE 0 1 2 3 4 5 6 7\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:111 opus/48000/2\r\n', + 'a=rtpmap:126 telephone-event/8000\r\n', + 'a=fmtp:111 minptime=10;useinbandfec=1\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:111 transport-cc\r\n', + 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:0\r\n', + 'a=msid:- 836692af-4ea9-432f-811c-fef6ec7ee612\r\n', + 'a=recvonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=candidate:4240059272 1 UDP 2122260223 x.x.x.x 54192 typ host\r\n', + 'a=ice-options:trickle\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:101 VP9/90000\r\n', + 'a=rtpmap:97 rtx/90000\r\n', + 'a=rtpmap:100 VP8/90000\r\n', + 'a=rtpmap:96 rtx/90000\r\n', + 'a=rtpmap:107 H264/90000\r\n', + 'a=rtpmap:99 rtx/90000\r\n', + 'a=rtpmap:41 AV1/90000\r\n', + 'a=rtpmap:42 rtx/90000\r\n', + 'a=fmtp:101 profile-id=0\r\n', + 'a=fmtp:97 apt=101\r\n', + 'a=fmtp:96 apt=100\r\n', + 'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n', + 'a=fmtp:99 apt=107\r\n', + 'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n', + 'a=fmtp:42 apt=41\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:101 ccm fir\r\n', + 'a=rtcp-fb:101 nack\r\n', + 'a=rtcp-fb:101 nack pli\r\n', + 'a=rtcp-fb:101 transport-cc\r\n', + 'a=rtcp-fb:97 ccm fir\r\n', + 'a=rtcp-fb:97 nack\r\n', + 'a=rtcp-fb:97 nack pli\r\n', + 'a=rtcp-fb:100 ccm fir\r\n', + 'a=rtcp-fb:100 nack\r\n', + 'a=rtcp-fb:100 nack pli\r\n', + 'a=rtcp-fb:100 transport-cc\r\n', + 'a=rtcp-fb:96 ccm fir\r\n', + 'a=rtcp-fb:96 nack\r\n', + 'a=rtcp-fb:96 nack pli\r\n', + 'a=rtcp-fb:107 ccm fir\r\n', + 'a=rtcp-fb:107 nack\r\n', + 'a=rtcp-fb:107 nack pli\r\n', + 'a=rtcp-fb:107 transport-cc\r\n', + 'a=rtcp-fb:41 ccm fir\r\n', + 'a=rtcp-fb:41 nack\r\n', + 'a=rtcp-fb:41 nack pli\r\n', + 'a=rtcp-fb:41 transport-cc\r\n', + 'a=rtcp-fb:42 ccm fir\r\n', + 'a=rtcp-fb:42 nack\r\n', + 'a=rtcp-fb:42 nack pli\r\n', + 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:1\r\n', + 'a=recvonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=setup:active\r\n', + 'a=mid:2\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=sctp-port:5000\r\n', + 'a=max-message-size:262144\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:101 VP9/90000\r\n', + 'a=rtpmap:97 rtx/90000\r\n', + 'a=rtpmap:100 VP8/90000\r\n', + 'a=rtpmap:96 rtx/90000\r\n', + 'a=rtpmap:107 H264/90000\r\n', + 'a=rtpmap:99 rtx/90000\r\n', + 'a=rtpmap:41 AV1/90000\r\n', + 'a=rtpmap:42 rtx/90000\r\n', + 'a=fmtp:101 profile-id=0\r\n', + 'a=fmtp:97 apt=101\r\n', + 'a=fmtp:96 apt=100\r\n', + 'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n', + 'a=fmtp:99 apt=107\r\n', + 'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n', + 'a=fmtp:42 apt=41\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:101 ccm fir\r\n', + 'a=rtcp-fb:101 nack\r\n', + 'a=rtcp-fb:101 nack pli\r\n', + 'a=rtcp-fb:101 transport-cc\r\n', + 'a=rtcp-fb:97 ccm fir\r\n', + 'a=rtcp-fb:97 nack\r\n', + 'a=rtcp-fb:97 nack pli\r\n', + 'a=rtcp-fb:100 ccm fir\r\n', + 'a=rtcp-fb:100 nack\r\n', + 'a=rtcp-fb:100 nack pli\r\n', + 'a=rtcp-fb:100 transport-cc\r\n', + 'a=rtcp-fb:96 ccm fir\r\n', + 'a=rtcp-fb:96 nack\r\n', + 'a=rtcp-fb:96 nack pli\r\n', + 'a=rtcp-fb:107 ccm fir\r\n', + 'a=rtcp-fb:107 nack\r\n', + 'a=rtcp-fb:107 nack pli\r\n', + 'a=rtcp-fb:107 transport-cc\r\n', + 'a=rtcp-fb:41 ccm fir\r\n', + 'a=rtcp-fb:41 nack\r\n', + 'a=rtcp-fb:41 nack pli\r\n', + 'a=rtcp-fb:41 transport-cc\r\n', + 'a=rtcp-fb:42 ccm fir\r\n', + 'a=rtcp-fb:42 nack\r\n', + 'a=rtcp-fb:42 nack pli\r\n', + 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=extmap:11 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension\r\n', + 'a=setup:active\r\n', + 'a=mid:3\r\n', + 'a=recvonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:101 VP9/90000\r\n', + 'a=rtpmap:97 rtx/90000\r\n', + 'a=rtpmap:100 VP8/90000\r\n', + 'a=rtpmap:96 rtx/90000\r\n', + 'a=rtpmap:107 H264/90000\r\n', + 'a=rtpmap:99 rtx/90000\r\n', + 'a=rtpmap:41 AV1/90000\r\n', + 'a=rtpmap:42 rtx/90000\r\n', + 'a=fmtp:101 profile-id=0\r\n', + 'a=fmtp:97 apt=101\r\n', + 'a=fmtp:96 apt=100\r\n', + 'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n', + 'a=fmtp:99 apt=107\r\n', + 'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n', + 'a=fmtp:42 apt=41\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:101 ccm fir\r\n', + 'a=rtcp-fb:101 nack\r\n', + 'a=rtcp-fb:101 nack pli\r\n', + 'a=rtcp-fb:101 transport-cc\r\n', + 'a=rtcp-fb:97 ccm fir\r\n', + 'a=rtcp-fb:97 nack\r\n', + 'a=rtcp-fb:97 nack pli\r\n', + 'a=rtcp-fb:100 ccm fir\r\n', + 'a=rtcp-fb:100 nack\r\n', + 'a=rtcp-fb:100 nack pli\r\n', + 'a=rtcp-fb:100 transport-cc\r\n', + 'a=rtcp-fb:96 ccm fir\r\n', + 'a=rtcp-fb:96 nack\r\n', + 'a=rtcp-fb:96 nack pli\r\n', + 'a=rtcp-fb:107 ccm fir\r\n', + 'a=rtcp-fb:107 nack\r\n', + 'a=rtcp-fb:107 nack pli\r\n', + 'a=rtcp-fb:107 transport-cc\r\n', + 'a=rtcp-fb:41 ccm fir\r\n', + 'a=rtcp-fb:41 nack\r\n', + 'a=rtcp-fb:41 nack pli\r\n', + 'a=rtcp-fb:41 transport-cc\r\n', + 'a=rtcp-fb:42 ccm fir\r\n', + 'a=rtcp-fb:42 nack\r\n', + 'a=rtcp-fb:42 nack pli\r\n', + 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=extmap:11 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension\r\n', + 'a=setup:active\r\n', + 'a=mid:4\r\n', + 'a=recvonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:111 opus/48000/2\r\n', + 'a=rtpmap:126 telephone-event/8000\r\n', + 'a=fmtp:111 minptime=10;useinbandfec=1\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:111 transport-cc\r\n', + 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:5\r\n', + 'a=recvonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:111 opus/48000/2\r\n', + 'a=rtpmap:126 telephone-event/8000\r\n', + 'a=fmtp:111 minptime=10;useinbandfec=1\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:111 transport-cc\r\n', + 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:6\r\n', + 'a=recvonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 101 97 100 96 107 99 41 42\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=rtpmap:101 VP9/90000\r\n', + 'a=rtpmap:97 rtx/90000\r\n', + 'a=rtpmap:100 VP8/90000\r\n', + 'a=rtpmap:96 rtx/90000\r\n', + 'a=rtpmap:107 H264/90000\r\n', + 'a=rtpmap:99 rtx/90000\r\n', + 'a=rtpmap:41 AV1/90000\r\n', + 'a=rtpmap:42 rtx/90000\r\n', + 'a=fmtp:101 profile-id=0\r\n', + 'a=fmtp:97 apt=101\r\n', + 'a=fmtp:96 apt=100\r\n', + 'a=fmtp:107 ;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n', + 'a=fmtp:99 apt=107\r\n', + 'a=fmtp:41 level-idx=5;profile=0;tier=0\r\n', + 'a=fmtp:42 apt=41\r\n', + 'a=rtcp:9 IN IP4 0.0.0.0\r\n', + 'a=rtcp-fb:101 ccm fir\r\n', + 'a=rtcp-fb:101 nack\r\n', + 'a=rtcp-fb:101 nack pli\r\n', + 'a=rtcp-fb:101 transport-cc\r\n', + 'a=rtcp-fb:97 ccm fir\r\n', + 'a=rtcp-fb:97 nack\r\n', + 'a=rtcp-fb:97 nack pli\r\n', + 'a=rtcp-fb:100 ccm fir\r\n', + 'a=rtcp-fb:100 nack\r\n', + 'a=rtcp-fb:100 nack pli\r\n', + 'a=rtcp-fb:100 transport-cc\r\n', + 'a=rtcp-fb:96 ccm fir\r\n', + 'a=rtcp-fb:96 nack\r\n', + 'a=rtcp-fb:96 nack pli\r\n', + 'a=rtcp-fb:107 ccm fir\r\n', + 'a=rtcp-fb:107 nack\r\n', + 'a=rtcp-fb:107 nack pli\r\n', + 'a=rtcp-fb:107 transport-cc\r\n', + 'a=rtcp-fb:41 ccm fir\r\n', + 'a=rtcp-fb:41 nack\r\n', + 'a=rtcp-fb:41 nack pli\r\n', + 'a=rtcp-fb:41 transport-cc\r\n', + 'a=rtcp-fb:42 ccm fir\r\n', + 'a=rtcp-fb:42 nack\r\n', + 'a=rtcp-fb:42 nack pli\r\n', + 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n', + 'a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n', + 'a=setup:active\r\n', + 'a=mid:7\r\n', + 'a=recvonly\r\n', + 'a=ice-ufrag:/5Yo\r\n', + 'a=ice-pwd:Bn+13yvssP5vicDc0mUO7Aiu\r\n', + 'a=fingerprint:sha-256 54:99:99:D2:C9:FE:63:B2:12:A5:D6:BA:BD:FA:F0:46:7E:E4:18:F8:9C:DF:25:55:94:DA:21:AE:19:19:56:AB\r\n', + 'a=ice-options:trickle\r\n', + 'a=rtcp-mux\r\n', + 'a=extmap-allow-mixed\r\n' + ].join(''); + + it('correctly groups ssrcs lines', () => { + const sdp = new SDP(testSdp); + const accept = $iq({ + to: 'peerjid', + type: 'set' + }) + .c('jingle', { + xmlns: 'urn:xmpp:jingle:1', + action: 'session-accept', + initiator: false, + responder: true, + sid: 'temp-sid' + }); + + sdp.toJingle(accept, 'responder'); + const { nodeTree } = accept; + const content = nodeTree.querySelectorAll('jingle>content'); + + expect(content.length).toBe(3); + const videoSources = nodeTree.querySelectorAll('description[media=\'video\']>source'); + + expect(videoSources.length).toBe(0); + const audioSources = nodeTree.querySelectorAll('description[media=\'audio\']>source'); + + expect(audioSources.length).toBe(0); + const videoSourceGroups = nodeTree.querySelectorAll('description[media=\'video\']>ssrc-group'); + + expect(videoSourceGroups.length).toBe(0); + const data = nodeTree.querySelectorAll('jingle>content[name=\'data\']'); + + expect(data.length).toBe(1); + }); + }); + describe('fromJingle', () => { - /* eslint-disable max-len*/ const stanza = ` @@ -269,7 +1130,6 @@ a=ssrc:3758540092 label:mixedlabelvideo0 a=ssrc:3758540092 msid:mixedmslabel mixedlabelvideo0 a=ssrc:3758540092 mslabel:mixedmslabel `.split('\n').join('\r\n'); - /* eslint-enable max-len*/ it('gets converted to SDP', () => { const offer = createStanzaElement(stanza); @@ -283,7 +1143,6 @@ a=ssrc:3758540092 mslabel:mixedmslabel }); describe('fromJingleWithJSONFormat', () => { - /* eslint-disable max-len*/ const stanza = ` diff --git a/modules/sdp/SDPDiffer.js b/modules/sdp/SDPDiffer.js index 0abec4597b..db9cffa4c4 100644 --- a/modules/sdp/SDPDiffer.js +++ b/modules/sdp/SDPDiffer.js @@ -1,220 +1,121 @@ +import { isEqual } from 'lodash-es'; import { XEP } from '../../service/xmpp/XMPPExtensioProtocols'; import SDPUtil from './SDPUtil'; -// this could be useful in Array.prototype. /** - * - * @param array1 - * @param array2 + * A class that provides methods for comparing the source information present in two different SDPs so that the delta + * can be signaled to Jicofo via 'source-remove' or 'source-add'. */ -function arrayEquals(array1, array2) { - // if the other array is a falsy value, return - if (!array2) { - return false; +export class SDPDiffer { + /** + * Constructor. + * + * @param {SDP} mySdp - the new SDP. + * @param {SDP} othersSdp - the old SDP. + * @param {boolean} isP2P - Whether the SDPs belong to a p2p peerconnection. + */ + constructor(mySdp, othersSdp, isP2P = false) { + this.isP2P = isP2P; + this.mySdp = mySdp; + this.othersSdp = othersSdp; } - // compare lengths - can save a lot of time - if (array1.length !== array2.length) { - return false; - } + /** + * Returns a map of the sources that are present in 'othersSdp' but not in 'mySdp'. + * + * @returns {*} + */ + getNewMedia() { + const mySources = this.mySdp.getMediaSsrcMap(); + const othersSources = this.othersSdp.getMediaSsrcMap(); + const diff = {}; + + for (const [ index, othersSource ] of othersSources.entries()) { + const mySource = mySources.get(index); + + if (!mySource) { + diff[index] = othersSource; + continue; // eslint-disable-line no-continue + } + + const othersSsrcs = Object.keys(othersSource.ssrcs); - for (let i = 0, l = array1.length; i < l; i++) { - // Check if we have nested arrays - if (array1[i] instanceof Array && array2[i] instanceof Array) { - // recurse into the nested arrays - if (!array1[i].equals(array2[i])) { - return false; + if (othersSsrcs.length && !isEqual(Object.keys(mySource.ssrcs).sort(), [ ...othersSsrcs ].sort())) { + diff[index] = othersSource; } - } else if (array1[i] !== array2[i]) { - // Warning - two different object instances will never be - // equal: {x:20} != {x:20} - return false; } - } - - return true; -} -/** - * - * @param mySDP - * @param otherSDP - */ -export default function SDPDiffer(mySDP, otherSDP) { - this.mySDP = mySDP; - this.otherSDP = otherSDP; - if (!mySDP) { - throw new Error('"mySDP" is undefined!'); - } else if (!otherSDP) { - throw new Error('"otherSDP" is undefined!'); + return diff; } -} -/** - * Returns map of MediaChannel that contains media contained in - * 'mySDP', but not contained in 'otherSdp'. Mapped by channel idx. - */ -SDPDiffer.prototype.getNewMedia = function() { - - const myMedias = this.mySDP.getMediaSsrcMap(); - const othersMedias = this.otherSDP.getMediaSsrcMap(); - const newMedia = {}; - - Object.keys(othersMedias).forEach(othersMediaIdx => { - const myMedia = myMedias[othersMediaIdx]; - const othersMedia = othersMedias[othersMediaIdx]; + /** + * Adds the diff source info to the provided IQ stanza. + * + * @param {*} modify - Stanza IQ. + * @returns {boolean} + */ + toJingle(modify) { + let modified = false; + const diffSourceInfo = this.getNewMedia(); + + for (const media of Object.values(diffSourceInfo)) { + modified = true; + modify.c('content', { name: this.isP2P ? media.mid : media.mediaType }); + + modify.c('description', { + xmlns: XEP.RTP_MEDIA, + media: media.mediaType + }); - if (!myMedia && othersMedia) { - // Add whole channel - newMedia[othersMediaIdx] = othersMedia; + Object.keys(media.ssrcs).forEach(ssrcNum => { + const mediaSsrc = media.ssrcs[ssrcNum]; + const ssrcLines = mediaSsrc.lines; + const sourceName = SDPUtil.parseSourceNameLine(ssrcLines); + const videoType = SDPUtil.parseVideoTypeLine(ssrcLines); + + modify.c('source', { xmlns: XEP.SOURCE_ATTRIBUTES }); + modify.attrs({ + name: sourceName, + videoType, + ssrc: mediaSsrc.ssrc + }); - return; - } + // Only MSID attribute is sent + const msid = SDPUtil.parseMSIDAttribute(ssrcLines); - // Look for new ssrcs across the channel - Object.keys(othersMedia.ssrcs).forEach(ssrc => { - if (Object.keys(myMedia.ssrcs).indexOf(ssrc) === -1) { - // Allocate channel if we've found ssrc that doesn't exist in - // our channel - if (!newMedia[othersMediaIdx]) { - newMedia[othersMediaIdx] = { - mediaindex: othersMedia.mediaindex, - mid: othersMedia.mid, - ssrcs: {}, - ssrcGroups: [] - }; + if (msid) { + modify.c('parameter'); + modify.attrs({ name: 'msid' }); + modify.attrs({ value: msid }); + modify.up(); } - newMedia[othersMediaIdx].ssrcs[ssrc] = othersMedia.ssrcs[ssrc]; - } else if (othersMedia.ssrcs[ssrc].lines - && myMedia.ssrcs[ssrc].lines) { - // we want to detect just changes in adding/removing msid - const myContainMsid = myMedia.ssrcs[ssrc].lines.find( - line => line.indexOf('msid') !== -1) !== undefined; - const newContainMsid = othersMedia.ssrcs[ssrc].lines.find( - line => line.indexOf('msid') !== -1) !== undefined; - - if (myContainMsid !== newContainMsid) { - if (!newMedia[othersMediaIdx]) { - newMedia[othersMediaIdx] = { - mediaindex: othersMedia.mediaindex, - mid: othersMedia.mid, - ssrcs: {}, - ssrcGroups: [] - }; - } - newMedia[othersMediaIdx].ssrcs[ssrc] - = othersMedia.ssrcs[ssrc]; - } - } - }); - - // Look for new ssrc groups across the channels - othersMedia.ssrcGroups.forEach(otherSsrcGroup => { - // try to match the other ssrc-group with an ssrc-group of ours - let matched = false; + modify.up(); // end of source + }); - for (let i = 0; i < myMedia.ssrcGroups.length; i++) { - const mySsrcGroup = myMedia.ssrcGroups[i]; + // generate source groups from lines + media.ssrcGroups.forEach(ssrcGroup => { + if (ssrcGroup.ssrcs.length) { - if (otherSsrcGroup.semantics === mySsrcGroup.semantics - && arrayEquals(otherSsrcGroup.ssrcs, mySsrcGroup.ssrcs)) { + modify.c('ssrc-group', { + semantics: ssrcGroup.semantics, + xmlns: XEP.SOURCE_ATTRIBUTES + }); - matched = true; - break; + ssrcGroup.ssrcs.forEach(ssrc => { + modify.c('source', { ssrc }) + .up(); // end of source + }); + modify.up(); // end of ssrc-group } - } - - if (!matched) { - // Allocate channel if we've found an ssrc-group that doesn't - // exist in our channel - - if (!newMedia[othersMediaIdx]) { - newMedia[othersMediaIdx] = { - mediaindex: othersMedia.mediaindex, - mid: othersMedia.mid, - ssrcs: {}, - ssrcGroups: [] - }; - } - newMedia[othersMediaIdx].ssrcGroups.push(otherSsrcGroup); - } - }); - }); - - return newMedia; -}; - -/** - * TODO: document! - */ -SDPDiffer.prototype.toJingle = function(modify) { - const sdpMediaSsrcs = this.getNewMedia(); - - let modified = false; - - Object.keys(sdpMediaSsrcs).forEach(mediaindex => { - modified = true; - const media = sdpMediaSsrcs[mediaindex]; - - modify.c('content', { name: media.mid }); - - modify.c('description', { - xmlns: XEP.RTP_MEDIA, - media: media.mid - }); - - // FIXME: not completely sure this operates on blocks and / or handles - // different ssrcs correctly - // generate sources from lines - Object.keys(media.ssrcs).forEach(ssrcNum => { - const mediaSsrc = media.ssrcs[ssrcNum]; - const ssrcLines = mediaSsrc.lines; - const sourceName = SDPUtil.parseSourceNameLine(ssrcLines); - const videoType = SDPUtil.parseVideoTypeLine(ssrcLines); - - modify.c('source', { xmlns: XEP.SOURCE_ATTRIBUTES }); - modify.attrs({ - name: sourceName, - videoType, - ssrc: mediaSsrc.ssrc }); - // Only MSID attribute is sent - const msid = SDPUtil.parseMSIDAttribute(ssrcLines); - - if (msid) { - modify.c('parameter'); - modify.attrs({ name: 'msid' }); - modify.attrs({ value: msid }); - modify.up(); - } - - modify.up(); // end of source - }); - - // generate source groups from lines - media.ssrcGroups.forEach(ssrcGroup => { - if (ssrcGroup.ssrcs.length) { - - modify.c('ssrc-group', { - semantics: ssrcGroup.semantics, - xmlns: XEP.SOURCE_ATTRIBUTES - }); - - ssrcGroup.ssrcs.forEach(ssrc => { - modify.c('source', { ssrc }) - .up(); // end of source - }); - modify.up(); // end of ssrc-group - } - }); - - modify.up(); // end of description - modify.up(); // end of content - }); + modify.up(); // end of description + modify.up(); // end of content + } - return modified; -}; + return modified; + } +} diff --git a/modules/sdp/SDPDiffer.spec.js b/modules/sdp/SDPDiffer.spec.js index 8d7961a2d1..13821ea566 100644 --- a/modules/sdp/SDPDiffer.spec.js +++ b/modules/sdp/SDPDiffer.spec.js @@ -3,54 +3,58 @@ import { $iq } from 'strophe.js'; import FeatureFlags from '../flags/FeatureFlags'; import SDP from './SDP'; -import SDPDiffer from './SDPDiffer'; +import { SDPDiffer } from './SDPDiffer'; +import SampleSdpStrings from './SampleSdpStrings'; + +/* eslint-disable max-len*/ describe('SDPDiffer', () => { beforeEach(() => { FeatureFlags.init({ }); }); describe('toJingle', () => { - /* eslint-disable max-len*/ - const testSdpOld = [ - 'v=0\r\n', - 'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n', - 's=-\r\n', - 't=0 0\r\n', - 'a=group:BUNDLE audio video\r\n', - 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', - 'a=mid:audio\r\n', - 'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n', - 'a=ssrc:2002 cname:juejgy8a01\r\n', - 'a=ssrc:2002 name:a8f7g30-a0\r\n', - 'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n', - 'a=mid:video\r\n' - ].join(''); - const testSdpNew = [ - 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', - 'a=mid:audio\r\n', - 'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n', - 'a=mid:video\r\n', - 'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', - 'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', - 'a=ssrc:4004 cname:juejgy8a01\r\n', - 'a=ssrc:4005 cname:juejgy8a01\r\n', - 'a=ssrc:4004 name:a8f7g30-v0\r\n', - 'a=ssrc:4005 name:a8f7g30-v0\r\n', - 'a=ssrc-group:FID 4004 4005\r\n' - ].join(''); - /* eslint-enable max-len*/ - it('should include source names in added/removed sources', () => { FeatureFlags.init({ }); + const testSdpOld = [ + 'v=0\r\n', + 'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n', + 's=-\r\n', + 't=0 0\r\n', + 'a=group:BUNDLE audio video\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'a=mid:audio\r\n', + 'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n', + 'a=ssrc:2002 cname:juejgy8a01\r\n', + 'a=ssrc:2002 name:a8f7g30-a0\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n', + 'a=mid:video\r\n' + ].join(''); + const testSdpNew = [ + 'v=0\r\n', + 'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n', + 's=-\r\n', + 't=0 0\r\n', + 'a=group:BUNDLE audio video\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'a=mid:audio\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n', + 'a=mid:video\r\n', + 'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4004 cname:juejgy8a01\r\n', + 'a=ssrc:4005 cname:juejgy8a01\r\n', + 'a=ssrc:4004 name:a8f7g30-v0\r\n', + 'a=ssrc:4005 name:a8f7g30-v0\r\n', + 'a=ssrc-group:FID 4004 4005\r\n' + ].join(''); const newToOldDiff = new SDPDiffer(new SDP(testSdpNew), new SDP(testSdpOld)); const sourceRemoveIq = $iq({}) .c('jingle', { action: 'source-remove' }); newToOldDiff.toJingle(sourceRemoveIq); - const removedAudioSources = sourceRemoveIq.nodeTree - .querySelectorAll('description[media=\'audio\']>source'); + const removedAudioSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'audio\']>source'); expect(removedAudioSources[0].getAttribute('name')).toBe('a8f7g30-a0'); @@ -60,12 +64,266 @@ describe('SDPDiffer', () => { oldToNewDiff.toJingle(sourceAddIq); - const addedVideoSources = sourceAddIq.nodeTree - .querySelectorAll('description[media=\'video\']>source'); + const addedVideoSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>source'); + const addedVideoSourceGroups = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>ssrc-group'); expect(addedVideoSources.length).toBe(2); expect(addedVideoSources[0].getAttribute('name')).toBe('a8f7g30-v0'); expect(addedVideoSources[1].getAttribute('name')).toBe('a8f7g30-v0'); + expect(addedVideoSourceGroups.length).toBe(1); + }); + + it('should send source-remove/source-add when ssrc changes', () => { + FeatureFlags.init({ }); + + const testSdpOld = [ + 'v=0\r\n', + 'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n', + 's=-\r\n', + 't=0 0\r\n', + 'a=group:BUNDLE audio video\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'a=mid:audio\r\n', + 'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n', + 'a=ssrc:2002 cname:juejgy8a01\r\n', + 'a=ssrc:2002 name:a8f7g30-a0\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n', + 'a=mid:video\r\n', + 'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4004 cname:juejgy8a01\r\n', + 'a=ssrc:4005 cname:juejgy8a01\r\n', + 'a=ssrc:4004 name:a8f7g30-v0\r\n', + 'a=ssrc:4005 name:a8f7g30-v0\r\n', + 'a=ssrc-group:FID 4004 4005\r\n' + ].join(''); + const testSdpNew = [ + 'v=0\r\n', + 'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n', + 's=-\r\n', + 't=0 0\r\n', + 'a=group:BUNDLE audio video\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'a=mid:audio\r\n', + 'a=ssrc:2003 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n', + 'a=ssrc:2003 cname:juejgy8a01\r\n', + 'a=ssrc:2003 name:a8f7g30-a0\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n', + 'a=mid:video\r\n', + 'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4004 cname:juejgy8a01\r\n', + 'a=ssrc:4005 cname:juejgy8a01\r\n', + 'a=ssrc:4004 name:a8f7g30-v0\r\n', + 'a=ssrc:4005 name:a8f7g30-v0\r\n', + 'a=ssrc-group:FID 4004 4005\r\n' + ].join(''); + const newToOldDiff = new SDPDiffer(new SDP(testSdpNew), new SDP(testSdpOld)); + const sourceRemoveIq = $iq({}) + .c('jingle', { action: 'source-remove' }); + + newToOldDiff.toJingle(sourceRemoveIq); + + const removedAudioSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'audio\']>source'); + const removedVideoSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'video\']>source'); + + expect(removedAudioSources.length).toBe(1); + expect(removedAudioSources[0].getAttribute('name')).toBe('a8f7g30-a0'); + expect(removedAudioSources[0].getAttribute('ssrc')).toBe('2002'); + expect(removedVideoSources.length).toBe(0); + + const oldToNewDiff = new SDPDiffer(new SDP(testSdpOld), new SDP(testSdpNew)); + const sourceAddIq = $iq({}) + .c('jingle', { action: 'source-add' }); + + oldToNewDiff.toJingle(sourceAddIq); + + const addedAudioSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'audio\']>source'); + const addedVideoSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>source'); + + expect(addedAudioSources.length).toBe(1); + expect(addedAudioSources[0].getAttribute('name')).toBe('a8f7g30-a0'); + expect(addedAudioSources[0].getAttribute('ssrc')).toBe('2003'); + expect(addedVideoSources.length).toBe(0); + }); + + it('should not send source-remove/source-add when nothing changes', () => { + FeatureFlags.init({ }); + + const testSdpOld = [ + 'v=0\r\n', + 'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n', + 's=-\r\n', + 't=0 0\r\n', + 'a=group:BUNDLE audio video\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'a=mid:audio\r\n', + 'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n', + 'a=ssrc:2002 cname:juejgy8a01\r\n', + 'a=ssrc:2002 name:a8f7g30-a0\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n', + 'a=mid:video\r\n', + 'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4004 cname:juejgy8a01\r\n', + 'a=ssrc:4005 cname:juejgy8a01\r\n', + 'a=ssrc:4004 name:a8f7g30-v0\r\n', + 'a=ssrc:4005 name:a8f7g30-v0\r\n', + 'a=ssrc-group:FID 4004 4005\r\n' + ].join(''); + const testSdpNew = [ + 'v=0\r\n', + 'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n', + 's=-\r\n', + 't=0 0\r\n', + 'a=group:BUNDLE audio video\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'a=mid:audio\r\n', + 'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n', + 'a=ssrc:2002 cname:juejgy8a01\r\n', + 'a=ssrc:2002 name:a8f7g30-a0\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n', + 'a=mid:video\r\n', + 'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4004 cname:juejgy8a01\r\n', + 'a=ssrc:4005 cname:juejgy8a01\r\n', + 'a=ssrc:4004 name:a8f7g30-v0\r\n', + 'a=ssrc:4005 name:a8f7g30-v0\r\n', + 'a=ssrc-group:FID 4004 4005\r\n' + ].join(''); + const newToOldDiff = new SDPDiffer(new SDP(testSdpNew), new SDP(testSdpOld)); + const sourceRemoveIq = $iq({}) + .c('jingle', { action: 'source-remove' }); + + newToOldDiff.toJingle(sourceRemoveIq); + + const removedAudioSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'audio\']>source'); + const removedVideoSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'video\']>source'); + + expect(removedAudioSources.length).toBe(0); + expect(removedVideoSources.length).toBe(0); + + const oldToNewDiff = new SDPDiffer(new SDP(testSdpOld), new SDP(testSdpNew)); + const sourceAddIq = $iq({}) + .c('jingle', { action: 'source-add' }); + + oldToNewDiff.toJingle(sourceAddIq); + + const addedAudioSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'audio\']>source'); + const addedVideoSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>source'); + + expect(addedAudioSources.length).toBe(0); + expect(addedVideoSources.length).toBe(0); + }); + + it('should send source-adds for 2 sources', () => { + FeatureFlags.init({ }); + + const testSdpOld = [ + 'v=0\r\n', + 'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n', + 's=-\r\n', + 't=0 0\r\n', + 'a=group:BUNDLE audio video\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'a=mid:audio\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n', + 'a=mid:video\r\n' + ].join(''); + const testSdpNew = [ + 'v=0\r\n', + 'o=thisisadapterortc 2719486166053431 0 IN IP4 127.0.0.1\r\n', + 's=-\r\n', + 't=0 0\r\n', + 'a=group:BUNDLE audio video\r\n', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111 126\r\n', + 'a=mid:audio\r\n', + 'a=ssrc:2002 msid:26D16D51-503A-420B-8274-3DD1174E498F 8205D1FC-50B4-407C-87D5-9C45F1B779F0\r\n', + 'a=ssrc:2002 cname:juejgy8a01\r\n', + 'a=ssrc:2002 name:a8f7g30-a0\r\n', + 'm=video 9 UDP/TLS/RTP/SAVPF 107 100 99 96\r\n', + 'a=mid:video\r\n', + 'a=ssrc:4004 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4005 msid:7C0035E5-2DA1-4AEA-804A-9E75BF9B3768 225E9CDA-0384-4C92-92DD-E74C1153EC68\r\n', + 'a=ssrc:4004 cname:juejgy8a01\r\n', + 'a=ssrc:4005 cname:juejgy8a01\r\n', + 'a=ssrc:4004 name:a8f7g30-v0\r\n', + 'a=ssrc:4005 name:a8f7g30-v0\r\n', + 'a=ssrc-group:FID 4004 4005\r\n' + ].join(''); + const newToOldDiff = new SDPDiffer(new SDP(testSdpNew), new SDP(testSdpOld)); + const sourceRemoveIq = $iq({}) + .c('jingle', { action: 'source-remove' }); + + newToOldDiff.toJingle(sourceRemoveIq); + + const removedAudioSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'audio\']>source'); + const removedVideoSources = sourceRemoveIq.nodeTree.querySelectorAll('description[media=\'video\']>source'); + + expect(removedAudioSources.length).toBe(0); + expect(removedVideoSources.length).toBe(0); + + const oldToNewDiff = new SDPDiffer(new SDP(testSdpOld), new SDP(testSdpNew)); + const sourceAddIq = $iq({}) + .c('jingle', { action: 'source-add' }); + + oldToNewDiff.toJingle(sourceAddIq); + + const addedAudioSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'audio\']>source'); + const addedVideoSources = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>source'); + const addedVideoSourceGroups = sourceAddIq.nodeTree.querySelectorAll('description[media=\'video\']>ssrc-group'); + + expect(addedAudioSources.length).toBe(1); + expect(addedVideoSources.length).toBe(2); + expect(addedVideoSourceGroups.length).toBe(1); + }); + }); + + describe('getNewMedia', () => { + it(' should generate sources for source-remove when SSCRs are missing from the new SDP', () => { + const oldSdp = new SDP(SampleSdpStrings.simulcastSdpStr); + const newSdp = new SDP(SampleSdpStrings.recvOnlySdpStrChrome); + + let sdpDiffer = new SDPDiffer(newSdp, oldSdp, false); + let diff = sdpDiffer.getNewMedia(); + + // There should be 2 sources for source-remove. + expect(Object.keys(diff).length).toBe(2); + + sdpDiffer = new SDPDiffer(oldSdp, newSdp, false); + diff = sdpDiffer.getNewMedia(); + + // There should zero sources for source-add. + expect(Object.keys(diff).length).toBe(0); + }); + + it(' should not generate sources for source-remove or source-add if the SDP does not change', () => { + const oldSdp = new SDP(SampleSdpStrings.simulcastSdpStr); + const newSdp = new SDP(SampleSdpStrings.simulcastSdpStr); + + const sdpDiffer = new SDPDiffer(newSdp, oldSdp, false); + const diff = sdpDiffer.getNewMedia(); + + // There should be zero sources in diff. + expect(Object.keys(diff).length).toBe(0); + }); + + it(' should generate sources for source-remove and source-add when SSRC changes', () => { + const oldSdp = new SDP(SampleSdpStrings.simulcastSdpStr); + const newSdp = new SDP(SampleSdpStrings.simulcastDifferentSsrcSdpStr); + + let sdpDiffer = new SDPDiffer(newSdp, oldSdp, false); + let diff = sdpDiffer.getNewMedia(); + + // There should be 1 source for source-remove. + expect(Object.keys(diff).length).toBe(1); + + sdpDiffer = new SDPDiffer(oldSdp, newSdp, false); + diff = sdpDiffer.getNewMedia(); + + // There should 1 source for source-add. + expect(Object.keys(diff).length).toBe(1); }); }); }); diff --git a/modules/sdp/SDPUtil.js b/modules/sdp/SDPUtil.js index 2e8a7b4bfb..271b98b34c 100644 --- a/modules/sdp/SDPUtil.js +++ b/modules/sdp/SDPUtil.js @@ -283,6 +283,21 @@ const SDPUtil = { return data; }, + /** + * Parses the 'a=ssrc-group' line. + * + * @param {string} line - The media line to parse. + * @returns {object} + */ + parseSSRCGroupLine(line) { + const parts = line.substr(13).split(' '); + + return { + semantics: parts.shift(), + ssrcs: parts + }; + }, + /** * Gets the source name out of the name attribute "a=ssrc:254321 name:name1". * diff --git a/modules/sdp/SampleSdpStrings.js b/modules/sdp/SampleSdpStrings.js index be79c240e9..ad5ce29299 100644 --- a/modules/sdp/SampleSdpStrings.js +++ b/modules/sdp/SampleSdpStrings.js @@ -36,6 +36,31 @@ const baseAudioMLineSdp = '' + 'a=ssrc:124723944 label:40abf2d3-a415-4c68-8c17-2a038e8bebcf\r\n' + 'a=rtcp-mux\r\n'; +const baseAudioMLineSdpDifferentSSRC = '' ++ 'm=audio 54405 RTP/SAVPF 111 103 104 126\r\n' ++ 'c=IN IP4 172.29.32.39\r\n' ++ 'a=rtpmap:111 opus/48000/2\r\n' ++ 'a=rtpmap:103 ISAC/16000\r\n' ++ 'a=rtpmap:104 ISAC/32000\r\n' ++ 'a=rtpmap:126 telephone-event/8000\r\n' ++ 'a=fmtp:111 minptime=10;useinbandfec=1\r\n' ++ 'a=rtcp:9 IN IP4 0.0.0.0\r\n' ++ 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n' ++ 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' ++ 'a=setup:passive\r\n' ++ 'a=mid:audio\r\n' ++ 'a=msid:- 40abf2d3-a415-4c68-8c17-2a038e8bebcf\r\n' ++ 'a=sendrecv\r\n' ++ 'a=ice-ufrag:adPg\r\n' ++ 'a=ice-pwd:Xsr05Mq8S7CR44DAnusZE26F\r\n' ++ 'a=fingerprint:sha-256 6A:39:DE:11:24:AD:2E:4E:63:D6:69:D3:85:05:53:C7:3C:38:A4:B7:91:74:C0:91:44:FC:94:63:7F:01:AB:A9\r\n' ++ 'a=candidate:1581043602 1 udp 2122260223 172.29.32.39 54405 typ host generation 0\r\n' ++ 'a=ssrc:1757014965 cname:peDGrDD6WsxUOki/\r\n' ++ 'a=ssrc:1757014965 msid:dcbb0236-cea5-402e-9e9a-595c65ffcc2a 40abf2d3-a415-4c68-8c17-2a038e8bebcf\r\n' ++ 'a=ssrc:1757014965 mslabel:dcbb0236-cea5-402e-9e9a-595c65ffcc2a\r\n' ++ 'a=ssrc:1757014965 label:40abf2d3-a415-4c68-8c17-2a038e8bebcf\r\n' ++ 'a=rtcp-mux\r\n'; + // A basic sdp application mline const baseDataMLineSdp = '' + 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n' @@ -324,6 +349,26 @@ const recvOnlyAudioMline = '' + 'a=ssrc:124723944 cname:peDGrDD6WsxUOki\r\n' + 'a=rtcp-mux\r\n'; +const recvOnlyAudioMlineChrome = '' ++ 'm=audio 54405 RTP/SAVPF 111 103 104 126\r\n' ++ 'c=IN IP4 172.29.32.39\r\n' ++ 'a=rtpmap:111 opus/48000/2\r\n' ++ 'a=rtpmap:103 ISAC/16000\r\n' ++ 'a=rtpmap:104 ISAC/32000\r\n' ++ 'a=rtpmap:126 telephone-event/8000\r\n' ++ 'a=fmtp:111 minptime=10;useinbandfec=1\r\n' ++ 'a=rtcp:9 IN IP4 0.0.0.0\r\n' ++ 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n' ++ 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' ++ 'a=setup:passive\r\n' ++ 'a=mid:audio\r\n' ++ 'a=recvonly\r\n' ++ 'a=ice-ufrag:adPg\r\n' ++ 'a=ice-pwd:Xsr05Mq8S7CR44DAnusZE26F\r\n' ++ 'a=fingerprint:sha-256 6A:39:DE:11:24:AD:2E:4E:63:D6:69:D3:85:05:53:C7:3C:38:A4:B7:91:74:C0:91:44:FC:94:63:7F:01:AB:A9\r\n' ++ 'a=candidate:1581043602 1 udp 2122260223 172.29.32.39 54405 typ host generation 0\r\n' ++ 'a=rtcp-mux\r\n'; + const recvOnlyVideoMline = '' + 'm=video 9 RTP/SAVPF 96 97 98 99 102 121 127 120\r\n' + 'c=IN IP4 0.0.0.0\r\n' @@ -372,6 +417,53 @@ const recvOnlyVideoMline = '' + 'a=ssrc:1757014965 cname:peDGrDD6WsxUOki\r\n' + 'a=rtcp-mux\r\n'; +const recvOnlyVideoMlineChrome = '' ++ 'm=video 9 RTP/SAVPF 96 97 98 99 102 121 127 120\r\n' ++ 'c=IN IP4 0.0.0.0\r\n' ++ 'a=rtpmap:96 VP8/90000\r\n' ++ 'a=rtpmap:97 rtx/90000\r\n' ++ 'a=rtpmap:98 VP9/90000\r\n' ++ 'a=rtpmap:99 rtx/90000\r\n' ++ 'a=rtpmap:102 H264/90000\r\n' ++ 'a=rtpmap:121 rtx/90000\r\n' ++ 'a=rtpmap:127 H264/90000\r\n' ++ 'a=rtpmap:120 rtx/90000\r\n' ++ 'a=rtcp:9 IN IP4 0.0.0.0\r\n' ++ 'a=rtcp-fb:96 ccm fir\r\n' ++ 'a=rtcp-fb:96 transport-cc\r\n' ++ 'a=rtcp-fb:96 nack\r\n' ++ 'a=rtcp-fb:96 nack pli\r\n' ++ 'a=rtcp-fb:96 goog-remb\r\n' ++ 'a=rtcp-fb:98 ccm fir\r\n' ++ 'a=rtcp-fb:98 transport-cc\r\n' ++ 'a=rtcp-fb:98 nack\r\n' ++ 'a=rtcp-fb:98 nack pli\r\n' ++ 'a=rtcp-fb:98 goog-remb\r\n' ++ 'a=rtcp-fb:102 ccm fir\r\n' ++ 'a=rtcp-fb:102 transport-cc\r\n' ++ 'a=rtcp-fb:102 nack\r\n' ++ 'a=rtcp-fb:102 nack pli\r\n' ++ 'a=rtcp-fb:102 goog-remb\r\n' ++ 'a=rtcp-fb:127 ccm fir\r\n' ++ 'a=rtcp-fb:127 transport-cc\r\n' ++ 'a=rtcp-fb:127 nack\r\n' ++ 'a=rtcp-fb:127 nack pli\r\n' ++ 'a=rtcp-fb:127 goog-remb\r\n' ++ 'a=fmtp:97 apt=96\r\n' ++ 'a=fmtp:98 profile-id=0\r\n' ++ 'a=fmtp:102 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1\r\n' ++ 'a=fmtp:121 apt=102\r\n' ++ 'a=fmtp:127 profile-level-id=42e01f;level-asymmetry-allowed=1:packetization-mode=0\r\n' ++ 'a=fmtp:120 apt=127\r\n' ++ 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' ++ 'a=setup:passive\r\n' ++ 'a=mid:video\r\n' ++ 'a=recvonly\r\n' ++ 'a=ice-ufrag:adPg\r\n' ++ 'a=ice-pwd:Xsr05Mq8S7CR44DAnusZE26F\r\n' ++ 'a=fingerprint:sha-256 6A:39:DE:11:24:AD:2E:4E:63:D6:69:D3:85:05:53:C7:3C:38:A4:B7:91:74:C0:91:44:FC:94:63:7F:01:AB:A9\r\n' ++ 'a=rtcp-mux\r\n'; + const videoMlineFF = '' + 'm=video 9 RTP/SAVPF 100 96\r\n' + 'c=IN IP4 0.0.0.0\r\n' @@ -443,6 +535,8 @@ const simulcastVideoMLineNoRtxSdp = '' // A full sdp string representing a client doing simulcast const simulcastSdpStr = baseSessionSdp + baseAudioMLineSdp + simulcastVideoMLineSdp + baseDataMLineSdp; +const simulcastDifferentSsrcSdpStr = baseSessionSdp + baseAudioMLineSdpDifferentSSRC + simulcastVideoMLineSdp + baseDataMLineSdp; + // A full sdp string representing a remote client doing simucast when RTX is not negotiated with the jvb. const simulcastNoRtxSdpStr = baseSessionSdp + baseAudioMLineSdp + simulcastVideoMLineNoRtxSdp; @@ -465,7 +559,10 @@ const multiCodecVideoSdpStr = baseSessionSdp + baseAudioMLineSdp + multiCodecVid const flexFecSdpStr = baseSessionSdp + baseAudioMLineSdp + flexFecVideoMLineSdp + baseDataMLineSdp; // A full sdp string representing a client that doesn't have local sources added on Firefox. -const recvOnlySdpStr = baseSessionSdp + recvOnlyAudioMline + recvOnlyVideoMline; +const recvOnlySdpStr = baseSessionSdp + recvOnlyAudioMline + recvOnlyVideoMline + baseDataMLineSdp; + +// A full sdp string representing a client that doesn't have local sources added on Chrome. +const recvOnlySdpStrChrome = baseSessionSdp + recvOnlyAudioMlineChrome + recvOnlyVideoMlineChrome + baseDataMLineSdp; // A full sdp string representing a Firefox client with msid set to '-'. const sdpFirefoxStr = baseSessionSdp + baseAudioMLineSdp + videoMlineFF; @@ -474,6 +571,14 @@ const sdpFirefoxStr = baseSessionSdp + baseAudioMLineSdp + videoMlineFF; const sdpFirefoxP2pStr = baseSessionSdp + baseAudioMLineSdp + videoLineP2pFF; export default { + get simulcastSdpStr() { + return simulcastSdpStr; + }, + + get simulcastDifferentSsrcSdpStr() { + return simulcastDifferentSsrcSdpStr; + }, + get simulcastSdp() { return transform.parse(simulcastSdpStr); }, @@ -506,6 +611,14 @@ export default { return transform.parse(flexFecSdpStr); }, + get recvOnlySdpStr() { + return recvOnlySdpStr; + }, + + get recvOnlySdpStrChrome() { + return recvOnlySdpStrChrome; + }, + get recvOnlySdp() { return transform.parse(recvOnlySdpStr); }, diff --git a/modules/xmpp/JingleSessionPC.js b/modules/xmpp/JingleSessionPC.js index 4ee4dcf5a9..2ef4590fa2 100644 --- a/modules/xmpp/JingleSessionPC.js +++ b/modules/xmpp/JingleSessionPC.js @@ -17,7 +17,7 @@ import { XEP } from '../../service/xmpp/XMPPExtensioProtocols'; import { SS_DEFAULT_FRAME_RATE } from '../RTC/ScreenObtainer'; import FeatureFlags from '../flags/FeatureFlags'; import SDP from '../sdp/SDP'; -import SDPDiffer from '../sdp/SDPDiffer'; +import { SDPDiffer } from '../sdp/SDPDiffer'; import SDPUtil from '../sdp/SDPUtil'; import Statistics from '../statistics/statistics'; import AsyncQueue, { ClearedQueueError } from '../util/AsyncQueue'; @@ -1009,7 +1009,7 @@ export default class JingleSessionPC extends JingleSession { sid: this.sid }); - new SDP(offerSdp).toJingle( + new SDP(offerSdp, this.isP2P).toJingle( init, this.isInitiator ? 'initiator' : 'responder'); init = init.tree(); @@ -1198,7 +1198,7 @@ export default class JingleSessionPC extends JingleSession { sendSessionAccept(success, failure) { // NOTE: since we're just reading from it, we don't need to be within // the modification queue to access the local description - const localSDP = new SDP(this.peerconnection.localDescription.sdp); + const localSDP = new SDP(this.peerconnection.localDescription.sdp, this.isP2P); const accept = $iq({ to: this.remoteJid, type: 'set' }) .c('jingle', { xmlns: 'urn:xmpp:jingle:1', @@ -2334,7 +2334,7 @@ export default class JingleSessionPC extends JingleSession { Object.keys(newMedia).forEach(mediaIndex => { const signaledSsrcs = Object.keys(newMedia[mediaIndex].ssrcs); - mediaType = newMedia[mediaIndex].mid; + mediaType = newMedia[mediaIndex].mediaType; if (signaledSsrcs?.length) { ssrcs = ssrcs.concat(signaledSsrcs); } @@ -2347,7 +2347,7 @@ export default class JingleSessionPC extends JingleSession { }; // send source-remove IQ. - let sdpDiffer = new SDPDiffer(newSDP, oldSDP); + let sdpDiffer = new SDPDiffer(newSDP, oldSDP, this.isP2P); const remove = $iq({ to: this.remoteJid, type: 'set' }) .c('jingle', { @@ -2381,7 +2381,7 @@ export default class JingleSessionPC extends JingleSession { } // send source-add IQ. - sdpDiffer = new SDPDiffer(oldSDP, newSDP); + sdpDiffer = new SDPDiffer(oldSDP, newSDP, this.isP2P); const add = $iq({ to: this.remoteJid, type: 'set' }) .c('jingle', { diff --git a/service/RTC/RTCEvents.spec.ts b/service/RTC/RTCEvents.spec.ts index 29293ce90c..3cac995e2e 100644 --- a/service/RTC/RTCEvents.spec.ts +++ b/service/RTC/RTCEvents.spec.ts @@ -14,7 +14,6 @@ describe( "/service/RTC/RTCEvents members", () => { PERMISSIONS_CHANGED, SENDER_VIDEO_CONSTRAINTS_CHANGED, LASTN_VALUE_CHANGED, - LOCAL_TRACK_SSRC_UPDATED, LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED, TRACK_ATTACHED, REMOTE_TRACK_ADDED, @@ -49,7 +48,6 @@ describe( "/service/RTC/RTCEvents members", () => { expect( PERMISSIONS_CHANGED ).toBe( 'rtc.permissions_changed' ); expect( SENDER_VIDEO_CONSTRAINTS_CHANGED ).toBe( 'rtc.sender_video_constraints_changed' ); expect( LASTN_VALUE_CHANGED ).toBe( 'rtc.lastn_value_changed' ); - expect( LOCAL_TRACK_SSRC_UPDATED ).toBe( 'rtc.local_track_ssrc_updated' ); expect( LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED ).toBe( 'rtc.local_track_max_enabled_resolution_changed' ); expect( TRACK_ATTACHED ).toBe( 'rtc.track_attached' ); expect( REMOTE_TRACK_ADDED ).toBe( 'rtc.remote_track_added' ); @@ -79,7 +77,6 @@ describe( "/service/RTC/RTCEvents members", () => { expect( RTCEvents.PERMISSIONS_CHANGED ).toBe( 'rtc.permissions_changed' ); expect( RTCEvents.SENDER_VIDEO_CONSTRAINTS_CHANGED ).toBe( 'rtc.sender_video_constraints_changed' ); expect( RTCEvents.LASTN_VALUE_CHANGED ).toBe( 'rtc.lastn_value_changed' ); - expect( RTCEvents.LOCAL_TRACK_SSRC_UPDATED ).toBe( 'rtc.local_track_ssrc_updated' ); expect( RTCEvents.LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED ).toBe( 'rtc.local_track_max_enabled_resolution_changed' ); expect( RTCEvents.TRACK_ATTACHED ).toBe( 'rtc.track_attached' ); expect( RTCEvents.REMOTE_TRACK_ADDED ).toBe( 'rtc.remote_track_added' ); @@ -110,7 +107,6 @@ describe( "/service/RTC/RTCEvents members", () => { expect( RTCEventsDefault.PERMISSIONS_CHANGED ).toBe( 'rtc.permissions_changed' ); expect( RTCEventsDefault.SENDER_VIDEO_CONSTRAINTS_CHANGED ).toBe( 'rtc.sender_video_constraints_changed' ); expect( RTCEventsDefault.LASTN_VALUE_CHANGED ).toBe( 'rtc.lastn_value_changed' ); - expect( RTCEventsDefault.LOCAL_TRACK_SSRC_UPDATED ).toBe( 'rtc.local_track_ssrc_updated' ); expect( RTCEventsDefault.LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED ).toBe( 'rtc.local_track_max_enabled_resolution_changed' ); expect( RTCEventsDefault.TRACK_ATTACHED ).toBe( 'rtc.track_attached' ); expect( RTCEventsDefault.REMOTE_TRACK_ADDED ).toBe( 'rtc.remote_track_added' ); diff --git a/service/RTC/RTCEvents.ts b/service/RTC/RTCEvents.ts index b0e7da0c31..bd1aeed5d7 100644 --- a/service/RTC/RTCEvents.ts +++ b/service/RTC/RTCEvents.ts @@ -30,14 +30,6 @@ export enum RTCEvents { */ LASTN_VALUE_CHANGED = 'rtc.lastn_value_changed', - /** - * Event emitted when ssrc for a local track is extracted and stored - * in {@link TraceablePeerConnection}. - * @param {JitsiLocalTrack} track which ssrc was updated - * @param {string} ssrc that was stored - */ - LOCAL_TRACK_SSRC_UPDATED = 'rtc.local_track_ssrc_updated', - /** * The max enabled resolution of a local video track was changed. */ @@ -135,7 +127,6 @@ export const FORWARDED_SOURCES_CHANGED = RTCEvents.FORWARDED_SOURCES_CHANGED; export const PERMISSIONS_CHANGED = RTCEvents.PERMISSIONS_CHANGED; export const SENDER_VIDEO_CONSTRAINTS_CHANGED = RTCEvents.SENDER_VIDEO_CONSTRAINTS_CHANGED; export const LASTN_VALUE_CHANGED = RTCEvents.LASTN_VALUE_CHANGED; -export const LOCAL_TRACK_SSRC_UPDATED = RTCEvents.LOCAL_TRACK_SSRC_UPDATED; export const LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED = RTCEvents.LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED; export const TRACK_ATTACHED = RTCEvents.TRACK_ATTACHED; export const REMOTE_TRACK_ADDED = RTCEvents.REMOTE_TRACK_ADDED; diff --git a/types/hand-crafted/service/RTC/RTCEvents.d.ts b/types/hand-crafted/service/RTC/RTCEvents.d.ts index 1cddcea91d..91f8c98de4 100644 --- a/types/hand-crafted/service/RTC/RTCEvents.d.ts +++ b/types/hand-crafted/service/RTC/RTCEvents.d.ts @@ -8,7 +8,6 @@ export enum RTCEvents { PERMISSIONS_CHANGED = 'rtc.permissions_changed', SENDER_VIDEO_CONSTRAINTS_CHANGED = 'rtc.sender_video_constraints_changed', LASTN_VALUE_CHANGED = 'rtc.lastn_value_changed', - LOCAL_TRACK_SSRC_UPDATED = 'rtc.local_track_ssrc_updated', LOCAL_TRACK_MAX_ENABLED_RESOLUTION_CHANGED = 'rtc.local_track_max_enabled_resolution_changed', TRACK_ATTACHED = 'rtc.track_attached', REMOTE_TRACK_ADDED = 'rtc.remote_track_added',