summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webrtc-stats/supported-stats.https.html
blob: 677736f3cd8f33963c53d4fa9bee0c52f4826a5f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
<!doctype html>
<meta charset=utf-8>
<meta name="timeout" content="long">
<title>Support for all stats defined in WebRTC Stats</title>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script src="../webrtc/RTCPeerConnection-helper.js"></script>
<script src="/resources/WebIDLParser.js"></script>
<script>
'use strict';

// inspired from similar test for MTI stats in ../webrtc/RTCPeerConnection-mandatory-getStats.https.html

// From https://w3c.github.io/webrtc-stats/webrtc-stats.html#rtcstatstype-str*
const dictionaryNames = {
  "codec": "RTCCodecStats",
  "inbound-rtp": "RTCInboundRtpStreamStats",
  "outbound-rtp": "RTCOutboundRtpStreamStats",
  "remote-inbound-rtp": "RTCRemoteInboundRtpStreamStats",
  "remote-outbound-rtp": "RTCRemoteOutboundRtpStreamStats",
  "csrc": "RTCRtpContributingSourceStats",
  "peer-connection": "RTCPeerConnectionStats",
  "data-channel": "RTCDataChannelStats",
  "media-source": {
    audio: "RTCAudioSourceStats",
    video: "RTCVideoSourceStats"
  },
  "media-playout": "RTCAudioPlayoutStats",
  "sender": {
    audio: "RTCAudioSenderStats",
    video: "RTCVideoSenderStats"
  },
  "receiver": {
    audio: "RTCAudioReceiverStats",
    video: "RTCVideoReceiverStats",
  },
  "transport": "RTCTransportStats",
  "candidate-pair": "RTCIceCandidatePairStats",
  "local-candidate": "RTCIceCandidateStats",
  "remote-candidate": "RTCIceCandidateStats",
  "certificate": "RTCCertificateStats",
};

function isPropertyTestable(type, property) {
  // List of properties which are not testable by this test.
  // When adding something to this list, please explain why.
  const untestablePropertiesByType = {
    'candidate-pair': [
      'availableIncomingBitrate', // requires REMB, no TWCC.
    ],
    'certificate': [
      'issuerCertificateId', // we only use self-signed certificates.
    ],
    'local-candidate': [
      'url', // requires a STUN/TURN server.
      'relayProtocol', // requires a TURN server.
      'relatedAddress', // requires a STUN/TURN server.
      'relatedPort', // requires a STUN/TURN server.
    ],
    'remote-candidate': [
      'url', // requires a STUN/TURN server.
      'relayProtocol', // requires a TURN server.
      'relatedAddress', // requires a STUN/TURN server.
      'relatedPort', // requires a STUN/TURN server.
      'tcpType', // requires ICE-TCP connection.
    ],
    'outbound-rtp': [
      'rid', // requires simulcast.
    ],
    'inbound-rtp': [
      'fecSsrc', // requires FlexFEC to be negotiated.
    ],
    'media-source': [
      'echoReturnLoss', // requires gUM with an audio input device.
      'echoReturnLossEnhancement', // requires gUM with an audio input device.
    ]
  };
  if (!untestablePropertiesByType[type]) {
    return true;
  }
  return !untestablePropertiesByType[type].includes(property);
}

async function getAllStats(t, pc) {
  // Try to obtain as many stats as possible, waiting up to 20 seconds for
  // roundTripTime which can take several RTCP messages to calculate.
  let stats;
  for (let i = 0; i < 20; i++) {
    stats = await pc.getStats();
    const values = [...stats.values()];
    const [remoteInboundAudio, remoteInboundVideo] =
        ["audio", "video"].map(kind =>
            values.find(s => s.type == "remote-inbound-rtp" && s.kind == kind));
    const [remoteOutboundAudio, remoteOutboundVideo] =
        ["audio", "video"].map(kind =>
            values.find(s => s.type == "remote-outbound-rtp" && s.kind == kind));
    // We expect both audio and video remote-inbound-rtp RTT.
    const hasRemoteInbound =
        remoteInboundAudio && "roundTripTime" in remoteInboundAudio &&
        remoteInboundVideo && "roundTripTime" in remoteInboundVideo;
    // Due to current implementation limitations, we don't put as hard
    // requirements on remote-outbound-rtp as remote-inbound-rtp. It's enough if
    // it is available for either kind and `roundTripTime` is not required. In
    // Chromium, remote-outbound-rtp is only implemented for audio and
    // `roundTripTime` is missing in this test, but awaiting for any
    // remote-outbound-rtp avoids flaky failures.
    const hasRemoteOutbound = remoteOutboundAudio || remoteOutboundVideo;
    const hasMediaPlayout = values.find(({type}) => type == "media-playout") != undefined;
    if (hasRemoteInbound && hasRemoteOutbound && hasMediaPlayout) {
      return stats;
    }
    await new Promise(r => t.step_timeout(r, 1000));
  }
  return stats;
}

promise_test(async t => {
  // load the IDL to know which members to be looking for
  const idl = await fetch("/interfaces/webrtc-stats.idl").then(r => r.text());
  // for RTCStats definition
  const webrtcIdl = await fetch("/interfaces/webrtc.idl").then(r => r.text());
  const astArray = WebIDL2.parse(idl + webrtcIdl);

  let all = {};
  for (let type in dictionaryNames) {
      // TODO: make use of audio/video distinction
    let dictionaries = dictionaryNames[type].audio ? Object.values(dictionaryNames[type]) : [dictionaryNames[type]];
    all[type] = [];
    let i = 0;
    // Recursively collect members from inherited dictionaries
    while (i < dictionaries.length) {
      const dictName = dictionaries[i];
      const dict = astArray.find(i => i.name === dictName && i.type === "dictionary");
      if (dict && dict.members) {
        all[type] = all[type].concat(dict.members.map(m => m.name));
        if (dict.inheritance) {
          dictionaries.push(dict.inheritance);
        }
      }
      i++;
    }
    // Unique-ify
    all[type] = [...new Set(all[type])];
  }

  const remaining = JSON.parse(JSON.stringify(all));
  for (const type in remaining) {
    remaining[type] = new Set(remaining[type]);
  }

  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  const dc1 = pc1.createDataChannel("dummy", {negotiated: true, id: 0});
  const dc2 = pc2.createDataChannel("dummy", {negotiated: true, id: 0});
  // Use a real gUM to ensure that all stats exposing hardware capabilities are
  // also exposed.
  const stream = await navigator.mediaDevices.getUserMedia(
    {video: true, audio: true});
  for (const track of stream.getTracks()) {
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);
    t.add_cleanup(() => track.stop());
  }

  // Do a non-trickle ICE handshake to ensure that TCP candidates are gathered.
  await pc1.setLocalDescription();
  await waitForIceGatheringState(pc1, ['complete']);
  await pc2.setRemoteDescription(pc1.localDescription);
  await pc2.setLocalDescription();
  await waitForIceGatheringState(pc2, ['complete']);
  await pc1.setRemoteDescription(pc2.localDescription);
  // Await the DTLS handshake.
  await Promise.all([
    listenToConnected(pc1),
    listenToConnected(pc2),
  ]);
  const stats = await getAllStats(t, pc1);

  // The focus of this test is that there are no dangling references,
  // i.e. keys ending with `Id` as described in
  // https://w3c.github.io/webrtc-stats/#guidelines-for-design-of-stats-objects
  test(t => {
    for (const stat of stats.values()) {
      Object.keys(stat).forEach(key => {
        if (!key.endsWith('Id')) return;
        assert_true(stats.has(stat[key]), `${stat.type}.${key} can be resolved`);
      });
    }
  }, 'All references resolve');

  // The focus of this test is not API correctness, but rather to provide an
  // accessible metric of implementation progress by dictionary member. We count
  // whether we've seen each dictionary's members in getStats().

  test(t => {
    for (const stat of stats.values()) {
      if (all[stat.type]) {
        const memberNames = all[stat.type];
        const remainingNames = remaining[stat.type];
        assert_true(memberNames.length > 0, "Test error. No member found.");
        for (const memberName of memberNames) {
          if (memberName in stat) {
            assert_not_equals(stat[memberName], undefined, "Not undefined");
            remainingNames.delete(memberName);
          }
        }
      }
    }
  }, "Validating stats");

  for (const type in all) {
    for (const memberName of all[type]) {
      test(t => {
        assert_implements_optional(isPropertyTestable(type, memberName),
          `${type}.${memberName} marked as not testable.`);
        assert_true(!remaining[type].has(memberName),
                    `Is ${memberName} present`);
      }, `${type}'s ${memberName}`);
    }
  }
}, 'getStats succeeds');
</script>