<!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",
  ],
  RTCInboundRtpStreamStats: [
    "trackIdentifier",
    "remoteId",
    "framesDecoded",
    "framesDropped",
    "nackCount",
    "framesReceived",
    "bytesReceived",
    "totalAudioEnergy",
    "totalSamplesDuration",
    "packetsDiscarded",
  ],
  RTCRemoteInboundRtpStreamStats: [
    "localId",
    "roundTripTime",
  ],
  RTCSentRtpStreamStats: [
    "packetsSent",
    "bytesSent"
  ],
  RTCOutboundRtpStreamStats: [
    "remoteId",
    "framesEncoded",
    "nackCount",
    "framesSent"
  ],
  RTCRemoteOutboundRtpStreamStats: [
    "localId",
    "remoteTimestamp",
  ],
  RTCPeerConnectionStats: [
    "dataChannelsOpened",
    "dataChannelsClosed",
  ],
  RTCDataChannelStats: [
    "label",
    "protocol",
    "dataChannelIdentifier",
    "state",
    "messagesSent",
    "bytesSent",
    "messagesReceived",
    "bytesReceived",
  ],
  RTCMediaSourceStats: [
    "trackIdentifier",
     "kind"
  ],
  RTCAudioSourceStats: [
    "totalAudioEnergy",
     "totalSamplesDuration"
  ],
  RTCVideoSourceStats: [
    "width",
    "height",
    "framesPerSecond"
  ],
  RTCCodecStats: [
    "payloadType",
    /* codecType is part of MTI but is not systematically set
       per https://www.w3.org/TR/webrtc-stats/#dom-rtccodecstats-codectype
       If the dictionary member is not present, it means that
      this media format can be both encoded and decoded. */
    // "codecType",
    "mimeType",
    "clockRate",
    "channels",
    "sdpFmtpLine",
  ],
  RTCTransportStats: [
    "bytesSent",
    "bytesReceived",
    "selectedCandidatePairId",
    "localCertificateId",
    "remoteCertificateId",
  ],
  RTCIceCandidatePairStats: [
    "transportId",
    "localCandidateId",
    "remoteCandidateId",
    "state",
    "nominated",
    "bytesSent",
    "bytesReceived",
    "totalRoundTripTime",
    "currentRoundTripTime"
  ],
  RTCIceCandidateStats: [
    "address",
    "port",
    "protocol",
    "candidateType",
    "url",
  ],
  RTCCertificateStats: [
    "fingerprint",
    "fingerprintAlgorithm",
    "base64Certificate",
    /* issuerCertificateId is part of MTI but is not systematically set
       per https://www.w3.org/TR/webrtc-stats/#dom-rtccertificatestats-issuercertificateid
       If the current certificate is at the end of the chain
       (i.e. a self-signed certificate), this will not be set. */
    // "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",
  "media-source": {
    audio: "RTCAudioSourceStats",
    video: "RTCVideoSourceStats"
  },
  "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 = {
  RTCVideoSourceStats: "RTCMediaSourceStats",
  RTCAudioSourceStats: "RTCMediaSourceStats",
  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 of RTCRemoteInboundRtpStreamStats and
  // remoteTimestamp of RTCRemoteOutboundRtpStreamStats which can take
  // several RTCP messages to calculate.
  let stats;
  let remoteInboundFound = false;
  let remoteOutboundFound = false;
  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));
    if (remoteInboundAudio && "roundTripTime" in remoteInboundAudio &&
        remoteInboundVideo && "roundTripTime" in remoteInboundVideo) {
      remoteInboundFound = true;
    }
    const [remoteOutboundAudio, remoteOutboundVideo] = ["audio", "video"].map(
      kind => values.find(s =>
        s.type == "remote-outbound-rtp" && s.kind == kind));
    if (remoteOutboundAudio && "remoteTimestamp" in remoteOutboundAudio &&
        remoteOutboundVideo && "remoteTimestamp" in remoteOutboundVideo) {
      remoteOutboundFound = true;
    }
    if (remoteInboundFound && remoteOutboundFound) {
      return stats;
    }
    await new Promise(r => t.step_timeout(r, 1000));
  }
  return stats;
}

promise_test(async t => {
  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});

  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 exchangeOfferAnswer(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().

  test(t => {
    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);
      }
    }
  }, "Validating stats");

  for (const dictName in mandatory) {
    for (const memberName of mandatory[dictName]) {
      test(t => {
        assert_true(!remaining[dictName].has(memberName),
                    `Is ${memberName} present`);
      }, `${dictName}'s ${memberName}`);
    }
  }
}, 'getStats succeeds');

</script>
