| <!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="../webrtc/dictionary-helper.js"></script> |
| <script src="../webrtc/RTCStats-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. |
| ], |
| '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 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> |