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