/**
 *  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<r.length;o++)s(r[o]);return s})({1:[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:(\d+)(?:\/(\w+))? (\S*)(?: (\S*))?/,
      names: ['value', 'direction', 'uri', 'config'],
      format: function (o) {
        return 'extmap:%d' + (o.direction ? '/%s' : '%v') + ' %s' + (o.config ? ' %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 network-id 3 network-cost 10
      //a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 network-id 3 network-cost 10
      //a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 network-id 3 network-cost 10
      //a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 network-id 3 network-cost 10
      push:'candidates',
      reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?(?: network-id (\d*))?(?: network-cost (\d*))?/,
      names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation', 'network-id', 'network-cost'],
      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';
        }

        str += (o['network-id'] != null) ? ' network-id %d' : '%v';
        str += (o['network-cost'] != null) ? ' network-cost %d' : '%v';
        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: function (o) {
        var str = 'ssrc:%d';
        if (o.attribute != null) {
          str += ' %s';
          if (o.value != null) {
            str += ':%s';
          }
        }
        return str;
      }
    },
    { //a=ssrc-group:FEC 1 2
      //a=ssrc-group:FEC-FR 3004364195 1080772241
      push: 'ssrcGroups',
      // token-char = %x21 / %x23-27 / %x2A-2B / %x2D-2E / %x30-39 / %x41-5A / %x5E-7E
      reg: /^ssrc-group:([\x21\x23\x24\x25\x26\x27\x2A\x2B\x2D\x2E\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)/
    },
    { //a=sctpmap:5000 webrtc-datachannel 1024
      name: 'sctpmap',
      reg: /^sctpmap:([\w_\/]*) (\S*)(?: (\S*))?/,
      names: ['sctpmapNumber', 'app', 'maxMessageSize'],
      format: function (o) {
        return (o.maxMessageSize != null) ?
          'sctpmap:%s %s %s' :
          'sctpmap:%s %s';
      }
    },
    { //a=x-google-flag:conference
      name: 'xGoogleFlag',
      reg: /^x-google-flag:([^\s]*)/,
      format: 'x-google-flag:%s'
    },
    { //a=rid:1 send max-width=1280;max-height=720;max-fps=30;depend=0
      push: 'rids',
      reg: /^rid:([\d\w]+) (\w+)(?: ([\S| ]*))?/,
      names: ['id', 'direction', 'params'],
      format: function (o) {
        return (o.params) ? 'rid:%s %s %s' : 'rid:%s %s';
      }
    },
    { //a=imageattr:97 send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] recv [x=330,y=250]
      //a=imageattr:* send [x=800,y=640] recv *
      //a=imageattr:100 recv [x=320,y=240]
      push: 'imageattrs',
      reg: new RegExp(
        //a=imageattr:97
        '^imageattr:(\\d+|\\*)' +
        //send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320]
        '[\\s\\t]+(send|recv)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*)' +
        //recv [x=330,y=250]
        '(?:[\\s\\t]+(recv|send)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*))?'
      ),
      names: ['pt', 'dir1', 'attrs1', 'dir2', 'attrs2'],
      format: function (o) {
        return 'imageattr:%s %s %s' + (o.dir2 ? ' %s %s' : '');
      }
    },
    { //a=simulcast:send 1,2,3;~4,~5 recv 6;~7,~8
      //a=simulcast:recv 1;4,5 send 6;7
      name: 'simulcast',
      reg: new RegExp(
        //a=simulcast:
        '^simulcast:' +
        //send 1,2,3;~4,~5
        '(send|recv) ([a-zA-Z0-9\\-_~;,]+)' +
        //space + recv 6;~7,~8
        '(?:\\s?(send|recv) ([a-zA-Z0-9\\-_~;,]+))?' +
        //end
        '$'
      ),
      names: ['dir1', 'list1', 'dir2', 'list2'],
      format: function (o) {
        return 'simulcast:%s %s' + (o.dir2 ? ' %s %s' : '');
      }
    },
    { //Old simulcast draft 03 (implemented by Firefox)
      //  https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-03
      //a=simulcast: recv pt=97;98 send pt=97
      //a=simulcast: send rid=5;6;7 paused=6,7
      name: 'simulcast_03',
      reg: /^simulcast:[\s\t]+([\S+\s\t]+)$/,
      names: ['value'],
      format: 'simulcast: %s'
    },
    {
      //a=framerate:25
      //a=framerate:29.97
      name: 'framerate',
      reg: /^framerate:(\d+(?:$|\.\d+))/,
      format: 'framerate:%s'
    },
    { // 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';
    }
  });
});

},{}],2:[function(require,module,exports){
var parser = require('./parser');
var writer = require('./writer');

exports.write = writer;
exports.parse = parser.parse;
exports.parseFmtpConfig = parser.parseFmtpConfig;
exports.parseParams = parser.parseParams;
exports.parsePayloads = parser.parsePayloads;
exports.parseRemoteCandidates = parser.parseRemoteCandidates;
exports.parseImageAttributes = parser.parseImageAttributes;
exports.parseSimulcastStreamList = parser.parseSimulcastStreamList;

},{"./parser":3,"./writer":4}],3:[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 paramReducer = function (acc, expr) {
  var s = expr.split(/=(.+)/, 2);
  if (s.length === 2) {
    acc[s[0]] = toIntIfInt(s[1]);
  }
  return acc;
};

exports.parseParams = function (str) {
  return str.split(/\;\s?/).reduce(paramReducer, {});
};

// For backward compatibility - alias will be removed in 3.0.0
exports.parseFmtpConfig = exports.parseParams;

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;
};

exports.parseImageAttributes = function (str) {
  return str.split(' ').map(function (item) {
    return item.substring(1, item.length-1).split(',').reduce(paramReducer, {});
  });
};

exports.parseSimulcastStreamList = function (str) {
  return str.split(';').map(function (stream) {
    return stream.split(',').map(function (format) {
      var scid, paused = false;

      if (format[0] !== '~') {
        scid = toIntIfInt(format);
      } else {
        scid = toIntIfInt(format.substring(1, format.length));
        paused = true;
      }

      return {
        scid: scid,
        paused: paused
      };
    });
  });
};

},{"./grammar":1}],4:[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":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)
});