tor-browser

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

h264-unidirectional-codec-offer.https.html (12857B)


      1 <!doctype html>
      2 <meta charset=utf-8>
      3 <title>RTX codec integrity checks</title>
      4 <script src="/resources/testharness.js"></script>
      5 <script src="/resources/testharnessreport.js"></script>
      6 <script>
      7 'use strict';
      8 
      9 // Test unidirectional codec support.
     10 //
     11 // These tests ensure setCodecPreferences() and SDP negotiation work with
     12 // sendonly and recvonly codecs, i.e. using codec values that exist in
     13 // RTCRtpSender.getCapabilities() but not RTCRtpReceiver.getCapabilities() or
     14 // vice versa.
     15 //
     16 // While this is not necessarily unique to H264, these tests use H264 because
     17 // there are many popular devices where support is unidirectional. If the
     18 // prerequisite capabilities are not available to the test it ends in
     19 // [PRECONDITION_FAILED] rather than test failures.
     20 
     21 function containsCodec(codecs, codec) {
     22  return codecs.find(c => c.mimeType == codec.mimeType &&
     23                          c.sdpFmtpLine == codec.sdpFmtpLine) != null;
     24 }
     25 
     26 function getCodecsWithDirection(mimeType, direction) {
     27  const sendCodecs = RTCRtpSender.getCapabilities('video').codecs.filter(
     28      c => c.mimeType == mimeType);
     29  const recvCodecs = RTCRtpReceiver.getCapabilities('video').codecs.filter(
     30      c => c.mimeType == mimeType);
     31  const codecs = [];
     32  if (direction == 'sendrecv') {
     33    for (const sendCodec of sendCodecs) {
     34      if (containsCodec(recvCodecs, sendCodec)) {
     35        codecs.push(sendCodec);
     36      }
     37    }
     38  } else if (direction == 'sendonly') {
     39    for (const sendCodec of sendCodecs) {
     40      if (!containsCodec(recvCodecs, sendCodec)) {
     41        codecs.push(sendCodec);
     42      }
     43    }
     44  } else if (direction == 'recvonly') {
     45    for (const recvCodec of recvCodecs) {
     46      if (!containsCodec(sendCodecs, recvCodec)) {
     47        codecs.push(recvCodec);
     48      }
     49    }
     50  }
     51  return codecs;
     52 }
     53 
     54 // Returns an array of { mimeType, payloadType, sdpFmtpLine } entries in codec
     55 // preference order from the first m-section of the SDP.
     56 function parseCodecsFromSdp(sdp) {
     57  const codecs = [];
     58  const kMLineRegex = /\r\nm=video \d+ [A-Z\/]+ (?<pts>[\d\s]+)\r\n/;
     59  const {groups: {pts}} = kMLineRegex.exec(sdp);
     60  for (const pt of pts.split(" ")) {
     61    const rtpmapRegex = new RegExp(`\r\na=rtpmap:${pt} (?<name>[^ \/]+)\/`);
     62    const {groups: {name}} = rtpmapRegex.exec(sdp);
     63    const fmtpRegex = new RegExp(`\r\na=fmtp:${pt} (?<sdpFmtpLine>.*)\r\n`);
     64    // Guard against there not being an fmtp line.
     65    const {groups: {sdpFmtpLine} = {}} = fmtpRegex.exec(sdp) ?? {};
     66    const codec = { mimeType: `video/${name}`, payloadType: pt, sdpFmtpLine };
     67    codecs.push(codec);
     68  }
     69  return codecs;
     70 }
     71 
     72 function replaceCodecInSdp(sdp, oldCodec, newCodec) {
     73  // Replace the payload type in the m=video line.
     74  sdp = sdp.replace(
     75      new RegExp(`(m=video [ \\dA-Z\/]+)${oldCodec.payloadType}`),
     76      `$1${newCodec.payloadType}`
     77  );
     78  // Replace the payload type in all rtpmap, fmtp and rtcp-fb lines.
     79  sdp = sdp.replaceAll(
     80      new RegExp(`(a=(rtpmap|fmtp|rtcp-fb):)${oldCodec.payloadType}`, "g"),
     81      `$1${newCodec.payloadType}`
     82  );
     83  // Lastly, replace the actual "sdpFmtpLine" string.
     84  sdp = sdp.replace(oldCodec.sdpFmtpLine, newCodec.sdpFmtpLine);
     85  return sdp;
     86 }
     87 
     88 promise_test(async t => {
     89  const pc1 = new RTCPeerConnection();
     90  t.add_cleanup(() => pc1.close());
     91  const pc2 = new RTCPeerConnection();
     92  t.add_cleanup(() => pc2.close());
     93 
     94  // We need at least one recvonly codec to test "offer to receive".
     95  const recvOnlyCodecs = getCodecsWithDirection('video/H264', 'recvonly');
     96  // A sendrecv codec is used to ensure `pc2` has something to answer with which
     97  // makes modifying the SDP answer easier, but because we cannot send the
     98  // recvonly codec we have to fake it with remote SDP munging.
     99  const sendRecvCodecs = getCodecsWithDirection('video/H264', 'sendrecv');
    100  // If any of the following optional asserts fail the test ends with
    101  // [PRECONDITION_FAILED] as opposed to [FAIL].
    102  assert_implements_optional(
    103      recvOnlyCodecs.length > 0,
    104      `There are no recvonly H264 codecs available in getCapabilities.`);
    105  assert_implements_optional(
    106      sendRecvCodecs.length > 0,
    107      `There are no sendrecv H264 codecs available in getCapabilities.`);
    108  const recvOnlyCodec = recvOnlyCodecs[0];
    109  const sendRecvCodec = sendRecvCodecs[0];
    110 
    111  const pc1Transceiver = pc1.addTransceiver('video', {direction: 'recvonly'});
    112  pc1Transceiver.setCodecPreferences([recvOnlyCodec, sendRecvCodec]);
    113 
    114  // Offer to receive.
    115  await pc1.setLocalDescription();
    116  const offeredCodecs = parseCodecsFromSdp(pc1.localDescription.sdp);
    117  assert_equals(offeredCodecs.length, 2, 'Two codecs should be offered');
    118  assert_equals(offeredCodecs[0].mimeType, 'video/H264');
    119  assert_true(offeredCodecs[0].sdpFmtpLine == recvOnlyCodec.sdpFmtpLine,
    120              `The first offered codec's sdpFmtpLine is the recvonly one.`);
    121  assert_equals(offeredCodecs[1].mimeType, 'video/H264');
    122  assert_true(offeredCodecs[1].sdpFmtpLine == sendRecvCodec.sdpFmtpLine,
    123              `The second offered codec's sdpFmtpLine is the sendrecv one.`);
    124  await pc2.setRemoteDescription(pc1.localDescription);
    125 
    126  // Answer to send.
    127  const pc2Transceiver = pc2.getTransceivers()[0];
    128  pc2Transceiver.direction = 'sendonly';
    129  await pc2.setLocalDescription();
    130  // Verify that because we are not capable of sending the first codec, it has
    131  // been removed from the SDP answer.
    132  const answeredCodecs = parseCodecsFromSdp(pc2.localDescription.sdp);
    133  assert_equals(answeredCodecs.length, 1, 'One codec should be answered');
    134  assert_equals(answeredCodecs[0].mimeType, 'video/H264');
    135  assert_true(answeredCodecs[0].sdpFmtpLine == sendRecvCodec.sdpFmtpLine,
    136              `The answered codec's sdpFmtpLine is the sendrecv one.`);
    137  // Trick `pc1` into thinking `pc2` can send the codec by modifying the SDP.
    138  // Receiving media is not testable but this ensures that the SDP is accepted.
    139  const modifiedSdp = replaceCodecInSdp(
    140      pc2.localDescription.sdp, answeredCodecs[0], offeredCodecs[0]);
    141  await pc1.setRemoteDescription({type: 'answer', sdp: modifiedSdp});
    142 }, `Offer to receive a recvonly H264 codec on a recvonly transceiver`);
    143 
    144 promise_test(async t => {
    145  const pc = new RTCPeerConnection();
    146  t.add_cleanup(() => pc.close());
    147 
    148  const h264RecvOnlyCodecs = getCodecsWithDirection('video/H264', 'recvonly');
    149  const h264SendOnlyCodecs = getCodecsWithDirection('video/H264', 'sendonly');
    150  const vp8SendRecvCodecs = getCodecsWithDirection('video/VP8', 'sendrecv');
    151  // If any of the following optional asserts fail the test ends with
    152  // [PRECONDITION_FAILED] as opposed to [FAIL].
    153  assert_implements_optional(
    154      h264RecvOnlyCodecs.length > 0,
    155      `There are no recvonly H264 codecs available in getCapabilities.`);
    156  assert_implements_optional(
    157      vp8SendRecvCodecs.length > 0,
    158      `There are no sendrecv VP8 codecs available in getCapabilities.`);
    159  // Find a recvonly codec with the required level (3.1) for both sending and
    160  // receiving, that has a corresponding sendonly codec with the same profile
    161  // but a higher level. If there is such a match, we should be able to use the
    162  // lower level of the two for sendrecv.
    163  const kProfileLevelIdRegex =
    164    /profile-level-id=(?<profile_idc>..)(?<profile_iop>..)(?<level_idc>..)/;
    165  const kProfileLevelIdReqLevelRegex = /profile-level-id=....1f/;
    166  const h264RecvOnlyReqLevelCodecs = h264RecvOnlyCodecs.filter(
    167      codec => codec.sdpFmtpLine.match(kProfileLevelIdReqLevelRegex));
    168  const h264RecvOnlyCodec = h264RecvOnlyReqLevelCodecs.find(recv => {
    169    const {groups: {
    170                      profile_idc: recvProfile,
    171                      profile_iop: recvConstraints,
    172                      level_idc: recvLevelIdc,
    173                   }
    174          } = kProfileLevelIdRegex.exec(recv.sdpFmtpLine);
    175    const recvLevel = parseInt(recvLevelIdc, 16);
    176    return h264SendOnlyCodecs.find(send => {
    177      const {groups: {
    178                      profile_idc: sendProfile,
    179                      profile_iop: sendConstraints,
    180                      level_idc: sendLevelIdc,
    181                     }
    182            } = kProfileLevelIdRegex.exec(send.sdpFmtpLine);
    183      const sendLevel = parseInt(sendLevelIdc, 16);
    184      return sendProfile == recvProfile &&
    185             sendConstraints == recvConstraints &&
    186             sendLevelIdc > recvLevelIdc;
    187    });
    188  });
    189  assert_implements_optional(
    190      h264RecvOnlyCodec != undefined,
    191      `No recvonly profile-level-id=....1f that matches a higher level ` +
    192      `sendonly codec`);
    193  const vp8SendRecvCodec = vp8SendRecvCodecs[0];
    194 
    195  const transceiver = pc.addTransceiver('video', {direction: 'sendrecv'});
    196  transceiver.setCodecPreferences([h264RecvOnlyCodec, vp8SendRecvCodec]);
    197 
    198  await pc.setLocalDescription();
    199  const offeredCodecs = parseCodecsFromSdp(pc.localDescription.sdp);
    200  // Even though this H264 codec with its level ID is recvonly, we should still
    201  // offer to sendrecv it due to sender capabilities being even greater.
    202  assert_equals(offeredCodecs.length, 2, 'Two codecs are offered (H264, VP8).');
    203  assert_equals(offeredCodecs[0].mimeType, 'video/H264',
    204                'The first offered codec is H264.');
    205  assert_true(offeredCodecs[0].sdpFmtpLine == h264RecvOnlyCodec.sdpFmtpLine,
    206              'The offered H264 profile-level-id should match the recvonly ' +
    207              'codec since we expect the sender capability to be even higher.');
    208  assert_equals(offeredCodecs[1].mimeType, 'video/VP8',
    209                'The second offered codec is VP8.');
    210 }, `Offering a recvonly codec on a sendrecv transceiver`);
    211 
    212 promise_test(async t => {
    213  const pc1 = new RTCPeerConnection();
    214  t.add_cleanup(() => pc1.close());
    215  const pc2 = new RTCPeerConnection();
    216  t.add_cleanup(() => pc2.close());
    217 
    218  // We need at least one sendonly codec to test "offer to send".
    219  const sendOnlyCodecs = getCodecsWithDirection('video/H264', 'sendonly');
    220  // A sendrecv codec is used to ensure `pc2` has something to answer with which
    221  // makes modifying the SDP answer easier, but because we cannot receive the
    222  // sendonly codec we have to fake it with remote SDP munging.
    223  const sendRecvCodecs = getCodecsWithDirection('video/H264', 'sendrecv');
    224  // If any of the following optional asserts fail the test ends with
    225  // [PRECONDITION_FAILED] as opposed to [FAIL].
    226  assert_implements_optional(
    227      sendOnlyCodecs.length > 0,
    228      `There are no sendonly H264 codecs available in getCapabilities.`);
    229  assert_implements_optional(
    230      sendRecvCodecs.length > 0,
    231      `There are no sendrecv H264 codecs available in getCapabilities.`);
    232  const sendOnlyCodec = sendOnlyCodecs[0];
    233  const sendRecvCodec = sendRecvCodecs[0];
    234 
    235  const transceiver = pc1.addTransceiver('video', {direction: 'sendonly'});
    236  transceiver.setCodecPreferences([sendOnlyCodec, sendRecvCodec]);
    237 
    238  // Offer to send.
    239  await pc1.setLocalDescription();
    240  const offeredCodecs = parseCodecsFromSdp(pc1.localDescription.sdp);
    241  assert_equals(offeredCodecs.length, 2, 'Two codecs should be offered');
    242  assert_equals(offeredCodecs[0].mimeType, 'video/H264');
    243  assert_true(offeredCodecs[0].sdpFmtpLine == sendOnlyCodec.sdpFmtpLine,
    244              `The first offered codec's sdpFmtpLine is the sendonly one.`);
    245  assert_equals(offeredCodecs[1].mimeType, 'video/H264');
    246  assert_true(offeredCodecs[1].sdpFmtpLine == sendRecvCodec.sdpFmtpLine,
    247              `The second offered codec's sdpFmtpLine is the sendrecv one.`);
    248  await pc2.setRemoteDescription(pc1.localDescription);
    249 
    250  // Answer to receive.
    251  await pc2.setLocalDescription();
    252  // Verify that because we are not capable of receiving the first codec, it has
    253  // been removed from the SDP answer.
    254  const answeredCodecs = parseCodecsFromSdp(pc2.localDescription.sdp);
    255  assert_equals(answeredCodecs.length, 1, 'One codec should be answered');
    256  assert_equals(answeredCodecs[0].mimeType, 'video/H264');
    257  assert_true(answeredCodecs[0].sdpFmtpLine == sendRecvCodec.sdpFmtpLine,
    258              `The answered codec's sdpFmtpLine is the sendrecv one.`);
    259  // Trick `pc1` into thinking `pc2` can receive the codec by modifying the SDP.
    260  const modifiedSdp = replaceCodecInSdp(
    261      pc2.localDescription.sdp, answeredCodecs[0], offeredCodecs[0]);
    262  await pc1.setRemoteDescription({type: 'answer', sdp: modifiedSdp});
    263 
    264  // The sendonly codec is the only codec in the list of negotiated codecs.
    265  const params = transceiver.sender.getParameters();
    266  assert_equals(params.codecs.length, 1,
    267                `Only one codec should have been negotiated`);
    268  assert_equals(params.codecs[0].payloadType, offeredCodecs[0].payloadType,
    269                `The sendonly codec's payloadType shows up in getParameters()`);
    270  assert_true(params.codecs[0].sdpFmtpLine == offeredCodecs[0].sdpFmtpLine,
    271              `The sendonly codec's sdpFmtpLine shows up in getParameters()`);
    272 }, `Offer to send a sendonly H264 codec on a sendonly transceiver`);
    273 </script>