tor-browser

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

supported-stats.https.html (8587B)


      1 <!doctype html>
      2 <meta charset=utf-8>
      3 <meta name="timeout" content="long">
      4 <title>Support for all stats defined in WebRTC Stats</title>
      5 <script src=/resources/testharness.js></script>
      6 <script src=/resources/testharnessreport.js></script>
      7 <script src="../webrtc/RTCPeerConnection-helper.js"></script>
      8 <script src="/resources/WebIDLParser.js"></script>
      9 <script>
     10 'use strict';
     11 
     12 // inspired from similar test for MTI stats in ../webrtc/RTCPeerConnection-mandatory-getStats.https.html
     13 
     14 // From https://w3c.github.io/webrtc-stats/webrtc-stats.html#rtcstatstype-str*
     15 const dictionaryNames = {
     16  "codec": "RTCCodecStats",
     17  "inbound-rtp": "RTCInboundRtpStreamStats",
     18  "outbound-rtp": "RTCOutboundRtpStreamStats",
     19  "remote-inbound-rtp": "RTCRemoteInboundRtpStreamStats",
     20  "remote-outbound-rtp": "RTCRemoteOutboundRtpStreamStats",
     21  "csrc": "RTCRtpContributingSourceStats",
     22  "peer-connection": "RTCPeerConnectionStats",
     23  "data-channel": "RTCDataChannelStats",
     24  "media-source": {
     25    audio: "RTCAudioSourceStats",
     26    video: "RTCVideoSourceStats"
     27  },
     28  "media-playout": "RTCAudioPlayoutStats",
     29  "sender": {
     30    audio: "RTCAudioSenderStats",
     31    video: "RTCVideoSenderStats"
     32  },
     33  "receiver": {
     34    audio: "RTCAudioReceiverStats",
     35    video: "RTCVideoReceiverStats",
     36  },
     37  "transport": "RTCTransportStats",
     38  "candidate-pair": "RTCIceCandidatePairStats",
     39  "local-candidate": "RTCIceCandidateStats",
     40  "remote-candidate": "RTCIceCandidateStats",
     41  "certificate": "RTCCertificateStats",
     42 };
     43 
     44 function isPropertyTestable(type, property) {
     45  // List of properties which are not testable by this test.
     46  // When adding something to this list, please explain why.
     47  const untestablePropertiesByType = {
     48    'candidate-pair': [
     49      'availableIncomingBitrate', // requires REMB, no TWCC.
     50    ],
     51    'certificate': [
     52      'issuerCertificateId', // we only use self-signed certificates.
     53    ],
     54    'local-candidate': [
     55      'url', // requires a STUN/TURN server.
     56      'relayProtocol', // requires a TURN server.
     57      'relatedAddress', // requires a STUN/TURN server.
     58      'relatedPort', // requires a STUN/TURN server.
     59    ],
     60    'remote-candidate': [
     61      'url', // requires a STUN/TURN server.
     62      'relayProtocol', // requires a TURN server.
     63      'relatedAddress', // requires a STUN/TURN server.
     64      'relatedPort', // requires a STUN/TURN server.
     65      'tcpType', // requires ICE-TCP connection.
     66    ],
     67    'outbound-rtp': [
     68      'rid', // requires simulcast.
     69    ],
     70    'inbound-rtp': [
     71      'fecSsrc', // requires FlexFEC to be negotiated.
     72      'fecBytesReceived', // requires FlexFEC to be negotiated.
     73    ],
     74    'media-source': [
     75      'echoReturnLoss', // requires gUM with an audio input device.
     76      'echoReturnLossEnhancement', // requires gUM with an audio input device.
     77    ]
     78  };
     79  if (!untestablePropertiesByType[type]) {
     80    return true;
     81  }
     82  return !untestablePropertiesByType[type].includes(property);
     83 }
     84 
     85 async function getAllStats(t, pc) {
     86  // Try to obtain as many stats as possible, waiting up to 20 seconds for
     87  // roundTripTime which can take several RTCP messages to calculate.
     88  let stats;
     89  for (let i = 0; i < 20; i++) {
     90    stats = await pc.getStats();
     91    const values = [...stats.values()];
     92    const [remoteInboundAudio, remoteInboundVideo] =
     93        ["audio", "video"].map(kind =>
     94            values.find(s => s.type == "remote-inbound-rtp" && s.kind == kind));
     95    const [remoteOutboundAudio, remoteOutboundVideo] =
     96        ["audio", "video"].map(kind =>
     97            values.find(s => s.type == "remote-outbound-rtp" && s.kind == kind));
     98    // We expect both audio and video remote-inbound-rtp RTT.
     99    const hasRemoteInbound =
    100        remoteInboundAudio && "roundTripTime" in remoteInboundAudio &&
    101        remoteInboundVideo && "roundTripTime" in remoteInboundVideo;
    102    // Due to current implementation limitations, we don't put as hard
    103    // requirements on remote-outbound-rtp as remote-inbound-rtp. It's enough if
    104    // it is available for either kind and `roundTripTime` is not required. In
    105    // Chromium, remote-outbound-rtp is only implemented for audio and
    106    // `roundTripTime` is missing in this test, but awaiting for any
    107    // remote-outbound-rtp avoids flaky failures.
    108    const hasRemoteOutbound = remoteOutboundAudio || remoteOutboundVideo;
    109    const hasMediaPlayout = values.find(({type}) => type == "media-playout") != undefined;
    110    if (hasRemoteInbound && hasRemoteOutbound && hasMediaPlayout) {
    111      return stats;
    112    }
    113    await new Promise(r => t.step_timeout(r, 1000));
    114  }
    115  return stats;
    116 }
    117 
    118 promise_test(async t => {
    119  // load the IDL to know which members to be looking for
    120  const idl = await fetch("/interfaces/webrtc-stats.idl").then(r => r.text());
    121  // for RTCStats definition
    122  const webrtcIdl = await fetch("/interfaces/webrtc.idl").then(r => r.text());
    123  const astArray = WebIDL2.parse(idl + webrtcIdl);
    124 
    125  let all = {};
    126  for (let type in dictionaryNames) {
    127      // TODO: make use of audio/video distinction
    128    let dictionaries = dictionaryNames[type].audio ? Object.values(dictionaryNames[type]) : [dictionaryNames[type]];
    129    all[type] = [];
    130    let i = 0;
    131    // Recursively collect members from inherited dictionaries
    132    while (i < dictionaries.length) {
    133      const dictName = dictionaries[i];
    134      const dict = astArray.find(i => i.name === dictName && i.type === "dictionary");
    135      if (dict && dict.members) {
    136        all[type] = all[type].concat(dict.members.map(m => m.name));
    137        if (dict.inheritance) {
    138          dictionaries.push(dict.inheritance);
    139        }
    140      }
    141      i++;
    142    }
    143    // Unique-ify
    144    all[type] = [...new Set(all[type])];
    145  }
    146 
    147  const remaining = JSON.parse(JSON.stringify(all));
    148  for (const type in remaining) {
    149    remaining[type] = new Set(remaining[type]);
    150  }
    151 
    152  const pc1 = new RTCPeerConnection();
    153  t.add_cleanup(() => pc1.close());
    154  const pc2 = new RTCPeerConnection();
    155  t.add_cleanup(() => pc2.close());
    156 
    157  const dc1 = pc1.createDataChannel("dummy", {negotiated: true, id: 0});
    158  const dc2 = pc2.createDataChannel("dummy", {negotiated: true, id: 0});
    159  // Use a real gUM to ensure that all stats exposing hardware capabilities are
    160  // also exposed.
    161  const stream = await navigator.mediaDevices.getUserMedia(
    162    {video: true, audio: true});
    163  for (const track of stream.getTracks()) {
    164    pc1.addTrack(track, stream);
    165    pc2.addTrack(track, stream);
    166    t.add_cleanup(() => track.stop());
    167  }
    168 
    169  // Do a non-trickle ICE handshake to ensure that TCP candidates are gathered.
    170  await pc1.setLocalDescription();
    171  await waitForIceGatheringState(pc1, ['complete']);
    172  await pc2.setRemoteDescription(pc1.localDescription);
    173  await pc2.setLocalDescription();
    174  await waitForIceGatheringState(pc2, ['complete']);
    175  await pc1.setRemoteDescription(pc2.localDescription);
    176  // Await the DTLS handshake.
    177  await Promise.all([
    178    listenToConnected(pc1),
    179    listenToConnected(pc2),
    180  ]);
    181  const stats = await getAllStats(t, pc1);
    182 
    183  // The focus of this test is that there are no dangling references,
    184  // i.e. keys ending with `Id` as described in
    185  // https://w3c.github.io/webrtc-stats/#guidelines-for-design-of-stats-objects
    186  test(t => {
    187    for (const stat of stats.values()) {
    188      Object.keys(stat).forEach(key => {
    189        if (!key.endsWith('Id')) return;
    190        assert_true(stats.has(stat[key]), `${stat.type}.${key} can be resolved`);
    191      });
    192    }
    193  }, 'All references resolve');
    194 
    195  // The focus of this test is not API correctness, but rather to provide an
    196  // accessible metric of implementation progress by dictionary member. We count
    197  // whether we've seen each dictionary's members in getStats().
    198 
    199  test(t => {
    200    for (const stat of stats.values()) {
    201      if (all[stat.type]) {
    202        const memberNames = all[stat.type];
    203        const remainingNames = remaining[stat.type];
    204        assert_true(memberNames.length > 0, "Test error. No member found.");
    205        for (const memberName of memberNames) {
    206          if (memberName in stat) {
    207            assert_not_equals(stat[memberName], undefined, "Not undefined");
    208            remainingNames.delete(memberName);
    209          }
    210        }
    211      }
    212    }
    213  }, "Validating stats");
    214 
    215  for (const type in all) {
    216    for (const memberName of all[type]) {
    217      test(t => {
    218        assert_implements_optional(isPropertyTestable(type, memberName),
    219          `${type}.${memberName} marked as not testable.`);
    220        assert_true(!remaining[type].has(memberName),
    221                    `Is ${memberName} present`);
    222      }, `${type}'s ${memberName}`);
    223    }
    224  }
    225 }, 'getStats succeeds');
    226 </script>