tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

sdp.js (26247B)


      1 /* eslint-env node */
      2 "use strict";
      3 
      4 // SDP helpers.
      5 var SDPUtils = {};
      6 
      7 // Generate an alphanumeric identifier for cname or mids.
      8 // TODO: use UUIDs instead? https://gist.github.com/jed/982883
      9 SDPUtils.generateIdentifier = function() {
     10  return Math.random()
     11    .toString(36)
     12    .substr(2, 10);
     13 };
     14 
     15 // The RTCP CNAME used by all peerconnections from the same JS.
     16 SDPUtils.localCName = SDPUtils.generateIdentifier();
     17 
     18 // Splits SDP into lines, dealing with both CRLF and LF.
     19 SDPUtils.splitLines = function(blob) {
     20  return blob
     21    .trim()
     22    .split("\n")
     23    .map(function(line) {
     24      return line.trim();
     25    });
     26 };
     27 // Splits SDP into sessionpart and mediasections. Ensures CRLF.
     28 SDPUtils.splitSections = function(blob) {
     29  var parts = blob.split("\nm=");
     30  return parts.map(function(part, index) {
     31    return (index > 0 ? "m=" + part : part).trim() + "\r\n";
     32  });
     33 };
     34 
     35 // returns the session description.
     36 SDPUtils.getDescription = function(blob) {
     37  var sections = SDPUtils.splitSections(blob);
     38  return sections && sections[0];
     39 };
     40 
     41 // returns the individual media sections.
     42 SDPUtils.getMediaSections = function(blob) {
     43  var sections = SDPUtils.splitSections(blob);
     44  sections.shift();
     45  return sections;
     46 };
     47 
     48 // Returns lines that start with a certain prefix.
     49 SDPUtils.matchPrefix = function(blob, prefix) {
     50  return SDPUtils.splitLines(blob).filter(function(line) {
     51    return line.indexOf(prefix) === 0;
     52  });
     53 };
     54 
     55 SDPUtils.matchPrefixAndTrim = function(blob, prefix) {
     56  return SDPUtils.matchPrefix(blob, prefix).map(l => l.substr(prefix.length).trim());
     57 }
     58 
     59 // Parses an ICE candidate line. Sample input:
     60 // candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
     61 // rport 55996"
     62 SDPUtils.parseCandidate = function(line) {
     63  var parts;
     64  // Parse both variants.
     65  if (line.indexOf("a=candidate:") === 0) {
     66    parts = line.substring(12).split(" ");
     67  } else {
     68    parts = line.substring(10).split(" ");
     69  }
     70 
     71  var candidate = {
     72    foundation: parts[0],
     73    component: parseInt(parts[1], 10),
     74    protocol: parts[2].toLowerCase(),
     75    priority: parseInt(parts[3], 10),
     76    ip: parts[4],
     77    address: parts[4], // address is an alias for ip.
     78    port: parseInt(parts[5], 10),
     79    // skip parts[6] == 'typ'
     80    type: parts[7],
     81  };
     82 
     83  for (var i = 8; i < parts.length; i += 2) {
     84    switch (parts[i]) {
     85      case "raddr":
     86        candidate.relatedAddress = parts[i + 1];
     87        break;
     88      case "rport":
     89        candidate.relatedPort = parseInt(parts[i + 1], 10);
     90        break;
     91      case "tcptype":
     92        candidate.tcpType = parts[i + 1];
     93        break;
     94      case "ufrag":
     95        candidate.ufrag = parts[i + 1]; // for backward compability.
     96        candidate.usernameFragment = parts[i + 1];
     97        break;
     98      default:
     99        // extension handling, in particular ufrag
    100        candidate[parts[i]] = parts[i + 1];
    101        break;
    102    }
    103  }
    104  return candidate;
    105 };
    106 
    107 // Translates a candidate object into SDP candidate attribute.
    108 SDPUtils.writeCandidate = function(candidate) {
    109  var sdp = [];
    110  sdp.push(candidate.foundation);
    111  sdp.push(candidate.component);
    112  sdp.push(candidate.protocol.toUpperCase());
    113  sdp.push(candidate.priority);
    114  sdp.push(candidate.address || candidate.ip);
    115  sdp.push(candidate.port);
    116 
    117  var type = candidate.type;
    118  sdp.push("typ");
    119  sdp.push(type);
    120  if (type !== "host" && candidate.relatedAddress && candidate.relatedPort) {
    121    sdp.push("raddr");
    122    sdp.push(candidate.relatedAddress);
    123    sdp.push("rport");
    124    sdp.push(candidate.relatedPort);
    125  }
    126  if (candidate.tcpType && candidate.protocol.toLowerCase() === "tcp") {
    127    sdp.push("tcptype");
    128    sdp.push(candidate.tcpType);
    129  }
    130  if (candidate.usernameFragment || candidate.ufrag) {
    131    sdp.push("ufrag");
    132    sdp.push(candidate.usernameFragment || candidate.ufrag);
    133  }
    134  return "candidate:" + sdp.join(" ");
    135 };
    136 
    137 // Parses an ice-options line, returns an array of option tags.
    138 // a=ice-options:foo bar
    139 SDPUtils.parseIceOptions = function(line) {
    140  return line.substr(14).split(" ");
    141 };
    142 
    143 // Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
    144 // a=rtpmap:111 opus/48000/2
    145 SDPUtils.parseRtpMap = function(line) {
    146  var parts = line.substr(9).split(" ");
    147  var parsed = {
    148    payloadType: parseInt(parts.shift(), 10), // was: id
    149  };
    150 
    151  parts = parts[0].split("/");
    152 
    153  parsed.name = parts[0];
    154  parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
    155  parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
    156  // legacy alias, got renamed back to channels in ORTC.
    157  parsed.numChannels = parsed.channels;
    158  return parsed;
    159 };
    160 
    161 // Generate an a=rtpmap line from RTCRtpCodecCapability or
    162 // RTCRtpCodecParameters.
    163 SDPUtils.writeRtpMap = function(codec) {
    164  var pt = codec.payloadType;
    165  if (codec.preferredPayloadType !== undefined) {
    166    pt = codec.preferredPayloadType;
    167  }
    168  var channels = codec.channels || codec.numChannels || 1;
    169  return (
    170    "a=rtpmap:" +
    171    pt +
    172    " " +
    173    codec.name +
    174    "/" +
    175    codec.clockRate +
    176    (channels !== 1 ? "/" + channels : "") +
    177    "\r\n"
    178  );
    179 };
    180 
    181 // Parses an a=extmap line (headerextension from RFC 5285). Sample input:
    182 // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
    183 // a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
    184 SDPUtils.parseExtmap = function(line) {
    185  var parts = line.substr(9).split(" ");
    186  return {
    187    id: parseInt(parts[0], 10),
    188    direction: parts[0].indexOf("/") > 0 ? parts[0].split("/")[1] : "sendrecv",
    189    uri: parts[1],
    190  };
    191 };
    192 
    193 // Generates a=extmap line from RTCRtpHeaderExtensionParameters or
    194 // RTCRtpHeaderExtension.
    195 SDPUtils.writeExtmap = function(headerExtension) {
    196  return (
    197    "a=extmap:" +
    198    (headerExtension.id || headerExtension.preferredId) +
    199    (headerExtension.direction && headerExtension.direction !== "sendrecv"
    200      ? "/" + headerExtension.direction
    201      : "") +
    202    " " +
    203    headerExtension.uri +
    204    "\r\n"
    205  );
    206 };
    207 
    208 // Parses an ftmp line, returns dictionary. Sample input:
    209 // a=fmtp:96 vbr=on;cng=on
    210 // Also deals with vbr=on; cng=on
    211 SDPUtils.parseFmtp = function(line) {
    212  var parsed = {};
    213  var kv;
    214  var parts = line.substr(line.indexOf(" ") + 1).split(";");
    215  for (var j = 0; j < parts.length; j++) {
    216    kv = parts[j].trim().split("=");
    217    parsed[kv[0].trim()] = kv[1];
    218  }
    219  return parsed;
    220 };
    221 
    222 // Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
    223 SDPUtils.writeFmtp = function(codec) {
    224  var line = "";
    225  var pt = codec.payloadType;
    226  if (codec.preferredPayloadType !== undefined) {
    227    pt = codec.preferredPayloadType;
    228  }
    229  if (codec.parameters && Object.keys(codec.parameters).length) {
    230    var params = [];
    231    Object.keys(codec.parameters).forEach(function(param) {
    232      if (codec.parameters[param]) {
    233        params.push(param + "=" + codec.parameters[param]);
    234      } else {
    235        params.push(param);
    236      }
    237    });
    238    line += "a=fmtp:" + pt + " " + params.join(";") + "\r\n";
    239  }
    240  return line;
    241 };
    242 
    243 // Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
    244 // a=rtcp-fb:98 nack rpsi
    245 SDPUtils.parseRtcpFb = function(line) {
    246  var parts = line.substr(line.indexOf(" ") + 1).split(" ");
    247  return {
    248    type: parts.shift(),
    249    parameter: parts.join(" "),
    250  };
    251 };
    252 // Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
    253 SDPUtils.writeRtcpFb = function(codec) {
    254  var lines = "";
    255  var pt = codec.payloadType;
    256  if (codec.preferredPayloadType !== undefined) {
    257    pt = codec.preferredPayloadType;
    258  }
    259  if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
    260    // FIXME: special handling for trr-int?
    261    codec.rtcpFeedback.forEach(function(fb) {
    262      lines +=
    263        "a=rtcp-fb:" +
    264        pt +
    265        " " +
    266        fb.type +
    267        (fb.parameter && fb.parameter.length ? " " + fb.parameter : "") +
    268        "\r\n";
    269    });
    270  }
    271  return lines;
    272 };
    273 
    274 // Parses an RFC 5576 ssrc media attribute. Sample input:
    275 // a=ssrc:3735928559 cname:something
    276 SDPUtils.parseSsrcMedia = function(line) {
    277  var sp = line.indexOf(" ");
    278  var parts = {
    279    ssrc: parseInt(line.substr(7, sp - 7), 10),
    280  };
    281  var colon = line.indexOf(":", sp);
    282  if (colon > -1) {
    283    parts.attribute = line.substr(sp + 1, colon - sp - 1);
    284    parts.value = line.substr(colon + 1);
    285  } else {
    286    parts.attribute = line.substr(sp + 1);
    287  }
    288  return parts;
    289 };
    290 
    291 SDPUtils.parseSsrcGroup = function(line) {
    292  var parts = line.substr(13).split(" ");
    293  return {
    294    semantics: parts.shift(),
    295    ssrcs: parts.map(function(ssrc) {
    296      return parseInt(ssrc, 10);
    297    }),
    298  };
    299 };
    300 
    301 // Extracts the MID (RFC 5888) from a media section.
    302 // returns the MID or undefined if no mid line was found.
    303 SDPUtils.getMid = function(mediaSection) {
    304  var mid = SDPUtils.matchPrefix(mediaSection, "a=mid:")[0];
    305  if (mid) {
    306    return mid.substr(6);
    307  }
    308 };
    309 
    310 SDPUtils.parseFingerprint = function(line) {
    311  var parts = line.substr(14).split(" ");
    312  return {
    313    algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
    314    value: parts[1],
    315  };
    316 };
    317 
    318 // Extracts DTLS parameters from SDP media section or sessionpart.
    319 // FIXME: for consistency with other functions this should only
    320 //   get the fingerprint line as input. See also getIceParameters.
    321 SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
    322  var lines = SDPUtils.matchPrefix(
    323    mediaSection + sessionpart,
    324    "a=fingerprint:"
    325  );
    326  // Note: a=setup line is ignored since we use the 'auto' role.
    327  // Note2: 'algorithm' is not case sensitive except in Edge.
    328  return {
    329    role: "auto",
    330    fingerprints: lines.map(SDPUtils.parseFingerprint),
    331  };
    332 };
    333 
    334 // Serializes DTLS parameters to SDP.
    335 SDPUtils.writeDtlsParameters = function(params, setupType) {
    336  var sdp = "a=setup:" + setupType + "\r\n";
    337  params.fingerprints.forEach(function(fp) {
    338    sdp += "a=fingerprint:" + fp.algorithm + " " + fp.value + "\r\n";
    339  });
    340  return sdp;
    341 };
    342 
    343 // Parses a=crypto lines into
    344 //   https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members
    345 SDPUtils.parseCryptoLine = function(line) {
    346  var parts = line.substr(9).split(" ");
    347  return {
    348    tag: parseInt(parts[0], 10),
    349    cryptoSuite: parts[1],
    350    keyParams: parts[2],
    351    sessionParams: parts.slice(3),
    352  };
    353 };
    354 
    355 SDPUtils.writeCryptoLine = function(parameters) {
    356  return (
    357    "a=crypto:" +
    358    parameters.tag +
    359    " " +
    360    parameters.cryptoSuite +
    361    " " +
    362    (typeof parameters.keyParams === "object"
    363      ? SDPUtils.writeCryptoKeyParams(parameters.keyParams)
    364      : parameters.keyParams) +
    365    (parameters.sessionParams ? " " + parameters.sessionParams.join(" ") : "") +
    366    "\r\n"
    367  );
    368 };
    369 
    370 // Parses the crypto key parameters into
    371 //   https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam*
    372 SDPUtils.parseCryptoKeyParams = function(keyParams) {
    373  if (keyParams.indexOf("inline:") !== 0) {
    374    return null;
    375  }
    376  var parts = keyParams.substr(7).split("|");
    377  return {
    378    keyMethod: "inline",
    379    keySalt: parts[0],
    380    lifeTime: parts[1],
    381    mkiValue: parts[2] ? parts[2].split(":")[0] : undefined,
    382    mkiLength: parts[2] ? parts[2].split(":")[1] : undefined,
    383  };
    384 };
    385 
    386 SDPUtils.writeCryptoKeyParams = function(keyParams) {
    387  return (
    388    keyParams.keyMethod +
    389    ":" +
    390    keyParams.keySalt +
    391    (keyParams.lifeTime ? "|" + keyParams.lifeTime : "") +
    392    (keyParams.mkiValue && keyParams.mkiLength
    393      ? "|" + keyParams.mkiValue + ":" + keyParams.mkiLength
    394      : "")
    395  );
    396 };
    397 
    398 // Extracts all SDES paramters.
    399 SDPUtils.getCryptoParameters = function(mediaSection, sessionpart) {
    400  var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, "a=crypto:");
    401  return lines.map(SDPUtils.parseCryptoLine);
    402 };
    403 
    404 // Parses ICE information from SDP media section or sessionpart.
    405 // FIXME: for consistency with other functions this should only
    406 //   get the ice-ufrag and ice-pwd lines as input.
    407 SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
    408  var ufrag = SDPUtils.matchPrefix(
    409    mediaSection + sessionpart,
    410    "a=ice-ufrag:"
    411  )[0];
    412  var pwd = SDPUtils.matchPrefix(mediaSection + sessionpart, "a=ice-pwd:")[0];
    413  if (!(ufrag && pwd)) {
    414    return null;
    415  }
    416  return {
    417    usernameFragment: ufrag.substr(12),
    418    password: pwd.substr(10),
    419  };
    420 };
    421 
    422 // Serializes ICE parameters to SDP.
    423 SDPUtils.writeIceParameters = function(params) {
    424  return (
    425    "a=ice-ufrag:" +
    426    params.usernameFragment +
    427    "\r\n" +
    428    "a=ice-pwd:" +
    429    params.password +
    430    "\r\n"
    431  );
    432 };
    433 
    434 // Parses the SDP media section and returns RTCRtpParameters.
    435 SDPUtils.parseRtpParameters = function(mediaSection) {
    436  var description = {
    437    codecs: [],
    438    headerExtensions: [],
    439    fecMechanisms: [],
    440    rtcp: [],
    441  };
    442  var lines = SDPUtils.splitLines(mediaSection);
    443  var mline = lines[0].split(" ");
    444  for (var i = 3; i < mline.length; i++) {
    445    // find all codecs from mline[3..]
    446    var pt = mline[i];
    447    var rtpmapline = SDPUtils.matchPrefix(
    448      mediaSection,
    449      "a=rtpmap:" + pt + " "
    450    )[0];
    451    if (rtpmapline) {
    452      var codec = SDPUtils.parseRtpMap(rtpmapline);
    453      var fmtps = SDPUtils.matchPrefix(mediaSection, "a=fmtp:" + pt + " ");
    454      // Only the first a=fmtp:<pt> is considered.
    455      codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
    456      codec.rtcpFeedback = SDPUtils.matchPrefix(
    457        mediaSection,
    458        "a=rtcp-fb:" + pt + " "
    459      ).map(SDPUtils.parseRtcpFb);
    460      description.codecs.push(codec);
    461      // parse FEC mechanisms from rtpmap lines.
    462      switch (codec.name.toUpperCase()) {
    463        case "RED":
    464        case "ULPFEC":
    465          description.fecMechanisms.push(codec.name.toUpperCase());
    466          break;
    467        default:
    468          // only RED and ULPFEC are recognized as FEC mechanisms.
    469          break;
    470      }
    471    }
    472  }
    473  SDPUtils.matchPrefix(mediaSection, "a=extmap:").forEach(function(line) {
    474    description.headerExtensions.push(SDPUtils.parseExtmap(line));
    475  });
    476  // FIXME: parse rtcp.
    477  return description;
    478 };
    479 
    480 // Generates parts of the SDP media section describing the capabilities /
    481 // parameters.
    482 SDPUtils.writeRtpDescription = function(kind, caps) {
    483  var sdp = "";
    484 
    485  // Build the mline.
    486  sdp += "m=" + kind + " ";
    487  sdp += caps.codecs.length > 0 ? "9" : "0"; // reject if no codecs.
    488  sdp += " UDP/TLS/RTP/SAVPF ";
    489  sdp +=
    490    caps.codecs
    491      .map(function(codec) {
    492        if (codec.preferredPayloadType !== undefined) {
    493          return codec.preferredPayloadType;
    494        }
    495        return codec.payloadType;
    496      })
    497      .join(" ") + "\r\n";
    498 
    499  sdp += "c=IN IP4 0.0.0.0\r\n";
    500  sdp += "a=rtcp:9 IN IP4 0.0.0.0\r\n";
    501 
    502  // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
    503  caps.codecs.forEach(function(codec) {
    504    sdp += SDPUtils.writeRtpMap(codec);
    505    sdp += SDPUtils.writeFmtp(codec);
    506    sdp += SDPUtils.writeRtcpFb(codec);
    507  });
    508  var maxptime = 0;
    509  caps.codecs.forEach(function(codec) {
    510    if (codec.maxptime > maxptime) {
    511      maxptime = codec.maxptime;
    512    }
    513  });
    514  if (maxptime > 0) {
    515    sdp += "a=maxptime:" + maxptime + "\r\n";
    516  }
    517  sdp += "a=rtcp-mux\r\n";
    518 
    519  if (caps.headerExtensions) {
    520    caps.headerExtensions.forEach(function(extension) {
    521      sdp += SDPUtils.writeExtmap(extension);
    522    });
    523  }
    524  // FIXME: write fecMechanisms.
    525  return sdp;
    526 };
    527 
    528 // Parses the SDP media section and returns an array of
    529 // RTCRtpEncodingParameters.
    530 SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
    531  var encodingParameters = [];
    532  var description = SDPUtils.parseRtpParameters(mediaSection);
    533  var hasRed = description.fecMechanisms.indexOf("RED") !== -1;
    534  var hasUlpfec = description.fecMechanisms.indexOf("ULPFEC") !== -1;
    535 
    536  // filter a=ssrc:... cname:, ignore PlanB-msid
    537  var ssrcs = SDPUtils.matchPrefix(mediaSection, "a=ssrc:")
    538    .map(function(line) {
    539      return SDPUtils.parseSsrcMedia(line);
    540    })
    541    .filter(function(parts) {
    542      return parts.attribute === "cname";
    543    });
    544  var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
    545  var secondarySsrc;
    546 
    547  var flows = SDPUtils.matchPrefix(mediaSection, "a=ssrc-group:FID").map(
    548    function(line) {
    549      var parts = line.substr(17).split(" ");
    550      return parts.map(function(part) {
    551        return parseInt(part, 10);
    552      });
    553    }
    554  );
    555  if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
    556    secondarySsrc = flows[0][1];
    557  }
    558 
    559  description.codecs.forEach(function(codec) {
    560    if (codec.name.toUpperCase() === "RTX" && codec.parameters.apt) {
    561      var encParam = {
    562        ssrc: primarySsrc,
    563        codecPayloadType: parseInt(codec.parameters.apt, 10),
    564      };
    565      if (primarySsrc && secondarySsrc) {
    566        encParam.rtx = { ssrc: secondarySsrc };
    567      }
    568      encodingParameters.push(encParam);
    569      if (hasRed) {
    570        encParam = JSON.parse(JSON.stringify(encParam));
    571        encParam.fec = {
    572          ssrc: primarySsrc,
    573          mechanism: hasUlpfec ? "red+ulpfec" : "red",
    574        };
    575        encodingParameters.push(encParam);
    576      }
    577    }
    578  });
    579  if (encodingParameters.length === 0 && primarySsrc) {
    580    encodingParameters.push({
    581      ssrc: primarySsrc,
    582    });
    583  }
    584 
    585  // we support both b=AS and b=TIAS but interpret AS as TIAS.
    586  var bandwidth = SDPUtils.matchPrefix(mediaSection, "b=");
    587  if (bandwidth.length) {
    588    if (bandwidth[0].indexOf("b=TIAS:") === 0) {
    589      bandwidth = parseInt(bandwidth[0].substr(7), 10);
    590    } else if (bandwidth[0].indexOf("b=AS:") === 0) {
    591      // use formula from JSEP to convert b=AS to TIAS value.
    592      bandwidth =
    593        parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95 - 50 * 40 * 8;
    594    } else {
    595      bandwidth = undefined;
    596    }
    597    encodingParameters.forEach(function(params) {
    598      params.maxBitrate = bandwidth;
    599    });
    600  }
    601  return encodingParameters;
    602 };
    603 
    604 // parses http://draft.ortc.org/#rtcrtcpparameters*
    605 SDPUtils.parseRtcpParameters = function(mediaSection) {
    606  var rtcpParameters = {};
    607 
    608  // Gets the first SSRC. Note tha with RTX there might be multiple
    609  // SSRCs.
    610  var remoteSsrc = SDPUtils.matchPrefix(mediaSection, "a=ssrc:")
    611    .map(function(line) {
    612      return SDPUtils.parseSsrcMedia(line);
    613    })
    614    .filter(function(obj) {
    615      return obj.attribute === "cname";
    616    })[0];
    617  if (remoteSsrc) {
    618    rtcpParameters.cname = remoteSsrc.value;
    619    rtcpParameters.ssrc = remoteSsrc.ssrc;
    620  }
    621 
    622  // Edge uses the compound attribute instead of reducedSize
    623  // compound is !reducedSize
    624  var rsize = SDPUtils.matchPrefix(mediaSection, "a=rtcp-rsize");
    625  rtcpParameters.reducedSize = rsize.length > 0;
    626  rtcpParameters.compound = rsize.length === 0;
    627 
    628  // parses the rtcp-mux attrÑ–bute.
    629  // Note that Edge does not support unmuxed RTCP.
    630  var mux = SDPUtils.matchPrefix(mediaSection, "a=rtcp-mux");
    631  rtcpParameters.mux = mux.length > 0;
    632 
    633  return rtcpParameters;
    634 };
    635 
    636 // parses either a=msid: or a=ssrc:... msid lines and returns
    637 // the id of the MediaStream and MediaStreamTrack.
    638 SDPUtils.parseMsid = function(mediaSection) {
    639  var parts;
    640  var spec = SDPUtils.matchPrefix(mediaSection, "a=msid:");
    641  if (spec.length === 1) {
    642    parts = spec[0].substr(7).split(" ");
    643    return { stream: parts[0], track: parts[1] };
    644  }
    645  var planB = SDPUtils.matchPrefix(mediaSection, "a=ssrc:")
    646    .map(function(line) {
    647      return SDPUtils.parseSsrcMedia(line);
    648    })
    649    .filter(function(msidParts) {
    650      return msidParts.attribute === "msid";
    651    });
    652  if (planB.length > 0) {
    653    parts = planB[0].value.split(" ");
    654    return { stream: parts[0], track: parts[1] };
    655  }
    656 };
    657 
    658 // SCTP
    659 // parses draft-ietf-mmusic-sctp-sdp-26 first and falls back
    660 // to draft-ietf-mmusic-sctp-sdp-05
    661 SDPUtils.parseSctpDescription = function(mediaSection) {
    662  var mline = SDPUtils.parseMLine(mediaSection);
    663  var maxSizeLine = SDPUtils.matchPrefix(mediaSection, "a=max-message-size:");
    664  var maxMessageSize;
    665  if (maxSizeLine.length > 0) {
    666    maxMessageSize = parseInt(maxSizeLine[0].substr(19), 10);
    667  }
    668  if (isNaN(maxMessageSize)) {
    669    maxMessageSize = 65536;
    670  }
    671  var sctpPort = SDPUtils.matchPrefix(mediaSection, "a=sctp-port:");
    672  if (sctpPort.length > 0) {
    673    return {
    674      port: parseInt(sctpPort[0].substr(12), 10),
    675      protocol: mline.fmt,
    676      maxMessageSize,
    677    };
    678  }
    679  var sctpMapLines = SDPUtils.matchPrefix(mediaSection, "a=sctpmap:");
    680  if (sctpMapLines.length > 0) {
    681    var parts = SDPUtils.matchPrefix(mediaSection, "a=sctpmap:")[0]
    682      .substr(10)
    683      .split(" ");
    684    return {
    685      port: parseInt(parts[0], 10),
    686      protocol: parts[1],
    687      maxMessageSize,
    688    };
    689  }
    690 };
    691 
    692 // SCTP
    693 // outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers
    694 // support by now receiving in this format, unless we originally parsed
    695 // as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line
    696 // protocol of DTLS/SCTP -- without UDP/ or TCP/)
    697 SDPUtils.writeSctpDescription = function(media, sctp) {
    698  var output = [];
    699  if (media.protocol !== "DTLS/SCTP") {
    700    output = [
    701      "m=" + media.kind + " 9 " + media.protocol + " " + sctp.protocol + "\r\n",
    702      "c=IN IP4 0.0.0.0\r\n",
    703      "a=sctp-port:" + sctp.port + "\r\n",
    704    ];
    705  } else {
    706    output = [
    707      "m=" + media.kind + " 9 " + media.protocol + " " + sctp.port + "\r\n",
    708      "c=IN IP4 0.0.0.0\r\n",
    709      "a=sctpmap:" + sctp.port + " " + sctp.protocol + " 65535\r\n",
    710    ];
    711  }
    712  if (sctp.maxMessageSize !== undefined) {
    713    output.push("a=max-message-size:" + sctp.maxMessageSize + "\r\n");
    714  }
    715  return output.join("");
    716 };
    717 
    718 // Generate a session ID for SDP.
    719 // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
    720 // recommends using a cryptographically random +ve 64-bit value
    721 // but right now this should be acceptable and within the right range
    722 SDPUtils.generateSessionId = function() {
    723  return Math.floor((Math.random() * 4294967296) + 1);
    724 };
    725 
    726 // Write boilder plate for start of SDP
    727 // sessId argument is optional - if not supplied it will
    728 // be generated randomly
    729 // sessVersion is optional and defaults to 2
    730 // sessUser is optional and defaults to 'thisisadapterortc'
    731 SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) {
    732  var sessionId;
    733  var version = sessVer !== undefined ? sessVer : 2;
    734  if (sessId) {
    735    sessionId = sessId;
    736  } else {
    737    sessionId = SDPUtils.generateSessionId();
    738  }
    739  var user = sessUser || "thisisadapterortc";
    740  // FIXME: sess-id should be an NTP timestamp.
    741  return (
    742    "v=0\r\n" +
    743    "o=" +
    744    user +
    745    " " +
    746    sessionId +
    747    " " +
    748    version +
    749    " IN IP4 127.0.0.1\r\n" +
    750    "s=-\r\n" +
    751    "t=0 0\r\n"
    752  );
    753 };
    754 
    755 SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
    756  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
    757 
    758  // Map ICE parameters (ufrag, pwd) to SDP.
    759  sdp += SDPUtils.writeIceParameters(
    760    transceiver.iceGatherer.getLocalParameters()
    761  );
    762 
    763  // Map DTLS parameters to SDP.
    764  sdp += SDPUtils.writeDtlsParameters(
    765    transceiver.dtlsTransport.getLocalParameters(),
    766    type === "offer" ? "actpass" : "active"
    767  );
    768 
    769  sdp += "a=mid:" + transceiver.mid + "\r\n";
    770 
    771  if (transceiver.direction) {
    772    sdp += "a=" + transceiver.direction + "\r\n";
    773  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
    774    sdp += "a=sendrecv\r\n";
    775  } else if (transceiver.rtpSender) {
    776    sdp += "a=sendonly\r\n";
    777  } else if (transceiver.rtpReceiver) {
    778    sdp += "a=recvonly\r\n";
    779  } else {
    780    sdp += "a=inactive\r\n";
    781  }
    782 
    783  if (transceiver.rtpSender) {
    784    // spec.
    785    var msid =
    786      "msid:" + stream.id + " " + transceiver.rtpSender.track.id + "\r\n";
    787    sdp += "a=" + msid;
    788 
    789    // for Chrome.
    790    sdp += "a=ssrc:" + transceiver.sendEncodingParameters[0].ssrc + " " + msid;
    791    if (transceiver.sendEncodingParameters[0].rtx) {
    792      sdp +=
    793        "a=ssrc:" + transceiver.sendEncodingParameters[0].rtx.ssrc + " " + msid;
    794      sdp +=
    795        "a=ssrc-group:FID " +
    796        transceiver.sendEncodingParameters[0].ssrc +
    797        " " +
    798        transceiver.sendEncodingParameters[0].rtx.ssrc +
    799        "\r\n";
    800    }
    801  }
    802  // FIXME: this should be written by writeRtpDescription.
    803  sdp +=
    804    "a=ssrc:" +
    805    transceiver.sendEncodingParameters[0].ssrc +
    806    " cname:" +
    807    SDPUtils.localCName +
    808    "\r\n";
    809  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
    810    sdp +=
    811      "a=ssrc:" +
    812      transceiver.sendEncodingParameters[0].rtx.ssrc +
    813      " cname:" +
    814      SDPUtils.localCName +
    815      "\r\n";
    816  }
    817  return sdp;
    818 };
    819 
    820 // Gets the direction from the mediaSection or the sessionpart.
    821 SDPUtils.getDirection = function(mediaSection, sessionpart) {
    822  // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
    823  var lines = SDPUtils.splitLines(mediaSection);
    824  for (var i = 0; i < lines.length; i++) {
    825    switch (lines[i]) {
    826      case "a=sendrecv":
    827      case "a=sendonly":
    828      case "a=recvonly":
    829      case "a=inactive":
    830        return lines[i].substr(2);
    831      default:
    832      // FIXME: What should happen here?
    833    }
    834  }
    835  if (sessionpart) {
    836    return SDPUtils.getDirection(sessionpart);
    837  }
    838  return "sendrecv";
    839 };
    840 
    841 SDPUtils.getKind = function(mediaSection) {
    842  var lines = SDPUtils.splitLines(mediaSection);
    843  var mline = lines[0].split(" ");
    844  return mline[0].substr(2);
    845 };
    846 
    847 SDPUtils.isRejected = function(mediaSection) {
    848  return mediaSection.split(" ", 2)[1] === "0";
    849 };
    850 
    851 SDPUtils.parseMLine = function(mediaSection) {
    852  var lines = SDPUtils.splitLines(mediaSection);
    853  var parts = lines[0].substr(2).split(" ");
    854  return {
    855    kind: parts[0],
    856    port: parseInt(parts[1], 10),
    857    protocol: parts[2],
    858    fmt: parts.slice(3).join(" "),
    859  };
    860 };
    861 
    862 SDPUtils.parseOLine = function(mediaSection) {
    863  var line = SDPUtils.matchPrefix(mediaSection, "o=")[0];
    864  var parts = line.substr(2).split(" ");
    865  return {
    866    username: parts[0],
    867    sessionId: parts[1],
    868    sessionVersion: parseInt(parts[2], 10),
    869    netType: parts[3],
    870    addressType: parts[4],
    871    address: parts[5],
    872  };
    873 };
    874 
    875 // a very naive interpretation of a valid SDP.
    876 SDPUtils.isValidSDP = function(blob) {
    877  if (typeof blob !== "string" || blob.length === 0) {
    878    return false;
    879  }
    880  var lines = SDPUtils.splitLines(blob);
    881  for (var i = 0; i < lines.length; i++) {
    882    if (lines[i].length < 2 || lines[i].charAt(1) !== "=") {
    883      return false;
    884    }
    885    // TODO: check the modifier a bit more.
    886  }
    887  return true;
    888 };
    889 
    890 // Expose public methods.
    891 if (typeof module === "object") {
    892  module.exports = SDPUtils;
    893 }