tor-browser

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

sdp.js (25961B)


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