tor-browser

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

simulcast.js (7515B)


      1 "use strict";
      2 /* Helper functions to munge SDP and split the sending track into
      3 * separate tracks on the receiving end. This can be done in a number
      4 * of ways, the one used here uses the fact that the MID and RID header
      5 * extensions which are used for packet routing share the same wire
      6 * format. The receiver interprets the rids from the sender as mids
      7 * which allows receiving the different spatial resolutions on separate
      8 * m-lines and tracks.
      9 */
     10 
     11 // Borrowed from wpt, with some dependencies removed.
     12 
     13 const ridExtensions = [
     14  "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
     15  "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
     16 ];
     17 
     18 function ridToMid(description, rids) {
     19  const sections = SDPUtils.splitSections(description.sdp);
     20  const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]);
     21  const ice = SDPUtils.getIceParameters(sections[1], sections[0]);
     22  const rtpParameters = SDPUtils.parseRtpParameters(sections[1]);
     23  const setupValue = description.sdp.match(/a=setup:(.*)/)[1];
     24  const directionValue =
     25    description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) ||
     26    "a=sendrecv";
     27  const mline = SDPUtils.parseMLine(sections[1]);
     28 
     29  // Skip mid extension; we are replacing it with the rid extmap
     30  rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(
     31    ext => ext.uri != "urn:ietf:params:rtp-hdrext:sdes:mid"
     32  );
     33 
     34  for (const ext of rtpParameters.headerExtensions) {
     35    if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id") {
     36      ext.uri = "urn:ietf:params:rtp-hdrext:sdes:mid";
     37    }
     38  }
     39 
     40  // Filter rtx as we have no way to (re)interpret rrid.
     41  // Not doing this makes probing use RTX, it's not understood and ramp-up is slower.
     42  rtpParameters.codecs = rtpParameters.codecs.filter(
     43    c => c.name.toUpperCase() !== "RTX"
     44  );
     45 
     46  if (!rids) {
     47    rids = Array.from(description.sdp.matchAll(/a=rid:(.*) send/g)).map(
     48      r => r[1]
     49    );
     50  }
     51 
     52  let sdp =
     53    SDPUtils.writeSessionBoilerplate() +
     54    SDPUtils.writeDtlsParameters(dtls, setupValue) +
     55    SDPUtils.writeIceParameters(ice) +
     56    "a=group:BUNDLE " +
     57    rids.join(" ") +
     58    "\r\n";
     59  const baseRtpDescription = SDPUtils.writeRtpDescription(
     60    mline.kind,
     61    rtpParameters
     62  );
     63  for (const rid of rids) {
     64    sdp +=
     65      baseRtpDescription +
     66      "a=mid:" +
     67      rid +
     68      "\r\n" +
     69      "a=msid:rid-" +
     70      rid +
     71      " rid-" +
     72      rid +
     73      "\r\n";
     74    sdp += directionValue + "\r\n";
     75  }
     76  return sdp;
     77 }
     78 
     79 function midToRid(description, localDescription, rids) {
     80  const sections = SDPUtils.splitSections(description.sdp);
     81  const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]);
     82  const ice = SDPUtils.getIceParameters(sections[1], sections[0]);
     83  const rtpParameters = SDPUtils.parseRtpParameters(sections[1]);
     84  const setupValue = description.sdp.match(/a=setup:(.*)/)[1];
     85  const directionValue =
     86    description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) ||
     87    "a=sendrecv";
     88  const mline = SDPUtils.parseMLine(sections[1]);
     89 
     90  // Skip rid extensions; we are replacing them with the mid extmap
     91  rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(
     92    ext => !ridExtensions.includes(ext.uri)
     93  );
     94 
     95  for (const ext of rtpParameters.headerExtensions) {
     96    if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:mid") {
     97      ext.uri = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id";
     98    }
     99  }
    100 
    101  const localMid = localDescription
    102    ? SDPUtils.getMid(SDPUtils.splitSections(localDescription.sdp)[1])
    103    : "0";
    104 
    105  if (!rids) {
    106    rids = [];
    107    for (let i = 1; i < sections.length; i++) {
    108      rids.push(SDPUtils.getMid(sections[i]));
    109    }
    110  }
    111 
    112  let sdp =
    113    SDPUtils.writeSessionBoilerplate() +
    114    SDPUtils.writeDtlsParameters(dtls, setupValue) +
    115    SDPUtils.writeIceParameters(ice) +
    116    "a=group:BUNDLE " +
    117    localMid +
    118    "\r\n";
    119  sdp += SDPUtils.writeRtpDescription(mline.kind, rtpParameters);
    120  // Although we are converting mids to rids, we still need a mid.
    121  // The first one will be consistent with trickle ICE candidates.
    122  sdp += "a=mid:" + localMid + "\r\n";
    123  sdp += directionValue + "\r\n";
    124 
    125  for (const rid of rids) {
    126    const stringrid = String(rid); // allow integers
    127    const choices = stringrid.split(",");
    128    choices.forEach(choice => {
    129      sdp += "a=rid:" + choice + " recv\r\n";
    130    });
    131  }
    132  if (rids.length) {
    133    sdp += "a=simulcast:recv " + rids.join(";") + "\r\n";
    134  }
    135 
    136  return sdp;
    137 }
    138 
    139 async function doOfferToSendSimulcast(offerer, answerer) {
    140  await offerer.setLocalDescription();
    141 
    142  // Is this a renegotiation? If so, we cannot remove (or reorder!) any mids,
    143  // even if some rids have been removed or reordered.
    144  let mids = [];
    145  if (answerer.localDescription) {
    146    // Renegotiation. Mids must be the same as before, because renegotiation
    147    // can never remove or reorder mids, nor can it expand the simulcast
    148    // envelope.
    149    mids = [...answerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map(
    150      e => e[1]
    151    );
    152  } else {
    153    // First negotiation; the mids will be exactly the same as the rids
    154    const simulcastAttr = offerer.localDescription.sdp.match(
    155      /a=simulcast:send (.*)/
    156    );
    157    if (simulcastAttr) {
    158      mids = simulcastAttr[1].split(";");
    159    }
    160  }
    161 
    162  const nonSimulcastOffer = ridToMid(offerer.localDescription, mids);
    163  await answerer.setRemoteDescription({
    164    type: "offer",
    165    sdp: nonSimulcastOffer,
    166  });
    167 }
    168 
    169 async function doAnswerToRecvSimulcast(offerer, answerer, rids) {
    170  await answerer.setLocalDescription();
    171  const simulcastAnswer = midToRid(
    172    answerer.localDescription,
    173    offerer.localDescription,
    174    rids
    175  );
    176  await offerer.setRemoteDescription({ type: "answer", sdp: simulcastAnswer });
    177 }
    178 
    179 async function doOfferToRecvSimulcast(offerer, answerer, rids) {
    180  await offerer.setLocalDescription();
    181  const simulcastOffer = midToRid(
    182    offerer.localDescription,
    183    answerer.localDescription,
    184    rids
    185  );
    186  await answerer.setRemoteDescription({ type: "offer", sdp: simulcastOffer });
    187 }
    188 
    189 async function doAnswerToSendSimulcast(offerer, answerer) {
    190  await answerer.setLocalDescription();
    191 
    192  // See which mids the offerer had; it will barf if we remove or reorder them
    193  const mids = [...offerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map(
    194    e => e[1]
    195  );
    196 
    197  const nonSimulcastAnswer = ridToMid(answerer.localDescription, mids);
    198  await offerer.setRemoteDescription({
    199    type: "answer",
    200    sdp: nonSimulcastAnswer,
    201  });
    202 }
    203 
    204 async function doOfferToSendSimulcastAndAnswer(offerer, answerer, rids) {
    205  await doOfferToSendSimulcast(offerer, answerer);
    206  await doAnswerToRecvSimulcast(offerer, answerer, rids);
    207 }
    208 
    209 async function doOfferToRecvSimulcastAndAnswer(offerer, answerer, rids) {
    210  await doOfferToRecvSimulcast(offerer, answerer, rids);
    211  await doAnswerToSendSimulcast(offerer, answerer);
    212 }
    213 
    214 // This would be useful for cases other than simulcast, but we do not use it
    215 // anywhere else right now, nor do we have a place for wpt-friendly helpers at
    216 // the moment.
    217 function createPlaybackElement(track) {
    218  const elem = document.createElement(track.kind);
    219  elem.autoplay = true;
    220  elem.srcObject = new MediaStream([track]);
    221  elem.id = track.id;
    222  elem.width = 240;
    223  elem.height = 180;
    224  document.body.appendChild(elem);
    225  return elem;
    226 }
    227 
    228 async function getPlaybackWithLoadedMetadata(track) {
    229  const elem = createPlaybackElement(track);
    230  return new Promise(resolve => {
    231    elem.addEventListener("loadedmetadata", () => {
    232      resolve(elem);
    233    });
    234  });
    235 }