UNPKG

@jitsi/sdp-interop

Version:

A simple SDP interoperability layer for Unified Plan/Plan B

441 lines (367 loc) 16.4 kB
/* Copyright @ 2015 - Present, 8x8 Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import clonedeep from 'lodash.clonedeep'; import transform from './transform.js'; const PLAN_B_MIDS = [ 'audio', 'video', 'data' ]; const findSimGroup = ssrcGroup => ssrcGroup.find(grp => grp.semantics === 'SIM'); const findFidGroup = ssrcGroup => ssrcGroup.find(grp => grp.semantics === 'FID'); /** * Add the ssrcs of the SIM group and their corresponding FID group ssrcs * to the m-line. * @param {Object} mLine - The m-line to which ssrcs have to be added. * @param {Object} simGroup - The SIM group whose ssrcs have to be added to * the m-line. * @param {Object} sourceGroups - inverted source-group map. * @param {Array<Object>} sourceList - array containing all the sources. */ function addSimGroupSources(mLine, simGroup, sourceGroups, sourceList) { if (!mLine || !simGroup) { return; } const findSourcebyId = src => sourceList.find(source => source.id.toString() === src); simGroup.ssrcs.forEach(src => { mLine.sources.push(findSourcebyId(src)); // find the related FID group member for this ssrc. const relatedFidGroup = sourceGroups[parseInt(src, 10)].find(grp => grp.semantics === 'FID'); if (relatedFidGroup) { const relatedSsrc = relatedFidGroup.ssrcs.find(s => s !== src); mLine.sources.push(findSourcebyId(relatedSsrc)); mLine.ssrcGroups.push(relatedFidGroup); } }); // Add the SIM group last. mLine.ssrcGroups.push(simGroup); } /** * Add ssrcs and ssrc-groups to the m-line. When a primary ssrc, i.e., the * first ssrc in a SIM group is passed, all the other ssrcs from the SIM * group and the other ssrcs from the related FID groups are added to the same * m-line since they all belong to the same remote source. Since the ssrcs are * not guaranteed to be in the correct order, try to find if a SIM group exists, * if not, just add the FID group. * @param {Object} mLine - The m-line to which ssrcs have to be added. * @param {Object} ssrc - the primary ssrc. * @param {Object} sourceGroups - inverted source-group map. * @param {Array<Object>} sourceList - array containing all the sources. * @returns {void} */ function addSourcesToMline(mLine, ssrc, sourceGroups, sourceList) { if (!mLine || !ssrc) { return; } mLine.sources = []; mLine.ssrcGroups = []; // If there are no associated ssrc-groups, just add the ssrc and msid. if (!sourceGroups[ssrc.id]) { mLine.sources.push(ssrc); mLine.msid = ssrc.msid; return; } const findSourcebyId = src => sourceList.find(source => source.id.toString() === src); // Find the SIM and FID groups that this ssrc belongs to. const simGroup = findSimGroup(sourceGroups[ssrc.id]); const fidGroup = findFidGroup(sourceGroups[ssrc.id]); // Add the ssrcs for the SIM group and their corresponding FID groups. if (simGroup) { addSimGroupSources(mLine, simGroup, sourceGroups, sourceList); } else if (fidGroup) { // check if the other ssrc from this FID group is part of a SIM group const otherSsrc = fidGroup.ssrcs.find(s => s !== ssrc); const simGroup2 = findSimGroup(sourceGroups[otherSsrc]); if (simGroup2) { addSimGroupSources(mLine, simGroup2, sourceGroups, sourceList); } else { // Add the FID group ssrcs. fidGroup.ssrcs.forEach(src => { mLine.sources.push(findSourcebyId(src)); }); mLine.ssrcGroups.push(fidGroup); } } // Set the msid for the media description using the msid attribute of the ssrcs. mLine.msid = mLine.sources[0].msid; } /** * Checks if there is a mline for the given ssrc or its related primary ssrc. * We always implode the SIM group to the first ssrc in the SIM group before sRD, * so we also check if mline for that ssrc exists. * For example: * If the following ssrcs are in a SIM group, * <ssrc-group xmlns=\"urn:xmpp:jingle:apps:rtp:ssma:0\" semantics=\"SIM\"> * <source ssrc=\"1806330949\"/> * <source ssrc=\"4173145196\"/> * <source ssrc=\"2002632207\"/> * </ssrc-group> * This method returns true for any one of the 3 ssrcs if there is a mline for 1806330949. * @param {Object} ssrc - ssrc to check. * @param {Object} sourceGroups - inverted source-group map. * @param {Array<Object>} mlines - mlines in the description * @returns {Boolean} - Returns true if mline for the given ssrc or the related primary ssrc * exists, returns false otherwise. */ function checkIfMlineForSsrcExists(ssrc, sourceGroups, mlines) { const findMatchingMline = mline => { if (mline.sources) { return mline.sources.some(source => source.id === ssrc.id); } return false; }; if (!mlines.find(findMatchingMline)) { // check if this ssrc is member of a SIM group. If so, check if there // is a matching m-line for the primary ssrc of the SIM group. if (!sourceGroups[ssrc.id]) { return false; } const simGroup = findSimGroup(sourceGroups[ssrc.id]); const fidGroup = findFidGroup(sourceGroups[ssrc.id]); if (simGroup) { return mlines.some(mline => mline.sources && mline.sources.some(src => src.id.toString() === simGroup.ssrcs[0])); } else if (fidGroup && ssrc.id.toString() !== fidGroup.ssrcs[0]) { const otherSsrc = { id: fidGroup.ssrcs[0] }; return checkIfMlineForSsrcExists(otherSsrc, sourceGroups, mlines); } return false; } return true; } /** * Create an inverted sourceGroup map to put all the grouped ssrcs * in the same m-line. * @param {Array<Object>} sourceGroups * @returns {Object} - An inverted sourceGroup map. */ function createSourceGroupMap(sourceGroups) { const ssrc2group = {}; if (!sourceGroups || !Array.isArray(sourceGroups)) { return ssrc2group; } sourceGroups.forEach(group => { if (group.ssrcs && Array.isArray(group.ssrcs)) { group.ssrcs.forEach(ssrc => { if (typeof ssrc2group[ssrc] === 'undefined') { ssrc2group[ssrc] = []; } ssrc2group[ssrc].push(group); }); } }); return ssrc2group; } /** * Interop provides an API for tranforming a Plan B SDP to a Unified Plan SDP and * vice versa. */ export class Interop { /** * This method transforms a Unified Plan SDP to an equivalent Plan B SDP. * @param {RTCSessionDescription} description - The description in Unified plan format. * @returns RTCSessionDescription - The transformed session description. */ toPlanB(description) { if (!description || typeof description.sdp !== 'string') { console.warn('An empty description was passed as an argument.'); return description; } // Objectify the SDP for easier manipulation. const session = transform.parse(description.sdp); // If the SDP contains no media, there's nothing to transform. if (!session.media || !session.media.length) { console.warn('The description has no media.'); return description; } // Make sure this is a unified plan sdp if (session.media.every(m => PLAN_B_MIDS.indexOf(m.mid) !== -1)) { console.warn('The description does not look like unified plan sdp'); return description; } const media = {}; const sessionMedia = session.media; session.media = []; sessionMedia.forEach(mLine => { const type = mLine.type; if (type === 'application') { mLine.mid = 'data'; media[mLine.mid] = mLine; return; } if (typeof media[type] === 'undefined') { const bLine = clonedeep(mLine); // Copy the msid attribute to all the ssrcs if they belong to the same source group if (bLine.sources && Array.isArray(bLine.sources)) { bLine.sources.forEach(source => { mLine.msid ? source.msid = mLine.msid : delete source.msid; }); } // Do not signal the FID groups if there is no msid attribute present // on the sources as sesison-accept with this source info will fail strophe // validation and the session will not be established. This behavior is seen // on Firefox (with RTX enabled) when no video source is added at the join time. // FF generates two recvonly ssrcs with no msid and a corresponding FID group in // this case. if (!bLine.ssrcGroups || !mLine.msid) { bLine.ssrcGroups = []; } delete bLine.msid; bLine.mid = type; media[type] = bLine; } else if (mLine.msid) { // Add sources and source-groups to the existing m-line of the same media type. if (mLine.sources && Array.isArray(mLine.sources)) { media[type].sources = (media[type].sources || []).concat(mLine.sources); } if (typeof mLine.ssrcGroups !== 'undefined' && Array.isArray(mLine.ssrcGroups)) { media[type].ssrcGroups = media[type].ssrcGroups.concat(mLine.ssrcGroups); } } }); session.media = Object.values(media); // Bundle the media only if it is active. const bundle = []; Object.values(media).forEach(mline => { if (mline.direction !== 'inactive') { bundle.push(mline.mid); } }); // We regenerate the BUNDLE group with the new mids. session.groups.forEach(group => { if (group.type === 'BUNDLE') { group.mids = bundle.join(' '); } }); // msid semantic session.msidSemantic = { semantic: 'WMS', token: '*' }; const resStr = transform.write(session); return new RTCSessionDescription({ type: description.type, sdp: resStr }); } /** * This method transforms a Plan B SDP to an equivalent Unified Plan SDP. * @param {RTCSessionDescription} description - The description in plan-b format. * @param {RTCSessionDescription} current - The current description set on * the peerconnection in Unified-plan format, i.e., the readonly attribute * remoteDescription on the RTCPeerConnection object. * @returns RTCSessionDescription - The transformed session description. */ toUnifiedPlan(description, current = null) { if (!description || typeof description.sdp !== 'string') { console.warn('An empty description was passed as an argument.'); return description; } // Objectify the SDP for easier manipulation. const session = transform.parse(description.sdp); // If the SDP contains no media, there's nothing to transform. if (!session.media || !session.media.length) { console.warn('The description has no media.'); return description; } // Make sure this is a plan-b sdp. if (session.media.length > 3 || session.media.every(m => PLAN_B_MIDS.indexOf(m.mid) === -1)) { console.warn('The description does not look like plan-b'); return description; } const currentDesc = current ? transform.parse(current.sdp) : null; const media = {}; session.media.forEach(mLine => { const type = mLine.type; if (type === 'application') { if (!currentDesc || !currentDesc.media) { const newMline = clonedeep(mLine); newMline.mid = Object.keys(media).length.toString(); media[mLine.mid] = newMline; return; } const mLineForData = currentDesc.media.findIndex(m => m.type === type); if (mLineForData) { currentDesc.media[mLineForData] = mLine; currentDesc.media[mLineForData].mid = mLineForData; } return; } // Create an inverted sourceGroup map here to put all the grouped SSRCs in the same m-line. const ssrc2group = createSourceGroupMap(mLine.ssrcGroups); // If there are no sources advertised for a media type, add the description if this is the first // remote offer, i.e., no current description was passed. Chrome in Unified plan does not produce // recvonly ssrcs unlike Firefox and Safari. if (!mLine.sources) { if (!currentDesc) { const newMline = clonedeep(mLine); newMline.mid = Object.keys(media).length.toString(); media[mLine.mid] = newMline; } return; } mLine.sources.forEach((ssrc, idx) => { // Do not add the receive-only ssrcs that Jicofo sends in the source-add. // These ssrcs do not have the "msid" attribute set. if (!ssrc.msid) { return; } // If there is no description set on the peerconnection, create new m-lines. if (!currentDesc || !currentDesc.media) { if (checkIfMlineForSsrcExists(ssrc, ssrc2group, Object.values(media))) { return; } const newMline = clonedeep(mLine); newMline.mid = Object.keys(media).length.toString(); newMline.direction = idx ? 'sendonly' : mLine.direction === 'sendonly' ? 'sendonly' : 'sendrecv'; newMline.bundleOnly = undefined; addSourcesToMline(newMline, ssrc, ssrc2group, mLine.sources); media[newMline.mid] = newMline; return; } // Create and append the m-lines to the existing description. if (checkIfMlineForSsrcExists(ssrc, ssrc2group, currentDesc.media)) { return; } const newMline = clonedeep(mLine); newMline.mid = currentDesc.media.length.toString(); newMline.direction = 'sendonly'; addSourcesToMline(newMline, ssrc, ssrc2group, mLine.sources); currentDesc.media.push(newMline); }); }); session.media = currentDesc ? currentDesc.media : Object.values(media); const mids = []; session.media.forEach(mLine => { mids.push(mLine.mid); }); // We regenerate the BUNDLE group (since we regenerated the mids) session.groups.forEach(group => { if (group.type === 'BUNDLE') { group.mids = mids.join(' '); } }); // msid semantic session.msidSemantic = { semantic: 'WMS', token: '*' }; // Increment the session version every time. session.origin.sessionVersion++; const resultSdp = transform.write(session); return new RTCSessionDescription({ type: description.type, sdp: resultSdp }); } }