|  | <!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. | 
|  | 'fecBytesReceived', // 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> |