tor-browser

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

simulcast.js (10416B)


      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 const ridExtensions = [
     12  'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id',
     13  'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id',
     14 ];
     15 
     16 function ridToMid(description, rids) {
     17  const sections = SDPUtils.splitSections(description.sdp);
     18  const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]);
     19  const ice = SDPUtils.getIceParameters(sections[1], sections[0]);
     20  const rtpParameters = SDPUtils.parseRtpParameters(sections[1]);
     21  const setupValue = SDPUtils.matchPrefix(description.sdp, 'a=setup:')[0].substring(8);
     22  const direction = SDPUtils.getDirection(sections[1]);
     23  const mline = SDPUtils.parseMLine(sections[1]);
     24 
     25  // Skip mid extension; we are replacing it with the rid extmap
     26  rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(
     27    ext => ext.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'
     28  );
     29 
     30  for (const ext of rtpParameters.headerExtensions) {
     31    if (ext.uri === 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id') {
     32      ext.uri = 'urn:ietf:params:rtp-hdrext:sdes:mid';
     33    }
     34  }
     35 
     36  // Filter rtx as we have no way to (re)interpret rrid.
     37  // Not doing this makes probing use RTX, it's not understood and ramp-up is slower.
     38  rtpParameters.codecs = rtpParameters.codecs.filter(c => c.name.toUpperCase() !== 'RTX');
     39  if (!rids) {
     40    rids = SDPUtils.matchPrefix(sections[1], 'a=rid:')
     41      .filter(line => line.endsWith(' send'))
     42      .map(line => line.substring(6).split(' ')[0]);
     43  }
     44 
     45  let sdp = SDPUtils.writeSessionBoilerplate() +
     46    SDPUtils.writeDtlsParameters(dtls, setupValue) +
     47    SDPUtils.writeIceParameters(ice) +
     48    'a=group:BUNDLE ' + rids.join(' ') + '\r\n' +
     49    'a=msid-semantic: WMS *\r\n';
     50  const baseRtpDescription = SDPUtils.writeRtpDescription(mline.kind, rtpParameters);
     51  for (const rid of rids) {
     52    sdp += baseRtpDescription +
     53        'a=mid:' + rid + '\r\n' +
     54        'a=msid:rid-' + rid + ' rid-' + rid + '\r\n';
     55    sdp += 'a=' + direction + '\r\n';
     56  }
     57  return sdp;
     58 }
     59 
     60 function midToRid(description, localDescription, rids) {
     61  const sections = SDPUtils.splitSections(description.sdp);
     62  const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]);
     63  const ice = SDPUtils.getIceParameters(sections[1], sections[0]);
     64  const rtpParameters = SDPUtils.parseRtpParameters(sections[1]);
     65  const setupValue = description.sdp.match(/a=setup:(.*)/)[1];
     66  const direction = SDPUtils.getDirection(sections[1]);
     67  const mline = SDPUtils.parseMLine(sections[1]);
     68 
     69  // Skip rid extensions; we are replacing them with the mid extmap
     70  rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(
     71    ext => !ridExtensions.includes(ext.uri)
     72  );
     73 
     74  for (const ext of rtpParameters.headerExtensions) {
     75    if (ext.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid') {
     76      ext.uri = 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id';
     77    }
     78  }
     79 
     80  const localMid = localDescription ? SDPUtils.getMid(SDPUtils.splitSections(localDescription.sdp)[1]) : '0';
     81  if (localDescription) {
     82    const localVideoSection = SDPUtils.splitSections(localDescription.sdp)[1];
     83    const localParameters = SDPUtils.parseRtpParameters(localVideoSection);
     84 
     85    const localMidExtension = localParameters.headerExtensions
     86      .find(ext => ext.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid');
     87    if (localMidExtension) {
     88      rtpParameters.headerExtensions.push(localMidExtension);
     89    }
     90  } else {
     91    // Find unused id in remote description to formally have a mid.
     92    for (let id = 1; id < 15; id++) {
     93      if (rtpParameters.headerExtensions.find(ext => ext.id === id) === undefined) {
     94        rtpParameters.headerExtensions.push(
     95          {id, uri: 'urn:ietf:params:rtp-hdrext:sdes:mid'});
     96        break;
     97      }
     98    }
     99  }
    100 
    101  if (!rids) {
    102    rids = [];
    103    for (let i = 1; i < sections.length; i++) {
    104      rids.push(SDPUtils.getMid(sections[i]));
    105    }
    106  }
    107 
    108  let sdp = SDPUtils.writeSessionBoilerplate() +
    109    SDPUtils.writeDtlsParameters(dtls, setupValue) +
    110    SDPUtils.writeIceParameters(ice) +
    111    'a=group:BUNDLE ' + localMid + '\r\n' +
    112    'a=msid-semantic: WMS *\r\n';
    113  sdp += SDPUtils.writeRtpDescription(mline.kind, rtpParameters);
    114  // Although we are converting mids to rids, we still need a mid.
    115  // The first one will be consistent with trickle ICE candidates.
    116  sdp += 'a=mid:' + localMid + '\r\n';
    117  sdp += 'a=' + direction + '\r\n';
    118 
    119  for (const rid of rids) {
    120    const stringrid = String(rid); // allow integers
    121    const choices = stringrid.split(',');
    122    choices.forEach(choice => {
    123      sdp += 'a=rid:' + choice + ' recv\r\n';
    124    });
    125  }
    126  if (rids.length) {
    127    sdp += 'a=simulcast:recv ' + rids.join(';') + '\r\n';
    128  }
    129 
    130  return sdp;
    131 }
    132 
    133 async function doOfferToSendSimulcast(offerer, answerer) {
    134  await offerer.setLocalDescription();
    135 
    136  // Is this a renegotiation? If so, we cannot remove (or reorder!) any mids,
    137  // even if some rids have been removed or reordered.
    138  let mids = [];
    139  if (answerer.localDescription) {
    140    // Renegotiation. Mids must be the same as before, because renegotiation
    141    // can never remove or reorder mids, nor can it expand the simulcast
    142    // envelope.
    143    const sections = SDPUtils.splitSections(answerer.localDescription.sdp);
    144    sections.shift();
    145    mids = sections.map(section => SDPUtils.getMid(section));
    146  } else {
    147    // First negotiation; the mids will be exactly the same as the rids
    148    const simulcastAttr = SDPUtils.matchPrefix(offerer.localDescription.sdp,
    149      'a=simulcast:send ')[0];
    150    if (simulcastAttr) {
    151      mids = simulcastAttr.split(' ')[1].split(';');
    152    }
    153  }
    154 
    155  const nonSimulcastOffer = ridToMid(offerer.localDescription, mids);
    156  await answerer.setRemoteDescription({
    157    type: 'offer',
    158    sdp: nonSimulcastOffer,
    159  });
    160 }
    161 
    162 async function doAnswerToRecvSimulcast(offerer, answerer, rids) {
    163  await answerer.setLocalDescription();
    164  const simulcastAnswer = midToRid(
    165    answerer.localDescription,
    166    offerer.localDescription,
    167    rids
    168  );
    169  await offerer.setRemoteDescription({ type: 'answer', sdp: simulcastAnswer });
    170 }
    171 
    172 async function doOfferToRecvSimulcast(offerer, answerer, rids) {
    173  await offerer.setLocalDescription();
    174  const simulcastOffer = midToRid(
    175    offerer.localDescription,
    176    answerer.localDescription,
    177    rids
    178  );
    179  await answerer.setRemoteDescription({ type: 'offer', sdp: simulcastOffer });
    180 }
    181 
    182 async function doAnswerToSendSimulcast(offerer, answerer) {
    183  await answerer.setLocalDescription();
    184 
    185  // See which mids the offerer had; it will barf if we remove or reorder them.
    186  const sections = SDPUtils.splitSections(offerer.localDescription.sdp);
    187  sections.shift();
    188  const mids = sections.map(section => SDPUtils.getMid(section));
    189  let nonSimulcastAnswer = ridToMid(answerer.localDescription, mids);
    190  // Restore MID RTP header extension.
    191  const localParameters = SDPUtils.parseRtpParameters(sections[0]);
    192 
    193  const localMidExtension = localParameters.headerExtensions
    194    .find(ext => ext.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid');
    195  if (localMidExtension) {
    196    nonSimulcastAnswer += SDPUtils.writeExtmap(localMidExtension);
    197  }
    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 function swapRidAndMidExtensionsInSimulcastOffer(offer, rids) {
    215  return ridToMid(offer, rids);
    216 }
    217 
    218 function swapRidAndMidExtensionsInSimulcastAnswer(answer, localDescription, rids) {
    219  return midToRid(answer, localDescription, rids);
    220 }
    221 
    222 async function negotiateSimulcastAndWaitForVideo(
    223    t, stream, rids, pc1, pc2, codec, scalabilityMode = undefined) {
    224  exchangeIceCandidates(pc1, pc2);
    225 
    226  const metadataToBeLoaded = [];
    227  pc2.ontrack = (e) => {
    228    const stream = e.streams[0];
    229    const v = document.createElement('video');
    230    v.autoplay = true;
    231    v.srcObject = stream;
    232    v.id = stream.id
    233    metadataToBeLoaded.push(new Promise((resolve) => {
    234        v.addEventListener('loadedmetadata', () => {
    235            resolve();
    236        });
    237    }));
    238  };
    239 
    240  const sendEncodings = rids.map(rid => ({rid}));
    241  // Use a 2X downscale factor between each layer. To improve ramp-up time, the
    242  // top layer is scaled down by a factor 2. Smaller layer comes first. For
    243  // example if MediaStreamTrack is 720p and we want to send three layers we'll
    244  // get {90p, 180p, 360p}.
    245  let scaleResolutionDownBy = 2;
    246  for (let i = sendEncodings.length - 1; i >= 0; --i) {
    247    if (scalabilityMode) {
    248      sendEncodings[i].scalabilityMode = scalabilityMode;
    249    }
    250    sendEncodings[i].scaleResolutionDownBy = scaleResolutionDownBy;
    251    scaleResolutionDownBy *= 2;
    252  }
    253 
    254  const transceiver = pc1.addTransceiver(stream.getVideoTracks()[0], {
    255    streams: [stream],
    256    sendEncodings: sendEncodings,
    257  });
    258  if (codec) {
    259    preferCodec(transceiver, codec.mimeType, codec.sdpFmtpLine);
    260  }
    261 
    262  const offer = await pc1.createOffer();
    263  await pc1.setLocalDescription(offer),
    264  await pc2.setRemoteDescription({
    265    type: 'offer',
    266    sdp: swapRidAndMidExtensionsInSimulcastOffer(offer, rids),
    267  });
    268  const answer = await pc2.createAnswer();
    269  await pc2.setLocalDescription(answer);
    270  await pc1.setRemoteDescription({
    271    type: 'answer',
    272    sdp: swapRidAndMidExtensionsInSimulcastAnswer(answer, pc1.localDescription, rids),
    273  });
    274  assert_equals(metadataToBeLoaded.length, rids.length);
    275  return Promise.all(metadataToBeLoaded);
    276 }
    277 
    278 async function getCameraStream(t) {
    279  // Use getUserMedia as getNoiseStream does not have enough entropy to ramp-up.
    280  await setMediaPermission();
    281  const stream = await navigator.mediaDevices.getUserMedia({video: {width: 640, height: 480}});
    282  t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    283  return stream;
    284 }