| <!doctype html> |
| <meta charset=utf-8> |
| <meta name="timeout" content="long"> |
| <title>Mandatory-to-implement stats compliance (a subset of webrtc-stats)</title> |
| <script src=/resources/testharness.js></script> |
| <script src=/resources/testharnessreport.js></script> |
| <script src="RTCPeerConnection-helper.js"></script> |
| <script src="dictionary-helper.js"></script> |
| <script src="RTCStats-helper.js"></script> |
| <script> |
| 'use strict'; |
| |
| // From https://w3c.github.io/webrtc-pc/#mandatory-to-implement-stats |
| |
| const mandatory = { |
| RTCRtpStreamStats: [ |
| "ssrc", |
| "kind", |
| "transportId", |
| "codecId", |
| ], |
| RTCReceivedRtpStreamStats: [ |
| "packetsReceived", |
| "packetsLost", |
| "jitter", |
| "packetsDiscarded", |
| ], |
| RTCInboundRtpStreamStats: [ |
| "trackId", |
| "receiverId", |
| "remoteId", |
| "framesDecoded", |
| ], |
| RTCRemoteInboundRtpStreamStats: [ |
| "localId", |
| "roundTripTime", |
| ], |
| RTCSentRtpStreamStats: [ |
| "packetsSent", |
| "bytesSent" |
| ], |
| RTCOutboundRtpStreamStats: [ |
| "trackId", |
| "senderId", |
| "remoteId", |
| "framesEncoded", |
| ], |
| RTCRemoteOutboundRtpStreamStats: [ |
| "localId", |
| "remoteTimestamp", |
| ], |
| RTCPeerConnectionStats: [ |
| "dataChannelsOpened", |
| "dataChannelsClosed", |
| ], |
| RTCDataChannelStats: [ |
| "protocol", |
| "dataChannelIdentifier", |
| "state", |
| "messagesSent", |
| "bytesSent", |
| "messagesReceived", |
| "bytesReceived", |
| ], |
| RTCMediaStreamStats: [ |
| "streamIdentifer", |
| "trackIds", |
| ], |
| RTCMediaHandlerStats: [ |
| "trackIdentifier", |
| "remoteSource", |
| "ended", |
| ], |
| RTCAudioHandlerStats: [ |
| "audioLevel", |
| ], |
| RTCVideoHandlerStats: [ |
| "frameWidth", |
| "frameHeight", |
| "framesPerSecond", |
| ], |
| RTCVideoSenderStats: [ |
| "framesSent", |
| ], |
| RTCVideoReceiverStats: [ |
| "framesReceived", |
| "framesDecoded", |
| "framesDropped", |
| "partialFramesLost", |
| ], |
| RTCCodecStats: [ |
| "payloadType", |
| "codecType", |
| "clockRate", |
| "channels", |
| "sdpFmtpLine", |
| ], |
| RTCTransportStats: [ |
| "bytesSent", |
| "bytesReceived", |
| "rtcpTransportStatsId", |
| "selectedCandidatePairId", |
| "localCertificateId", |
| "remoteCertificateId", |
| ], |
| RTCIceCandidatePairStats: [ |
| "transportId", |
| "localCandidateId", |
| "remoteCandidateId", |
| "state", |
| "priority", |
| "nominated", |
| "bytesSent", |
| "bytesReceived", |
| "totalRoundTripTime", |
| "currentRoundTripTime", |
| ], |
| RTCIceCandidateStats: [ |
| "address", |
| "port", |
| "protocol", |
| "candidateType", |
| "url", |
| ], |
| RTCCertificateStats: [ |
| "fingerprint", |
| "fingerprintAlgorithm", |
| "base64Certificate", |
| "issuerCertificateId", |
| ], |
| }; |
| |
| // 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", |
| "stream": "RTCMediaStreamStats", |
| "track": { |
| video: "RTCSenderVideoTrackAttachmentStats", |
| audio: "RTCSenderAudioTrackAttachmentStats" |
| }, |
| "sender": { |
| audio: "RTCAudioSenderStats", |
| video: "RTCVideoSenderStats" |
| }, |
| "receiver": { |
| audio: "RTCAudioReceiverStats", |
| video: "RTCVideoReceiverStats", |
| }, |
| "transport": "RTCTransportStats", |
| "candidate-pair": "RTCIceCandidatePairStats", |
| "local-candidate": "RTCIceCandidateStats", |
| "remote-candidate": "RTCIceCandidateStats", |
| "certificate": "RTCCertificateStats", |
| }; |
| |
| // From https://w3c.github.io/webrtc-stats/webrtc-stats.html (webidl) |
| |
| const parents = { |
| RTCVideoHandlerStats: "RTCMediaHandlerStats", |
| RTCVideoSenderStats: "RTCVideoHandlerStats", |
| RTCSenderVideoTrackAttachmentStats: "RTCVideoSenderStats", |
| RTCVideoReceiverStats: "RTCVideoHandlerStats", |
| RTCAudioHandlerStats: "RTCMediaHandlerStats", |
| RTCAudioSenderStats: "RTCAudioHandlerStats", |
| RTCSenderAudioTrackAttachmentStats: "RTCAudioSenderStats", |
| RTCAudioReceiverStats: "RTCAudioHandlerStats", |
| RTCReceivedRtpStreamStats: "RTCRtpStreamStats", |
| RTCInboundRtpStreamStats: "RTCReceivedRtpStreamStats", |
| RTCRemoteInboundRtpStreamStats: "RTCReceivedRtpStreamStats", |
| RTCSentRtpStreamStats: "RTCRtpStreamStats", |
| RTCOutboundRtpStreamStats: "RTCSentRtpStreamStats", |
| RTCRemoteOutboundRtpStreamStats : "RTCSentRtpStreamStats", |
| }; |
| |
| const remaining = JSON.parse(JSON.stringify(mandatory)); |
| for (const dictName in remaining) { |
| remaining[dictName] = new Set(remaining[dictName]); |
| } |
| |
| 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 [audio, video] = ["audio", "video"].map(kind => |
| values.find(s => s.type == "remote-inbound-rtp" && s.kind == kind)); |
| if (audio && "roundTripTime" in audio && |
| video && "roundTripTime" in video) { |
| return stats; |
| } |
| await new Promise(r => t.step_timeout(r, 1000)); |
| } |
| return stats; |
| } |
| |
| let succeed; |
| const success = new Promise(r => succeed = r); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| pc1.createDataChannel("dummy", {negotiated: true, id: 0}); |
| pc2.createDataChannel("dummy", {negotiated: true, id: 0}); |
| const stream = await getNoiseStream({video: true, audio:true}); |
| for (const track of stream.getTracks()) { |
| pc1.addTrack(track, stream); |
| pc2.addTrack(track, stream); |
| t.add_cleanup(() => track.stop()); |
| } |
| exchangeIceCandidates(pc1, pc2); |
| await doSignalingHandshake(pc1, 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 mandatory members in getStats(). |
| |
| for (const stat of stats.values()) { |
| let dictName = dictionaryNames[stat.type]; |
| if (!dictName) continue; |
| if (typeof dictName == "object") { |
| dictName = dictName[stat.kind]; |
| } |
| assert_equals(typeof dictName, "string", "Test error. String."); |
| if (dictName && mandatory[dictName]) { |
| do { |
| const memberNames = mandatory[dictName]; |
| const remainingNames = remaining[dictName]; |
| assert_true(memberNames.length > 0, "Test error. Parent not found."); |
| for (const memberName of memberNames) { |
| if (memberName in stat) { |
| assert_not_equals(stat[memberName], undefined, "Not undefined"); |
| remainingNames.delete(memberName); |
| } |
| } |
| dictName = parents[dictName]; |
| } while (dictName); |
| } |
| } |
| succeed(); |
| }, 'getStats succeeds'); |
| |
| for (const dictName in mandatory) { |
| for (const memberName of mandatory[dictName]) { |
| promise_test(async t => { |
| await success; |
| assert_true(!remaining[dictName].has(memberName), |
| `Is ${memberName} present`); |
| }, `${dictName}'s ${memberName}`); |
| } |
| } |
| |
| </script> |