text: (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<r.length;o++)s(r[o]);return s})({1:[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.
   *
   * Modified by StarLeaf Ltd
   */
  
  var SdpInterop = module.exports = {
      InteropFF: require('./interop_on_ff'),
      InteropChrome: require('./interop_on_chrome'),
      transform: require('./transform')
  };
  
  },{"./interop_on_chrome":3,"./interop_on_ff":4,"./transform":7}],2:[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;
  };
  
  
  },{}],3:[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":5,"./on_chrome/to-unified-plan":6}],4:[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.
   *
   * Modified by StarLeaf Ltd
   */
  
  /* 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":2,"./transform":7}],5:[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":7}],6:[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":2,"../transform":7}],7:[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.
   *
   *  Modified by StarLeaf Ltd
   */
  
  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":9}],8:[function(require,module,exports){
  var grammar = module.exports = {
    v: [{
        name: 'version',
        reg: /^(\d*)$/
    }],
    o: [{ //o=- 20518 0 IN IP4 203.0.113.1
      // NB: sessionId will be a String in most cases because it is huge
      name: 'origin',
      reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/,
      names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'],
      format: "%s %s %d %s IP%d %s"
    }],
    // default parsing of these only (though some of these feel outdated)
    s: [{ name: 'name' }],
    i: [{ name: 'description' }],
    u: [{ name: 'uri' }],
    e: [{ name: 'email' }],
    p: [{ name: 'phone' }],
    z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly..
    r: [{ name: 'repeats' }],   // TODO: this one can also be parsed properly
    //k: [{}], // outdated thing ignored
    t: [{ //t=0 0
      name: 'timing',
      reg: /^(\d*) (\d*)/,
      names: ['start', 'stop'],
      format: "%d %d"
    }],
    c: [{ //c=IN IP4 10.47.197.26
        name: 'connection',
        reg: /^IN IP(\d) (\S*)/,
        names: ['version', 'ip'],
        format: "IN IP%d %s"
    }],
    b: [{ //b=AS:4000
        push: 'bandwidth',
        reg: /^(TIAS|AS|CT|RR|RS):(\d*)/,
        names: ['type', 'limit'],
        format: "%s:%s"
    }],
    m: [{ //m=video 51744 RTP/AVP 126 97 98 34 31
        // NB: special - pushes to session
        // TODO: rtp/fmtp should be filtered by the payloads found here?
        reg: /^(\w*) (\d*) ([\w\/]*)(?: (.*))?/,
        names: ['type', 'port', 'protocol', 'payloads'],
        format: "%s %d %s %s"
    }],
    a: [
      { //a=rtpmap:110 opus/48000/2
        push: 'rtp',
        reg: /^rtpmap:(\d*) ([\w\-]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/,
        names: ['payload', 'codec', 'rate', 'encoding'],
        format: function (o) {
          return (o.encoding) ?
            "rtpmap:%d %s/%s/%s":
            o.rate ?
            "rtpmap:%d %s/%s":
            "rtpmap:%d %s";
        }
      },
      {
        //a=fmtp:108 profile-level-id=24;object=23;bitrate=64000
        //a=fmtp:111 minptime=10; useinbandfec=1
        push: 'fmtp',
        reg: /^fmtp:(\d*) ([\S| ]*)/,
        names: ['payload', 'config'],
        format: "fmtp:%d %s"
      },
      { //a=control:streamid=0
          name: 'control',
          reg: /^control:(.*)/,
          format: "control:%s"
      },
      { //a=rtcp:65179 IN IP4 193.84.77.194
        name: 'rtcp',
        reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/,
        names: ['port', 'netType', 'ipVer', 'address'],
        format: function (o) {
          return (o.address != null) ?
            "rtcp:%d %s IP%d %s":
            "rtcp:%d";
        }
      },
      { //a=rtcp-fb:98 trr-int 100
        push: 'rtcpFbTrrInt',
        reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/,
        names: ['payload', 'value'],
        format: "rtcp-fb:%d trr-int %d"
      },
      { //a=rtcp-fb:98 nack rpsi
        push: 'rtcpFb',
        reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/,
        names: ['payload', 'type', 'subtype'],
        format: function (o) {
          return (o.subtype != null) ?
            "rtcp-fb:%s %s %s":
            "rtcp-fb:%s %s";
        }
      },
      { //a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
        //a=extmap:1/recvonly URI-gps-string
        push: 'ext',
        reg: /^extmap:([\w_\/]*) (\S*)(?: (\S*))?/,
        names: ['value', 'uri', 'config'], // value may include "/direction" suffix
        format: function (o) {
          return (o.config != null) ?
            "extmap:%s %s %s":
            "extmap:%s %s";
        }
      },
      {
        //a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32
        push: 'crypto',
        reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/,
        names: ['id', 'suite', 'config', 'sessionConfig'],
        format: function (o) {
          return (o.sessionConfig != null) ?
            "crypto:%d %s %s %s":
            "crypto:%d %s %s";
        }
      },
      { //a=setup:actpass
        name: 'setup',
        reg: /^setup:(\w*)/,
        format: "setup:%s"
      },
      { //a=mid:1
        name: 'mid',
        reg: /^mid:([^\s]*)/,
        format: "mid:%s"
      },
      { //a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a
        name: 'msid',
        reg: /^msid:(.*)/,
        format: "msid:%s"
      },
      { //a=ptime:20
        name: 'ptime',
        reg: /^ptime:(\d*)/,
        format: "ptime:%d"
      },
      { //a=maxptime:60
        name: 'maxptime',
        reg: /^maxptime:(\d*)/,
        format: "maxptime:%d"
      },
      { //a=sendrecv
        name: 'direction',
        reg: /^(sendrecv|recvonly|sendonly|inactive)/
      },
      { //a=ice-lite
        name: 'icelite',
        reg: /^(ice-lite)/
      },
      { //a=ice-ufrag:F7gI
        name: 'iceUfrag',
        reg: /^ice-ufrag:(\S*)/,
        format: "ice-ufrag:%s"
      },
      { //a=ice-pwd:x9cml/YzichV2+XlhiMu8g
        name: 'icePwd',
        reg: /^ice-pwd:(\S*)/,
        format: "ice-pwd:%s"
      },
      { //a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33
        name: 'fingerprint',
        reg: /^fingerprint:(\S*) (\S*)/,
        names: ['type', 'hash'],
        format: "fingerprint:%s %s"
      },
      {
        //a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host
        //a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0
        //a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0
        //a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0
        //a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0
        push:'candidates',
        reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/,
        names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation'],
        format: function (o) {
          var str = "candidate:%s %d %s %d %s %d typ %s";
  
          str += (o.raddr != null) ? " raddr %s rport %d" : "%v%v";
  
          // NB: candidate has three optional chunks, so %void middles one if it's missing
          str += (o.tcptype != null) ? " tcptype %s" : "%v";
  
          if (o.generation != null) {
            str += " generation %d";
          }
          return str;
        }
      },
      { //a=end-of-candidates (keep after the candidates line for readability)
        name: 'endOfCandidates',
        reg: /^(end-of-candidates)/
      },
      { //a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ...
        name: 'remoteCandidates',
        reg: /^remote-candidates:(.*)/,
        format: "remote-candidates:%s"
      },
      { //a=ice-options:google-ice
        name: 'iceOptions',
        reg: /^ice-options:(\S*)/,
        format: "ice-options:%s"
      },
      { //a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1
        push: "ssrcs",
        reg: /^ssrc:(\d*) ([\w_]*):(.*)/,
        names: ['id', 'attribute', 'value'],
        format: "ssrc:%d %s:%s"
      },
      { //a=ssrc-group:FEC 1 2
        push: "ssrcGroups",
        reg: /^ssrc-group:(\w*) (.*)/,
        names: ['semantics', 'ssrcs'],
        format: "ssrc-group:%s %s"
      },
      { //a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV
        name: "msidSemantic",
        reg: /^msid-semantic:\s?(\w*) (\S*)/,
        names: ['semantic', 'token'],
        format: "msid-semantic: %s %s" // space after ":" is not accidental
      },
      { //a=group:BUNDLE audio video
        push: 'groups',
        reg: /^group:(\w*) (.*)/,
        names: ['type', 'mids'],
        format: "group:%s %s"
      },
      { //a=rtcp-mux
        name: 'rtcpMux',
        reg: /^(rtcp-mux)/
      },
      { //a=rtcp-rsize
        name: 'rtcpRsize',
        reg: /^(rtcp-rsize)/
      },
      { // any a= that we don't understand is kepts verbatim on media.invalid
        push: 'invalid',
        names: ["value"]
      }
    ]
  };
  
  // set sensible defaults to avoid polluting the grammar with boring details
  Object.keys(grammar).forEach(function (key) {
    var objs = grammar[key];
    objs.forEach(function (obj) {
      if (!obj.reg) {
        obj.reg = /(.*)/;
      }
      if (!obj.format) {
        obj.format = "%s";
      }
    });
  });
  
  },{}],9:[function(require,module,exports){
  var parser = require('./parser');
  var writer = require('./writer');
  
  exports.write = writer;
  exports.parse = parser.parse;
  exports.parseFmtpConfig = parser.parseFmtpConfig;
  exports.parsePayloads = parser.parsePayloads;
  exports.parseRemoteCandidates = parser.parseRemoteCandidates;
  
  },{"./parser":10,"./writer":11}],10:[function(require,module,exports){
  var toIntIfInt = function (v) {
    return String(Number(v)) === v ? Number(v) : v;
  };
  
  var attachProperties = function (match, location, names, rawName) {
    if (rawName && !names) {
      location[rawName] = toIntIfInt(match[1]);
    }
    else {
      for (var i = 0; i < names.length; i += 1) {
        if (match[i+1] != null) {
          location[names[i]] = toIntIfInt(match[i+1]);
        }
      }
    }
  };
  
  var parseReg = function (obj, location, content) {
    var needsBlank = obj.name && obj.names;
    if (obj.push && !location[obj.push]) {
      location[obj.push] = [];
    }
    else if (needsBlank && !location[obj.name]) {
      location[obj.name] = {};
    }
    var keyLocation = obj.push ?
      {} :  // blank object that will be pushed
      needsBlank ? location[obj.name] : location; // otherwise, named location or root
  
    attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name);
  
    if (obj.push) {
      location[obj.push].push(keyLocation);
    }
  };
  
  var grammar = require('./grammar');
  var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/);
  
  exports.parse = function (sdp) {
    var session = {}
      , media = []
      , location = session; // points at where properties go under (one of the above)
  
    // parse lines we understand
    sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) {
      var type = l[0];
      var content = l.slice(2);
      if (type === 'm') {
        media.push({rtp: [], fmtp: []});
        location = media[media.length-1]; // point at latest media line
      }
  
      for (var j = 0; j < (grammar[type] || []).length; j += 1) {
        var obj = grammar[type][j];
        if (obj.reg.test(content)) {
          return parseReg(obj, location, content);
        }
      }
    });
  
    session.media = media; // link it up
    return session;
  };
  
  var fmtpReducer = function (acc, expr) {
    var s = expr.split('=');
    if (s.length === 2) {
      acc[s[0]] = toIntIfInt(s[1]);
    }
    return acc;
  };
  
  exports.parseFmtpConfig = function (str) {
    return str.split(/\;\s?/).reduce(fmtpReducer, {});
  };
  
  exports.parsePayloads = function (str) {
    return str.split(' ').map(Number);
  };
  
  exports.parseRemoteCandidates = function (str) {
    var candidates = [];
    var parts = str.split(' ').map(toIntIfInt);
    for (var i = 0; i < parts.length; i += 3) {
      candidates.push({
        component: parts[i],
        ip: parts[i + 1],
        port: parts[i + 2]
      });
    }
    return candidates;
  };
  
  },{"./grammar":8}],11:[function(require,module,exports){
  var grammar = require('./grammar');
  
  // customized util.format - discards excess arguments and can void middle ones
  var formatRegExp = /%[sdv%]/g;
  var format = function (formatStr) {
    var i = 1;
    var args = arguments;
    var len = args.length;
    return formatStr.replace(formatRegExp, function (x) {
      if (i >= 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":8}]},{},[1])(1)
  });