/** * Copyright (C) 2021 Double Bastion LLC * * This file is part of Roundpin, which is licensed under the * GNU Affero General Public License Version 3.0. The license terms * are detailed in the "LICENSE.txt" file located in the root directory. * * The file content from below is identical with that of the * original file "sdp-interop-sl-1.4.0.js". The copyright * notice for the original content follows: * * Copyright @ 2015 Atlassian Pty Ltd * License: Apache License Version 2.0 */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.SdpInterop = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= len) { return x; // missing argument } var arg = args[i]; i += 1; switch (x) { case '%%': return '%'; case '%s': return String(arg); case '%d': return Number(arg); case '%v': return ''; } }); // NB: we discard excess arguments - they are typically undefined from makeLine }; var makeLine = function (type, obj, location) { var str = obj.format instanceof Function ? (obj.format(obj.push ? location : location[obj.name])) : obj.format; var args = [type + '=' + str]; if (obj.names) { for (var i = 0; i < obj.names.length; i += 1) { var n = obj.names[i]; if (obj.name) { args.push(location[obj.name][n]); } else { // for mLine and push attributes args.push(location[obj.names[i]]); } } } else { args.push(location[obj.name]); } return format.apply(null, args); }; // RFC specified order // TODO: extend this with all the rest var defaultOuterOrder = [ 'v', 'o', 's', 'i', 'u', 'e', 'p', 'c', 'b', 't', 'r', 'z', 'a' ]; var defaultInnerOrder = ['i', 'c', 'b', 'a']; module.exports = function (session, opts) { opts = opts || {}; // ensure certain properties exist if (session.version == null) { session.version = 0; // 'v=0' must be there (only defined version atm) } if (session.name == null) { session.name = ' '; // 's= ' must be there if no meaningful name set } session.media.forEach(function (mLine) { if (mLine.payloads == null) { mLine.payloads = ''; } }); var outerOrder = opts.outerOrder || defaultOuterOrder; var innerOrder = opts.innerOrder || defaultInnerOrder; var sdp = []; // loop through outerOrder for matching properties on session outerOrder.forEach(function (type) { grammar[type].forEach(function (obj) { if (obj.name in session && session[obj.name] != null) { sdp.push(makeLine(type, obj, session)); } else if (obj.push in session && session[obj.push] != null) { session[obj.push].forEach(function (el) { sdp.push(makeLine(type, obj, el)); }); } }); }); // then for each media line, follow the innerOrder session.media.forEach(function (mLine) { sdp.push(makeLine('m', grammar.m[0], mLine)); innerOrder.forEach(function (type) { grammar[type].forEach(function (obj) { if (obj.name in mLine && mLine[obj.name] != null) { sdp.push(makeLine(type, obj, mLine)); } else if (obj.push in mLine && mLine[obj.push] != null) { mLine[obj.push].forEach(function (el) { sdp.push(makeLine(type, obj, el)); }); } }); }); }); return sdp.join('\r\n') + '\r\n'; }; },{"./grammar":1}],5:[function(require,module,exports){ /* Copyright @ 2015 Atlassian Pty Ltd * * 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. */ var SdpInterop = module.exports = { InteropFF: require('./interop_on_ff'), InteropChrome: require('./interop_on_chrome'), transform: require('./transform') }; },{"./interop_on_chrome":7,"./interop_on_ff":8,"./transform":11}],6:[function(require,module,exports){ /* Copyright @ 2015 Atlassian Pty Ltd * * 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. */ module.exports = function arrayEquals(array) { // if the other array is a falsy value, return if (!array) return false; // compare lengths - can save a lot of time if (this.length != array.length) return false; for (var i = 0, l = this.length; i < l; i++) { // Check if we have nested arrays if (this[i] instanceof Array && array[i] instanceof Array) { // recurse into the nested arrays if (!arrayEquals.apply(this[i], [array[i]])) return false; } else if (this[i] != array[i]) { // Warning - two different object instances will never be equal: // {x:20} != {x:20} return false; } } return true; }; },{}],7:[function(require,module,exports){ /** * Copyright(c) Starleaf Ltd. 2016. */ "use strict"; //Small library for plan b interop - Designed to be run on chrome. //Assumes you will do the following - convert unified plan received on the wire into plan B //before setting the remote description //Convert plan b generated by chrome into unified plan prior to sending. var Interop = function () { var cache = {}; var copyObj = function (obj) { return JSON.parse(JSON.stringify(obj)); }; var toUnifiedPlan = function (desc) { var uplan = require('./on_chrome/to-unified-plan')(desc, cache); //cache a copy cache.local = copyObj(uplan.sdp); return uplan; }; var toPlanB = function (desc) { //cache the last unified plan we received on the wire cache.remote = copyObj(desc.sdp); return require('./on_chrome/to-plan-b')(desc, cache); }; var that = {}; that.toUnifiedPlan = toUnifiedPlan; that.toPlanB = toPlanB; return that; }; module.exports = Interop; },{"./on_chrome/to-plan-b":9,"./on_chrome/to-unified-plan":10}],8:[function(require,module,exports){ /* Copyright @ 2015 Atlassian Pty Ltd * * 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. */ /* global RTCSessionDescription */ /* jshint -W097 */ "use strict"; var transform = require('./transform'); var arrayEquals = require('./array-equals'); function Interop() { /** * This map holds the most recent Unified Plan offer/answer SDP that was * converted to Plan B, with the SDP type ('offer' or 'answer') as keys and * the SDP string as values. * * @type {{}} */ this.cache = {}; } module.exports = Interop; /** * Returns the index of the first m-line with the given media type and with a * direction which allows sending, in the last Unified Plan description with * type "answer" converted to Plan B. Returns {null} if there is no saved * answer, or if none of its m-lines with the given type allow sending. * @param type the media type ("audio" or "video"). * @returns {*} */ Interop.prototype.getFirstSendingIndexFromAnswer = function (type) { if (!this.cache.answer) { return null; } var session = transform.parse(this.cache.answer); if (session && session.media && Array.isArray(session.media)) { for (var i = 0; i < session.media.length; i++) { if (session.media[i].type == type && (!session.media[i].direction /* default to sendrecv */ || session.media[i].direction === 'sendrecv' || session.media[i].direction === 'sendonly')) { return i; } } } return null; }; /** * This method transforms a Unified Plan SDP to an equivalent Plan B SDP. A * PeerConnection wrapper transforms the SDP to Plan B before passing it to the * application. * * @param desc * @returns {*} */ Interop.prototype.toPlanB = function (desc) { var self = this; //#region Preliminary input validation. if (typeof desc !== 'object' || desc === null || typeof desc.sdp !== 'string') { console.warn('An empty description was passed as an argument.'); return desc; } // Objectify the SDP for easier manipulation. var session = transform.parse(desc.sdp); // If the SDP contains no media, there's nothing to transform. if (typeof session.media === 'undefined' || !Array.isArray(session.media) || session.media.length === 0) { console.warn('The description has no media.'); return desc; } // Try some heuristics to "make sure" this is a Unified Plan SDP. Plan B // SDP has a video, an audio and a data "channel" at most. if (session.media.length <= 3 && session.media.every(function (m) { return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; } )) { console.warn('This description does not look like Unified Plan.'); return desc; } //#endregion // HACK https://bugzilla.mozilla.org/show_bug.cgi?id=1113443 var sdp = desc.sdp; var rewrite = false; for (var i = 0; i < session.media.length; i++) { var uLine = session.media[i]; uLine.rtp.forEach(function (rtp) { if (rtp.codec === 'NULL') { rewrite = true; var offer = transform.parse(self.cache.offer); rtp.codec = offer.media[i].rtp[0].codec; } } ); } if (rewrite) { sdp = transform.write(session); } // Unified Plan SDP is our "precious". Cache it for later use in the Plan B // -> Unified Plan transformation. this.cache[desc.type] = sdp; //#region Convert from Unified Plan to Plan B. // We rebuild the session.media array. var media = session.media; session.media = []; // Associative array that maps channel types to channel objects for fast // access to channel objects by their type, e.g. type2bl['audio']->channel // obj. var type2bl = {}; // Used to build the group:BUNDLE value after the channels construction // loop. var types = []; // Implode the Unified Plan m-lines/tracks into Plan B channels. media.forEach(function (uLine) { // rtcp-mux is required in the Plan B SDP. if ((typeof uLine.rtcpMux !== 'string' || uLine.rtcpMux !== 'rtcp-mux') && uLine.direction !== 'inactive') { throw new Error('Cannot convert to Plan B because m-lines ' + 'without the rtcp-mux attribute were found.' ); } if (uLine.type === 'application') { session.media.push(uLine); types.push(uLine.mid); return; } // If we don't have a channel for this uLine.type, then use this // uLine as the channel basis. if (typeof type2bl[uLine.type] === 'undefined') { type2bl[uLine.type] = uLine; } // Add sources to the channel and handle a=msid. if (typeof uLine.sources === 'object') { Object.keys(uLine.sources).forEach(function (ssrc) { if (typeof type2bl[uLine.type].sources !== 'object') type2bl[uLine.type].sources = {}; // Assign the sources to the channel. type2bl[uLine.type].sources[ssrc] = uLine.sources[ssrc]; if (typeof uLine.msid !== 'undefined') { // In Plan B the msid is an SSRC attribute. Also, we don't // care about the obsolete label and mslabel attributes. // // Note that it is not guaranteed that the uLine will // have an msid. recvonly channels in particular don't have // one. type2bl[uLine.type].sources[ssrc].msid = uLine.msid; } // NOTE ssrcs in ssrc groups will share msids, as // draft-uberti-rtcweb-plan-00 mandates. } ); } // Add ssrc groups to the channel. if (typeof uLine.ssrcGroups !== 'undefined' && Array.isArray(uLine.ssrcGroups)) { // Create the ssrcGroups array, if it's not defined. if (typeof type2bl[uLine.type].ssrcGroups === 'undefined' || !Array.isArray(type2bl[uLine.type].ssrcGroups )) { type2bl[uLine.type].ssrcGroups = []; } type2bl[uLine.type].ssrcGroups = type2bl[uLine.type].ssrcGroups.concat( uLine.ssrcGroups ); } if (type2bl[uLine.type] === uLine) { // Copy ICE related stuff from the principal media line. uLine.candidates = media[0].candidates; uLine.iceUfrag = media[0].iceUfrag; uLine.icePwd = media[0].icePwd; uLine.fingerprint = media[0].fingerprint; // Plan B mids are in ['audio', 'video', 'data'] uLine.mid = uLine.type; // Plan B doesn't support/need the bundle-only attribute. delete uLine.bundleOnly; // In Plan B the msid is an SSRC attribute. delete uLine.msid; // Used to build the group:BUNDLE value after this loop. types.push(uLine.type); // Add the channel to the new media array. session.media.push(uLine); } } ); // We regenerate the BUNDLE group with the new mids. session.groups.some(function (group) { if (group.type === 'BUNDLE') { group.mids = types.join(' '); return true; } } ); // msid semantic session.msidSemantic = { semantic: 'WMS', token: '*' }; var resStr = transform.write(session); return new RTCSessionDescription({ type: desc.type, sdp: resStr } ); //#endregion }; /** * This method transforms a Plan B SDP to an equivalent Unified Plan SDP. A * PeerConnection wrapper transforms the SDP to Unified Plan before passing it * to FF. * * @param desc * @returns {*} */ Interop.prototype.toUnifiedPlan = function (desc) { var self = this; //#region Preliminary input validation. if (typeof desc !== 'object' || desc === null || typeof desc.sdp !== 'string') { console.warn('An empty description was passed as an argument.'); return desc; } var session = transform.parse(desc.sdp); // If the SDP contains no media, there's nothing to transform. if (typeof session.media === 'undefined' || !Array.isArray(session.media) || session.media.length === 0) { console.warn('The description has no media.'); return desc; } // Try some heuristics to "make sure" this is a Plan B SDP. Plan B SDP has // a video, an audio and a data "channel" at most. if (session.media.length > 3 || !session.media.every(function (m) { return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; } )) { console.warn('This description does not look like Plan B.'); return desc; } // Make sure this Plan B SDP can be converted to a Unified Plan SDP. var mids = []; session.media.forEach(function (m) { mids.push(m.mid); } ); var hasBundle = false; if (typeof session.groups !== 'undefined' && Array.isArray(session.groups)) { hasBundle = session.groups.every(function (g) { return g.type !== 'BUNDLE' || arrayEquals.apply(g.mids.sort(), [mids.sort()]); } ); } if (!hasBundle) { throw new Error("Cannot convert to Unified Plan because m-lines that" + " are not bundled were found." ); } //#endregion //#region Convert from Plan B to Unified Plan. // Unfortunately, a Plan B offer/answer doesn't have enough information to // rebuild an equivalent Unified Plan offer/answer. // // For example, if this is a local answer (in Unified Plan style) that we // convert to Plan B prior to handing it over to the application (the // PeerConnection wrapper called us, for instance, after a successful // createAnswer), we want to remember the m-line at which we've seen the // (local) SSRC. That's because when the application wants to do call the // SLD method, forcing us to do the inverse transformation (from Plan B to // Unified Plan), we need to know to which m-line to assign the (local) // SSRC. We also need to know all the other m-lines that the original // answer had and include them in the transformed answer as well. // // Another example is if this is a remote offer that we convert to Plan B // prior to giving it to the application, we want to remember the mid at // which we've seen the (remote) SSRC. // // In the iteration that follows, we use the cached Unified Plan (if it // exists) to assign mids to ssrcs. var cached; if (typeof this.cache[desc.type] !== 'undefined') { cached = transform.parse(this.cache[desc.type]); } var recvonlySsrcs = { audio: {}, video: {} }; // A helper map that sends mids to m-line objects. We use it later to // rebuild the Unified Plan style session.media array. var mid2ul = {}; session.media.forEach(function (bLine) { if ((typeof bLine.rtcpMux !== 'string' || bLine.rtcpMux !== 'rtcp-mux') && bLine.direction !== 'inactive') { throw new Error("Cannot convert to Unified Plan because m-lines " + "without the rtcp-mux attribute were found." ); } if (bLine.type === 'application') { mid2ul[bLine.mid] = bLine; return; } // With rtcp-mux and bundle all the channels should have the same ICE // stuff. var sources = bLine.sources; var ssrcGroups = bLine.ssrcGroups; var candidates = bLine.candidates; var iceUfrag = bLine.iceUfrag; var icePwd = bLine.icePwd; var fingerprint = bLine.fingerprint; var port = bLine.port; // We'll use the "bLine" object as a prototype for each new "mLine" // that we create, but first we need to clean it up a bit. delete bLine.sources; delete bLine.ssrcGroups; delete bLine.candidates; delete bLine.iceUfrag; delete bLine.icePwd; delete bLine.fingerprint; delete bLine.port; delete bLine.mid; // inverted ssrc group map var ssrc2group = {}; if (typeof ssrcGroups !== 'undefined' && Array.isArray(ssrcGroups)) { ssrcGroups.forEach(function (ssrcGroup) { // TODO(gp) find out how to receive simulcast with FF. For the // time being, hide it. if (ssrcGroup.semantics === 'SIM') { return; } // XXX This might brake if an SSRC is in more than one group // for some reason. if (typeof ssrcGroup.ssrcs !== 'undefined' && Array.isArray(ssrcGroup.ssrcs)) { ssrcGroup.ssrcs.forEach(function (ssrc) { if (typeof ssrc2group[ssrc] === 'undefined') { ssrc2group[ssrc] = []; } ssrc2group[ssrc].push(ssrcGroup); } ); } } ); } // ssrc to m-line index. var ssrc2ml = {}; if (typeof sources === 'object') { // Explode the Plan B channel sources with one m-line per source. Object.keys(sources).forEach(function (ssrc) { // The (unified) m-line for this SSRC. We either create it from // scratch or, if it's a grouped SSRC, we re-use a related // mline. In other words, if the source is grouped with another // source, put the two together in the same m-line. var uLine; // We assume here that we are the answerer in the O/A, so any // offers which we translate come from the remote side, while // answers are local. So the check below is to make that we // handle receive-only SSRCs in a special way only if they come // from the remote side. if (desc.type === 'offer') { // We want to detect SSRCs which are used by a remote peer // in an m-line with direction=recvonly (i.e. they are // being used for RTCP only). // This information would have gotten lost if the remote // peer used Unified Plan and their local description was // translated to Plan B. So we use the lack of an MSID // attribute to deduce a "receive only" SSRC. if (!sources[ssrc].msid) { recvonlySsrcs[bLine.type][ssrc] = sources[ssrc]; // Receive-only SSRCs must not create new m-lines. We // will assign them to an existing m-line later. return; } } if (typeof ssrc2group[ssrc] !== 'undefined' && Array.isArray(ssrc2group[ssrc])) { ssrc2group[ssrc].some(function (ssrcGroup) { // ssrcGroup.ssrcs *is* an Array, no need to check // again here. return ssrcGroup.ssrcs.some(function (related) { if (typeof ssrc2ml[related] === 'object') { uLine = ssrc2ml[related]; return true; } } ); } ); } if (typeof uLine === 'object') { // the m-line already exists. Just add the source. uLine.sources[ssrc] = sources[ssrc]; delete sources[ssrc].msid; } else { // Use the "bLine" as a prototype for the "uLine". uLine = Object.create(bLine); ssrc2ml[ssrc] = uLine; if (typeof sources[ssrc].msid !== 'undefined') { // Assign the msid of the source to the m-line. Note // that it is not guaranteed that the source will have // msid. In particular "recvonly" sources don't have an // msid. Note that "recvonly" is a term only defined // for m-lines. uLine.msid = sources[ssrc].msid; uLine.direction = 'sendrecv'; delete sources[ssrc].msid; } // We assign one SSRC per media line. uLine.sources = {}; uLine.sources[ssrc] = sources[ssrc]; uLine.ssrcGroups = ssrc2group[ssrc]; // Use the cached Unified Plan SDP (if it exists) to assign // SSRCs to mids. if (typeof cached !== 'undefined' && typeof cached.media !== 'undefined' && Array.isArray(cached.media)) { cached.media.forEach(function (m) { if (typeof m.sources === 'object') { Object.keys(m.sources).forEach(function (s) { if (s === ssrc) { uLine.mid = m.mid; } } ); } } ); } if (typeof uLine.mid === 'undefined') { // If this is an SSRC that we see for the first time // assign it a new mid. This is typically the case when // this method is called to transform a remote // description for the first time or when there is a // new SSRC in the remote description because a new // peer has joined the conference. Local SSRCs should // have already been added to the map in the toPlanB // method. // // Because FF generates answers in Unified Plan style, // we MUST already have a cached answer with all the // local SSRCs mapped to some m-line/mid. if (desc.type === 'answer') { throw new Error("An unmapped SSRC was found."); } uLine.mid = [bLine.type, '-', ssrc].join(''); } // Include the candidates in the 1st media line. uLine.candidates = candidates; uLine.iceUfrag = iceUfrag; uLine.icePwd = icePwd; uLine.fingerprint = fingerprint; uLine.port = port; mid2ul[uLine.mid] = uLine; } } ); } } ); // Rebuild the media array in the right order and add the missing mLines // (missing from the Plan B SDP). session.media = []; mids = []; // reuse if (desc.type === 'answer') { // The media lines in the answer must match the media lines in the // offer. The order is important too. Here we assume that Firefox is // the answerer, so we merely have to use the reconstructed (unified) // answer to update the cached (unified) answer accordingly. // // In the general case, one would have to use the cached (unified) // offer to find the m-lines that are missing from the reconstructed // answer, potentially grabbing them from the cached (unified) answer. // One has to be careful with this approach because inactive m-lines do // not always have an mid, making it tricky (impossible?) to find where // exactly and which m-lines are missing from the reconstructed answer. for (var i = 0; i < cached.media.length; i++) { var uLine = cached.media[i]; if (typeof mid2ul[uLine.mid] === 'undefined') { // The mid isn't in the reconstructed (unified) answer. // This is either a (unified) m-line containing a remote // track only, or a (unified) m-line containing a remote // track and a local track that has been removed. // In either case, it MUST exist in the cached // (unified) answer. // // In case this is a removed local track, clean-up // the (unified) m-line and make sure it's 'recvonly' or // 'inactive'. delete uLine.msid; delete uLine.sources; delete uLine.ssrcGroups; if (!uLine.direction || uLine.direction === 'sendrecv') uLine.direction = 'recvonly'; else if (uLine.direction === 'sendonly') uLine.direction = 'inactive'; } else { // This is an (unified) m-line/channel that contains a local // track (sendrecv or sendonly channel) or it's a unified // recvonly m-line/channel. In either case, since we're // going from PlanB -> Unified Plan this m-line MUST // exist in the cached answer. } session.media.push(uLine); if (typeof uLine.mid === 'string') { // inactive lines don't/may not have an mid. mids.push(uLine.mid); } } } else { // SDP offer/answer (and the JSEP spec) forbids removing an m-section // under any circumstances. If we are no longer interested in sending a // track, we just remove the msid and ssrc attributes and set it to // either a=recvonly (as the reofferer, we must use recvonly if the // other side was previously sending on the m-section, but we can also // leave the possibility open if it wasn't previously in use), or // a=inactive. if (typeof cached !== 'undefined' && typeof cached.media !== 'undefined' && Array.isArray(cached.media)) { cached.media.forEach(function (uLine) { mids.push(uLine.mid); if (typeof mid2ul[uLine.mid] !== 'undefined') { session.media.push(mid2ul[uLine.mid]); } else { delete uLine.msid; delete uLine.sources; delete uLine.ssrcGroups; if (!uLine.direction || uLine.direction === 'sendrecv') uLine.direction = 'recvonly'; if (!uLine.direction || uLine.direction === 'sendonly') uLine.direction = 'inactive'; session.media.push(uLine); } } ); } // Add all the remaining (new) m-lines of the transformed SDP. Object.keys(mid2ul).forEach(function (mid) { if (mids.indexOf(mid) === -1) { mids.push(mid); if (mid2ul[mid].direction === 'recvonly') { // This is a remote recvonly channel. Add its SSRC to the // appropriate sendrecv or sendonly channel. // TODO(gp) what if we don't have sendrecv/sendonly // channel? session.media.some(function (uLine) { if ((uLine.direction === 'sendrecv' || uLine.direction === 'sendonly') && uLine.type === mid2ul[mid].type) { // mid2ul[mid] shouldn't have any ssrc-groups Object.keys(mid2ul[mid].sources).forEach( function (ssrc) { uLine.sources[ssrc] = mid2ul[mid].sources[ssrc]; } ); return true; } } ); } else { session.media.push(mid2ul[mid]); } } } ); } // After we have constructed the Plan Unified m-lines we can figure out // where (in which m-line) to place the 'recvonly SSRCs'. // Note: we assume here that we are the answerer in the O/A, so any offers // which we translate come from the remote side, while answers are local // (and so our last local description is cached as an 'answer'). ["audio", "video"].forEach(function (type) { if (!session || !session.media || !Array.isArray(session.media)) return; var idx = null; if (Object.keys(recvonlySsrcs[type]).length > 0) { idx = self.getFirstSendingIndexFromAnswer(type); if (idx === null) { // If this is the first offer we receive, we don't have a // cached answer. Assume that we will be sending media using // the first m-line for each media type. for (var i = 0; i < session.media.length; i++) { if (session.media[i].type === type) { idx = i; break; } } } } if (idx && session.media.length > idx) { var mLine = session.media[idx]; Object.keys(recvonlySsrcs[type]).forEach(function (ssrc) { if (mLine.sources && mLine.sources[ssrc]) { console.warn("Replacing an existing SSRC."); } if (!mLine.sources) { mLine.sources = {}; } mLine.sources[ssrc] = recvonlySsrcs[type][ssrc]; } ); } } ); // We regenerate the BUNDLE group (since we regenerated the mids) session.groups.some(function (group) { if (group.type === 'BUNDLE') { group.mids = mids.join(' '); return true; } } ); // msid semantic session.msidSemantic = { semantic: 'WMS', token: '*' }; var resStr = transform.write(session); // Cache the transformed SDP (Unified Plan) for later re-use in this // function. this.cache[desc.type] = resStr; return new RTCSessionDescription({ type: desc.type, sdp: resStr } ); //#endregion }; },{"./array-equals":6,"./transform":11}],9:[function(require,module,exports){ /** * Copyright(c) Starleaf Ltd. 2016. */ "use strict"; var transform = require('../transform'); module.exports = function (desc, cache) { if (typeof desc !== 'object' || desc === null || typeof desc.sdp !== 'string') { console.warn('An empty description was passed as an argument.'); return desc; } // Objectify the SDP for easier manipulation. var session = transform.parse(desc.sdp); // If the SDP contains no media, there's nothing to transform. if (typeof session.media === 'undefined' || !Array.isArray(session.media) || session.media.length === 0) { console.warn('The description has no media.'); return desc; } // Try some heuristics to "make sure" this is a Unified Plan SDP. Plan B // SDP has a video, an audio and a data "channel" at most. if (session.media.length <= 3 && session.media.every(function (m) { return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; })) { console.warn('This description does not look like Unified Plan.'); return desc; } //#endregion // HACK https://bugzilla.mozilla.org/show_bug.cgi?id=1113443 var rewrite = false; for (var i = 0; i < session.media.length; i++) { var uLine = session.media[i]; uLine.rtp.forEach(function (rtp) { if (rtp.codec === 'NULL') { rewrite = true; var offer = transform.parse(cache.local); rtp.codec = offer.media[i].rtp[0].codec; } }); } if (rewrite) { desc.sdp = transform.write(session); } // Unified Plan SDP is our "precious". Cache it for later use in the Plan B // -> Unified Plan transformation. //#region Convert from Unified Plan to Plan B. // We rebuild the session.media array. var media = session.media; session.media = []; // Associative array that maps channel types to channel objects for fast // access to channel objects by their type, e.g. type2bl['audio']->channel // obj. var type2bl = {}; // Used to build the group:BUNDLE value after the channels construction // loop. var types = []; // Implode the Unified Plan m-lines/tracks into Plan B channels. media.forEach(function (uLine, index) { // If we don't have a channel for this uLine.type, then use this // uLine as the channel basis. if (typeof type2bl[uLine.type] === 'undefined') { type2bl[uLine.type] = uLine; } if (uLine.port === 0) { if (index > 1 && uLine.type !== 'data') { //it's a secondary video stream - drop without further ado return; } else { delete uLine.mid; uLine.mid = uLine.type; //types.push(uLine.type); session.media.push(uLine); return; } } if (uLine.type === 'application') { session.media.push(uLine); types.push(uLine.mid); return; } // Add sources to the channel and handle a=msid. if (typeof uLine.sources === 'object') { Object.keys(uLine.sources).forEach(function (ssrc) { if (typeof type2bl[uLine.type].sources !== 'object') type2bl[uLine.type].sources = {}; // Assign the sources to the channel. type2bl[uLine.type].sources[ssrc] = uLine.sources[ssrc]; if (typeof uLine.msid !== 'undefined') { // In Plan B the msid is an SSRC attribute. Also, we don't // care about the obsolete label and mslabel attributes. // // Note that it is not guaranteed that the uLine will // have an msid. recvonly channels in particular don't have // one. type2bl[uLine.type].sources[ssrc].msid = uLine.msid; } // NOTE ssrcs in ssrc groups will share msids, as // draft-uberti-rtcweb-plan-00 mandates. }); } // Add ssrc groups to the channel. if (typeof uLine.ssrcGroups !== 'undefined' && Array.isArray(uLine.ssrcGroups)) { // Create the ssrcGroups array, if it's not defined. if (typeof type2bl[uLine.type].ssrcGroups === 'undefined' || !Array.isArray( type2bl[uLine.type].ssrcGroups)) { type2bl[uLine.type].ssrcGroups = []; } type2bl[uLine.type].ssrcGroups = type2bl[uLine.type].ssrcGroups.concat( uLine.ssrcGroups); } if (type2bl[uLine.type] === uLine) { // Copy ICE related stuff from the principal media line. uLine.candidates = media[0].candidates; uLine.iceUfrag = media[0].iceUfrag; uLine.icePwd = media[0].icePwd; uLine.fingerprint = media[0].fingerprint; // Plan B mids are in ['audio', 'video', 'data'] uLine.mid = uLine.type; // Plan B doesn't support/need the bundle-only attribute. delete uLine.bundleOnly; // In Plan B the msid is an SSRC attribute. delete uLine.msid; // Used to build the group:BUNDLE value after this loop. types.push(uLine.type); // Add the channel to the new media array. session.media.push(uLine); } }); // We regenerate the BUNDLE group with the new mids. session.groups.some(function (group) { if (group.type === 'BUNDLE') { group.mids = types.join(' '); return true; } }); // msid semantic session.msidSemantic = { semantic: 'WMS', token: '*' }; var resStr = transform.write(session); return new window.RTCSessionDescription({ type: desc.type, sdp: resStr }); }; },{"../transform":11}],10:[function(require,module,exports){ /** * Copyright(c) Starleaf Ltd. 2016. */ "use strict"; var transform = require('../transform'); var arrayEquals = require('../array-equals'); var copyObj = function (obj) { return JSON.parse(JSON.stringify(obj)); }; module.exports = function (desc, cache) { if (typeof desc !== 'object' || desc === null || typeof desc.sdp !== 'string') { console.warn('An empty description was passed as an argument.'); return desc; } var session = transform.parse(desc.sdp); // If the SDP contains no media, there's nothing to transform. if (typeof session.media === 'undefined' || !Array.isArray(session.media) || session.media.length === 0) { console.warn('The description has no media.'); return desc; } // Try some heuristics to "make sure" this is a Plan B SDP. Plan B SDP has // a video, an audio and a data "channel" at most. if (session.media.length > 3 || !session.media.every(function (m) { return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; } )) { console.warn('This description does not look like Plan B.'); return desc; } // Make sure this Plan B SDP can be converted to a Unified Plan SDP. var bmids = []; session.media.forEach(function (m) { if(m.port !== 0) { //ignore disabled streams, these can be removed from the bundle bmids.push(m.mid); } } ); var hasBundle = false; if (typeof session.groups !== 'undefined' && Array.isArray(session.groups)) { hasBundle = session.groups.every(function (g) { return g.type !== 'BUNDLE' || arrayEquals.apply(g.mids.sort(), [bmids.sort()]); } ); } if (!hasBundle) { throw new Error("Cannot convert to Unified Plan because m-lines that" + " are not bundled were found." ); } var localRef = null; if (typeof cache.local !== 'undefined') localRef = transform.parse(cache.local); var remoteRef = null; if (typeof cache.remote !== 'undefined') remoteRef = transform.parse(cache.remote); var mLines = []; session.media.forEach(function (bLine, index, lines) { var uLine; var ssrc; /*if ((typeof bLine.rtcpMux !== 'string' || bLine.rtcpMux !== 'rtcp-mux') && bLine.direction !== 'inactive') { throw new Error("Cannot convert to Unified Plan because m-lines " + "without the rtcp-mux attribute were found."); }*/ if(bLine.port === 0) { // change the mid to the last used mid for this media type, for consistency if(localRef !== null && localRef.media.length > index) { bLine.mid = localRef.media[index].mid; } mLines.push(bLine); return; } // if we're offering to recv-only on chrome, we won't have any ssrcs at all if (!bLine.sources) { uLine = copyObj(bLine); uLine.sources = {}; uLine.mid = uLine.type + "-" + 1; mLines.push(uLine); return; } var sources = bLine.sources || null; if (!sources) { throw new Error("can't convert to unified plan - each m-line must have an ssrc"); } var ssrcGroups = bLine.ssrcGroups || []; bLine.rtcp.port = bLine.port; var sourcesKeys = Object.keys(sources); if (sourcesKeys.length === 0) { return; } else if (sourcesKeys.length == 1) { ssrc = sourcesKeys[0]; uLine = copyObj(bLine); uLine.mid = uLine.type + "-" + ssrc; mLines.push(uLine); } else { //we might need to split this line delete bLine.sources; delete bLine.ssrcGroups; ssrcGroups.forEach(function (ssrcGroup) { //update in use ssrcs so we don't accidentally override it var primary = ssrcGroup.ssrcs[0]; //use the first ssrc as the main ssrc for this m-line; var copyLine = copyObj(bLine); copyLine.sources = {}; copyLine.sources[primary] = sources[primary]; copyLine.mid = copyLine.type + "-" + primary; mLines.push(copyLine); }); } }); if (desc.type === 'offer') { if (localRef) { // you can never remove media streams from SDP. while (mLines.length < localRef.media.length) { var copyline = localRef.media[mLines.length]; copyline.port = 0; mLines.push(copyline); } } } else { //if we're answering, if the browser accepted the transformed plan b we passed it, //then we're implicitly accepting every stream. //Check all the offers mlines - if we're missing one, we need to add it to our unified plan in recvOnly. //in this case the far end will need to dynamically determine our real SSRC for the RTCP stream, //as chrome won't tell us! if (remoteRef === undefined) { throw Error("remote cache required to generate answer?"); } remoteRef.media.forEach(function(remoteline, index) { if(index < mLines.length) { // the line is already present in the plan-b, so will be handled correctly by the browser; return; } if(remoteline.mid === undefined) { console.warn("remote sdp has undefined mid attribute"); return; } if(remoteline.port === 0) { var disabledline = {}; disabledline.port = 0; disabledline.type = remoteline.type; disabledline.protocol = remoteline.protocol; disabledline.payloads = remoteline.payloads; disabledline.mid = remoteline.mid; if(!session.connection) { if(mLines[0].connection) { disabledline.connection = copyObj(mLines[0].connection); } else { throw Error("missing connection attribute from sdp"); } } else { disabledline.connection = copyObj(session.connection); } disabledline.connection.ip = "0.0.0.0"; mLines.push(disabledline); console.log("added disabled m line to the media"); } else { for(var i = 0; i < mLines.length; i ++) { var typeref = mLines[i]; //check if we have any lines of the same type in the current answer to // build this new line from. if(typeref.type === remoteline.type) { var linecopy = copyObj(typeref); linecopy.mid = remoteline.mid; linecopy.direction = "recvonly"; mLines.push(linecopy); break; } } } }); } session.media = mLines; var mids = []; session.media.forEach(function (mLine) { mids.push(mLine.mid); } ); session.groups.some(function (group) { if (group.type === 'BUNDLE') { group.mids = mids.join(' '); return true; } } ); // msid semantic session.msidSemantic = { semantic: 'WMS', token: '*' }; var resStr = transform.write(session); return new window.RTCSessionDescription({ type: desc.type, sdp: resStr } ); }; },{"../array-equals":6,"../transform":11}],11:[function(require,module,exports){ /* Copyright @ 2015 Atlassian Pty Ltd * * 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. */ var transform = require('sdp-transform'); exports.write = function(session, opts) { if (typeof session !== 'undefined' && typeof session.media !== 'undefined' && Array.isArray(session.media)) { session.media.forEach(function (mLine) { // expand sources to ssrcs if (typeof mLine.sources !== 'undefined' && Object.keys(mLine.sources).length !== 0) { mLine.ssrcs = []; Object.keys(mLine.sources).forEach(function (ssrc) { var source = mLine.sources[ssrc]; Object.keys(source).forEach(function (attribute) { mLine.ssrcs.push({ id: ssrc, attribute: attribute, value: source[attribute] }); }); }); delete mLine.sources; } // join ssrcs in ssrc groups if (typeof mLine.ssrcGroups !== 'undefined' && Array.isArray(mLine.ssrcGroups)) { mLine.ssrcGroups.forEach(function (ssrcGroup) { if (typeof ssrcGroup.ssrcs !== 'undefined' && Array.isArray(ssrcGroup.ssrcs)) { ssrcGroup.ssrcs = ssrcGroup.ssrcs.join(' '); } }); } }); } // join group mids if (typeof session !== 'undefined' && typeof session.groups !== 'undefined' && Array.isArray(session.groups)) { session.groups.forEach(function (g) { if (typeof g.mids !== 'undefined' && Array.isArray(g.mids)) { g.mids = g.mids.join(' '); } }); } return transform.write(session, opts); }; exports.parse = function(sdp) { var session = transform.parse(sdp); if (typeof session !== 'undefined' && typeof session.media !== 'undefined' && Array.isArray(session.media)) { session.media.forEach(function (mLine) { // group sources attributes by ssrc if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) { mLine.sources = {}; mLine.ssrcs.forEach(function (ssrc) { if (!mLine.sources[ssrc.id]) mLine.sources[ssrc.id] = {}; mLine.sources[ssrc.id][ssrc.attribute] = ssrc.value; }); delete mLine.ssrcs; } // split ssrcs in ssrc groups if (typeof mLine.ssrcGroups !== 'undefined' && Array.isArray(mLine.ssrcGroups)) { mLine.ssrcGroups.forEach(function (ssrcGroup) { if (typeof ssrcGroup.ssrcs === 'string') { ssrcGroup.ssrcs = ssrcGroup.ssrcs.split(' '); } }); } }); } // split group mids if (typeof session !== 'undefined' && typeof session.groups !== 'undefined' && Array.isArray(session.groups)) { session.groups.forEach(function (g) { if (typeof g.mids === 'string') { g.mids = g.mids.split(' '); } }); } return session; }; },{"sdp-transform":2}]},{},[5])(5) });