Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  stats.js   Sprache: JAVA

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


"use strict";

const statsExpectedByType = {
  "inbound-rtp": {
    expected: [
      "trackIdentifier",
      "id",
      "mid",
      "timestamp",
      "type",
      "ssrc",
      "mediaType",
      "kind",
      "codecId",
      "packetsReceived",
      "packetsLost",
      "packetsDiscarded",
      "bytesReceived",
      "jitter",
      "lastPacketReceivedTimestamp",
      "headerBytesReceived",
      // Always missing from libwebrtc stats
      // "estimatedPlayoutTimestamp",
      "jitterBufferDelay",
      "jitterBufferEmittedCount",
    ],
    optional: ["remoteId""nackCount""qpSum"],
    localVideoOnly: [
      "firCount",
      "pliCount",
      "framesDecoded",
      "framesDropped",
      "discardedPackets",
      "framesPerSecond",
      "frameWidth",
      "frameHeight",
      "framesReceived",
      "totalDecodeTime",
      "totalInterFrameDelay",
      "totalProcessingDelay",
      "totalSquaredInterFrameDelay",
    ],
    localAudioOnly: [
      "totalSamplesReceived",
      // libwebrtc doesn't seem to do FEC for video
      "fecPacketsReceived",
      "fecPacketsDiscarded",
      "concealedSamples",
      "silentConcealedSamples",
      "concealmentEvents",
      "insertedSamplesForDeceleration",
      "removedSamplesForAcceleration",
      "audioLevel",
      "totalAudioEnergy",
      "totalSamplesDuration",
    ],
    unimplemented: [
      "mediaTrackId",
      "transportId",
      "associateStatsId",
      "sliCount",
      "packetsRepaired",
      "fractionLost",
      "burstPacketsLost",
      "burstLossCount",
      "burstDiscardCount",
      "gapDiscardRate",
      "gapLossRate",
    ],
    deprecated: ["mozRtt""isRemote"],
  },
  "outbound-rtp": {
    expected: [
      "id",
      "mid",
      "timestamp",
      "type",
      "ssrc",
      "mediaType",
      "kind",
      "codecId",
      "packetsSent",
      "bytesSent",
      "remoteId",
      "headerBytesSent",
      "retransmittedPacketsSent",
      "retransmittedBytesSent",
    ],
    optional: ["nackCount""qpSum""rid"],
    localAudioOnly: [],
    localVideoOnly: [
      "framesEncoded",
      "firCount",
      "pliCount",
      "frameWidth",
      "frameHeight",
      "framesPerSecond",
      "framesSent",
      "hugeFramesSent",
      "totalEncodeTime",
      "totalEncodedBytesTarget",
    ],
    unimplemented: ["mediaTrackId""transportId""sliCount""targetBitrate"],
    deprecated: ["isRemote"],
  },
  "remote-inbound-rtp": {
    expected: [
      "id",
      "timestamp",
      "type",
      "ssrc",
      "mediaType",
      "kind",
      "codecId",
      "packetsLost",
      "jitter",
      "localId",
      "totalRoundTripTime",
      "fractionLost",
      "roundTripTimeMeasurements",
    ],
    optional: ["roundTripTime""nackCount""packetsReceived"],
    unimplemented: [
      "mediaTrackId",
      "transportId",
      "packetsDiscarded",
      "associateStatsId",
      "sliCount",
      "packetsRepaired",
      "burstPacketsLost",
      "burstLossCount",
      "burstDiscardCount",
      "gapDiscardRate",
      "gapLossRate",
    ],
    deprecated: ["mozRtt""isRemote"],
  },
  "remote-outbound-rtp": {
    expected: [
      "id",
      "timestamp",
      "type",
      "ssrc",
      "mediaType",
      "kind",
      "codecId",
      "packetsSent",
      "bytesSent",
      "localId",
      "remoteTimestamp",
    ],
    optional: ["nackCount"],
    unimplemented: ["mediaTrackId""transportId""sliCount""targetBitrate"],
    deprecated: ["isRemote"],
  },
  "media-source": {
    expected: ["id""timestamp""type""trackIdentifier""kind"],
    unimplemented: [
      "audioLevel",
      "totalAudioEnergy",
      "totalSamplesDuration",
      "echoReturnLoss",
      "echoReturnLossEnhancement",
      "droppedSamplesDuration",
      "droppedSamplesEvents",
      "totalCaptureDelay",
      "totalSamplesCaptured",
    ],
    localAudioOnly: [],
    localVideoOnly: ["frames""framesPerSecond""width""height"],
    optional: [],
    deprecated: [],
  },
  csrc: { skip: true },
  codec: {
    expected: [
      "timestamp",
      "type",
      "id",
      "payloadType",
      "transportId",
      "mimeType",
      "clockRate",
    ],
    optional: ["codecType""channels""sdpFmtpLine"],
    unimplemented: [],
    deprecated: [],
  },
  "peer-connection": { skip: true },
  "data-channel": { skip: true },
  track: { skip: true },
  transport: { skip: true },
  "candidate-pair": {
    expected: [
      "id",
      "timestamp",
      "type",
      "transportId",
      "localCandidateId",
      "remoteCandidateId",
      "state",
      "priority",
      "nominated",
      "writable",
      "readable",
      "bytesSent",
      "bytesReceived",
      "lastPacketSentTimestamp",
      "lastPacketReceivedTimestamp",
    ],
    optional: ["selected"],
    unimplemented: [
      "totalRoundTripTime",
      "currentRoundTripTime",
      "availableOutgoingBitrate",
      "availableIncomingBitrate",
      "requestsReceived",
      "requestsSent",
      "responsesReceived",
      "responsesSent",
      "retransmissionsReceived",
      "retransmissionsSent",
      "consentRequestsSent",
    ],
    deprecated: [],
  },
  "local-candidate": {
    expected: [
      "id",
      "timestamp",
      "type",
      "address",
      "protocol",
      "port",
      "candidateType",
      "priority",
    ],
    optional: ["relayProtocol""proxied"],
    unimplemented: ["networkType""url""transportId"],
    deprecated: [
      "candidateId",
      "portNumber",
      "ipAddress",
      "componentId",
      "mozLocalTransport",
      "transport",
    ],
  },
  "remote-candidate": {
    expected: [
      "id",
      "timestamp",
      "type",
      "address",
      "protocol",
      "port",
      "candidateType",
      "priority",
    ],
    optional: ["relayProtocol""proxied"],
    unimplemented: ["networkType""url""transportId"],
    deprecated: [
      "candidateId",
      "portNumber",
      "ipAddress",
      "componentId",
      "mozLocalTransport",
      "transport",
    ],
  },
  certificate: { skip: true },
};

["inbound-rtp""outbound-rtp""media-source"].forEach(type => {
  let s = statsExpectedByType[type];
  s.optional = [...s.optional, ...s.localVideoOnly, ...s.localAudioOnly];
});

//
//  Checks that the fields in a report conform to the expectations in
// statExpectedByType
//
function checkExpectedFields(report) {
  report.forEach(stat => {
    let expectations = statsExpectedByType[stat.type];
    ok(expectations, "Stats type " + stat.type + " was expected");
    // If the type is not expected or if it is flagged for skipping continue to
    // the next
    if (!expectations || expectations.skip) {
      return;
    }
    // Check that all required fields exist
    expectations.expected.forEach(field => {
      ok(
        field in stat,
        "Expected stat field " + stat.type + "." + field + " exists"
      );
    });
    // Check that each field is either expected or optional
    let allowed = [...expectations.expected, ...expectations.optional];
    Object.keys(stat).forEach(field => {
      ok(
        allowed.includes(field),
        "Stat field " +
          stat.type +
          "." +
          field +
          ` is allowed. ${JSON.stringify(stat)}`
      );
    });

    //
    // Ensure that unimplemented fields are not implemented
    //   note: if a field is implemented it should be moved to expected or
    //   optional.
    //
    expectations.unimplemented.forEach(field => {
      ok(
        !Object.keys(stat).includes(field),
        "Unimplemented field " + stat.type + "." + field + " does not exist."
      );
    });

    //
    // Ensure that all deprecated fields are not present
    //
    expectations.deprecated.forEach(field => {
      ok(
        !Object.keys(stat).includes(field),
        "Deprecated field " + stat.type + "." + field + " does not exist."
      );
    });
  });
}

function pedanticChecks(report) {
  // Check that report is only-maplike
  [...report.keys()].forEach(key =>
    is(
      report[key],
      undefined,
      `Report is not dictionary like, it lacks a property for key ${key}`
    )
  );
  report.forEach((statObj, mapKey) => {
    info(`"${mapKey} = ${JSON.stringify(statObj, null, 2)}`);
  });
  // eslint-disable-next-line complexity
  report.forEach((statObj, mapKey) => {
    let tested = {};
    // Record what fields get tested.
    // To access a field foo without marking it as tested use stat.inner.foo
    let stat = new Proxy(statObj, {
      get(stat, key) {
        if (key == "inner") {
          return stat;
        }
        tested[key] = true;
        return stat[key];
      },
    });

    let expectations = statsExpectedByType[stat.type];

    if (expectations.skip) {
      return;
    }

    // All stats share the following attributes inherited from RTCStats
    is(stat.id, mapKey, stat.type + ".id is the same as the report key.");

    // timestamp
    ok(stat.timestamp >= 0, stat.type + ".timestamp is not less than 0");
    // If the timebase for the timestamp is not properly set the timestamp
    // will appear relative to the year 1970; Bug 1495446
    const date = new Date(stat.timestamp);
    ok(
      date.getFullYear() > 1970,
      `${stat.type}.timestamp is relative to current time, date=${date}`
    );
    //
    // RTCStreamStats attributes with common behavior
    //
    // inbound-rtp, outbound-rtp, remote-inbound-rtp, remote-outbound-rtp
    // inherit from RTCStreamStats
    if (
      [
        "inbound-rtp",
        "outbound-rtp",
        "remote-inbound-rtp",
        "remote-outbound-rtp",
      ].includes(stat.type)
    ) {
      const isRemote = stat.type.startsWith("remote-");
      //
      // Common RTCStreamStats fields
      //

      // SSRC
      ok(stat.ssrc, stat.type + ".ssrc has a value");

      // kind
      ok(
        ["audio""video"].includes(stat.kind),
        stat.type + ".kind is 'audio' or 'video'"
      );

      // mediaType, renamed to kind but remains for backward compability.
      ok(
        ["audio""video"].includes(stat.mediaType),
        stat.type + ".mediaType is 'audio' or 'video'"
      );

      ok(stat.kind == stat.mediaType, "kind equals legacy mediaType");

      // codecId
      ok(stat.codecId, `${stat.type}.codecId has a value`);
      ok(report.has(stat.codecId), `codecId ${stat.codecId} exists in report`);
      is(
        report.get(stat.codecId).type,
        "codec",
        `codecId ${stat.codecId} in report is codec type`
      );
      is(
        report.get(stat.codecId).mimeType.slice(0, 5),
        stat.kind,
        `codecId ${stat.codecId} in report is for a mimeType of the same ` +
          `media type as the referencing rtp stream stat`
      );

      if (isRemote) {
        // local id
        if (stat.localId) {
          ok(
            report.has(stat.localId),
            `localId ${stat.localId} exists in report.`
          );
          is(
            report.get(stat.localId).ssrc,
            stat.ssrc,
            "remote ssrc and local ssrc match."
          );
          is(
            report.get(stat.localId).remoteId,
            stat.id,
            "local object has remote object as it's own remote object."
          );
        }
      } else {
        // remote id
        if (stat.remoteId) {
          ok(
            report.has(stat.remoteId),
            `remoteId ${stat.remoteId} exists in report.`
          );
          is(
            report.get(stat.remoteId).ssrc,
            stat.ssrc,
            "remote ssrc and local ssrc match."
          );
          is(
            report.get(stat.remoteId).localId,
            stat.id,
            "remote object has local object as it's own local object."
          );
        }
      }

      // nackCount
      if (stat.nackCount) {
        ok(
          stat.nackCount >= 0,
          `${stat.type}.nackCount is sane (${stat.kind}).`
        );
      }

      if (!isRemote && stat.inner.kind == "video") {
        // firCount
        ok(
          stat.firCount >= 0 && stat.firCount < 100,
          `${stat.type}.firCount is a sane number for a short ` +
            `${stat.kind} test. value=${stat.firCount}`
        );

        // pliCount
        ok(
          stat.pliCount >= 0 && stat.pliCount < 200,
          `${stat.type}.pliCount is a sane number for a short ` +
            `${stat.kind} test. value=${stat.pliCount}`
        );

        // qpSum
        if (stat.qpSum !== undefined) {
          ok(
            stat.qpSum >= 0,
            `${stat.type}.qpSum is at least 0 ` +
              `${stat.kind} test. value=${stat.qpSum}`
          );
        }
      } else {
        is(
          stat.qpSum,
          undefined,
          `${stat.type}.qpSum does not exist when stat.kind != video`
        );
      }
    }

    if (stat.type == "inbound-rtp") {
      //
      // Required fields
      //

      // trackIdentifier
      is(typeof stat.trackIdentifier, "string");
      isnot(stat.trackIdentifier, "");

      // mid
      ok(
        parseInt(stat.mid) >= 0,
        `${stat.type}.mid is a positive integer. value=${stat.mid}`
      );
      let inboundRtpMids = [];
      report.forEach(r => {
        if (r.type == "inbound-rtp") {
          inboundRtpMids.push(r.mid);
        }
      });
      is(
        inboundRtpMids.filter(mid => mid == stat.mid).length,
        1,
        `${stat.type}.mid is distinct. value=${
          stat.mid
        }, others=${JSON.stringify(inboundRtpMids)}`
      );

      // packetsReceived
      ok(
        stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5,
        `${stat.type}.packetsReceived is a sane number for a short ` +
          `${stat.kind} test. value=${stat.packetsReceived}`
      );

      // packetsDiscarded
      ok(
        stat.packetsDiscarded >= 0 && stat.packetsDiscarded < 100,
        `${stat.type}.packetsDiscarded is sane number for a short test. ` +
          `value=${stat.packetsDiscarded}`
      );
      // bytesReceived
      ok(
        stat.bytesReceived >= 0 && stat.bytesReceived < 10 ** 9, // Not a magic number, just a guess
        `${stat.type}.bytesReceived is a sane number for a short ` +
          `${stat.kind} test. value=${stat.bytesReceived}`
      );

      // packetsLost
      ok(
        stat.packetsLost < 100,
        `${stat.type}.packetsLost is a sane number for a short ` +
          `${stat.kind} test. value=${stat.packetsLost}`
      );

      // This should be much lower for audio, TODO: Bug 1330575
      let expectedJitter = stat.kind == "video" ? 0.5 : 1;
      // jitter
      ok(
        stat.jitter < expectedJitter,
        `${stat.type}.jitter is sane number for a ${stat.kind} ` +
          `local only test. value=${stat.jitter}`
      );

      // lastPacketReceivedTimestamp
      ok(
        stat.lastPacketReceivedTimestamp !== undefined,
        `${stat.type}.lastPacketReceivedTimestamp has a value`
      );

      // headerBytesReceived
      ok(
        stat.headerBytesReceived >= 0 && stat.headerBytesReceived < 500000,
        `${stat.type}.headerBytesReceived is sane for a short test. ` +
          `value=${stat.headerBytesReceived}`
      );

      // Always missing from libwebrtc stats
      // estimatedPlayoutTimestamp
      // ok(
      //   stat.estimatedPlayoutTimestamp !== undefined,
      //   `${stat.type}.estimatedPlayoutTimestamp has a value`
      // );

      // jitterBufferEmittedCount
      ok(
        stat.jitterBufferEmittedCount > 0,
        `${stat.type}.jitterBufferEmittedCount is a sane number for a short ` +
          `${stat.kind} test. value=${stat.jitterBufferEmittedCount}`
      );

      // jitterBufferDelay
      let avgJitterBufferDelay =
        stat.jitterBufferDelay / stat.jitterBufferEmittedCount;
      ok(
        avgJitterBufferDelay > 0 && avgJitterBufferDelay < 10,
        `${stat.type}.jitterBufferDelay is a sane number for a short ` +
          `${stat.kind} test. value=${stat.jitterBufferDelay}/${stat.jitterBufferEmittedCount}=${avgJitterBufferDelay}`
      );

      //
      // Optional fields
      //

      //
      // Local audio only stats
      //
      if (stat.inner.kind != "audio") {
        expectations.localAudioOnly.forEach(field => {
          ok(
            stat[field] === undefined,
            `${stat.type} does not have field ${field}` +
              ` when kind is not 'audio'`
          );
        });
      } else {
        expectations.localAudioOnly.forEach(field => {
          ok(
            stat.inner[field] !== undefined,
            stat.type + " has field " + field + " when kind is video"
          );
        });
        // totalSamplesReceived
        ok(
          stat.totalSamplesReceived > 1000,
          `${stat.type}.totalSamplesReceived is a sane number for a short ` +
            `${stat.kind} test. value=${stat.totalSamplesReceived}`
        );

        // fecPacketsReceived
        ok(
          stat.fecPacketsReceived >= 0 && stat.fecPacketsReceived < 10 ** 5,
          `${stat.type}.fecPacketsReceived is a sane number for a short ` +
            `${stat.kind} test. value=${stat.fecPacketsReceived}`
        );

        // fecPacketsDiscarded
        ok(
          stat.fecPacketsDiscarded >= 0 && stat.fecPacketsDiscarded < 100,
          `${stat.type}.fecPacketsDiscarded is sane number for a short test. ` +
            `value=${stat.fecPacketsDiscarded}`
        );
        // concealedSamples
        ok(
          stat.concealedSamples >= 0 &&
            stat.concealedSamples <= stat.totalSamplesReceived,
          `${stat.type}.concealedSamples is a sane number for a short ` +
            `${stat.kind} test. value=${stat.concealedSamples}`
        );

        // silentConcealedSamples
        ok(
          stat.silentConcealedSamples >= 0 &&
            stat.silentConcealedSamples <= stat.concealedSamples,
          `${stat.type}.silentConcealedSamples is a sane number for a short ` +
            `${stat.kind} test. value=${stat.silentConcealedSamples}`
        );

        // concealmentEvents
        ok(
          stat.concealmentEvents >= 0 &&
            stat.concealmentEvents <= stat.packetsReceived,
          `${stat.type}.concealmentEvents is a sane number for a short ` +
            `${stat.kind} test. value=${stat.concealmentEvents}`
        );

        // insertedSamplesForDeceleration
        ok(
          stat.insertedSamplesForDeceleration >= 0 &&
            stat.insertedSamplesForDeceleration <= stat.totalSamplesReceived,
          `${stat.type}.insertedSamplesForDeceleration is a sane number for a short ` +
            `${stat.kind} test. value=${stat.insertedSamplesForDeceleration}`
        );

        // removedSamplesForAcceleration
        ok(
          stat.removedSamplesForAcceleration >= 0 &&
            stat.removedSamplesForAcceleration <= stat.totalSamplesReceived,
          `${stat.type}.removedSamplesForAcceleration is a sane number for a short ` +
            `${stat.kind} test. value=${stat.removedSamplesForAcceleration}`
        );

        // audioLevel
        ok(
          stat.audioLevel >= 0 && stat.audioLevel <= 128,
          `${stat.type}.bytesReceived is a sane number for a short ` +
            `${stat.kind} test. value=${stat.audioLevel}`
        );

        // totalAudioEnergy
        ok(
          stat.totalAudioEnergy >= 0 && stat.totalAudioEnergy <= 128,
          `${stat.type}.totalAudioEnergy is a sane number for a short ` +
            `${stat.kind} test. value=${stat.totalAudioEnergy}`
        );

        // totalSamplesDuration
        ok(
          stat.totalSamplesDuration >= 0 && stat.totalSamplesDuration <= 300,
          `${stat.type}.totalSamplesDuration is a sane number for a short ` +
            `${stat.kind} test. value=${stat.totalSamplesDuration}`
        );
      }

      //
      // Local video only stats
      //
      if (stat.inner.kind != "video") {
        expectations.localVideoOnly.forEach(field => {
          ok(
            stat[field] === undefined,
            `${stat.type} does not have field ${field}` +
              ` when kind is not 'video'`
          );
        });
      } else {
        expectations.localVideoOnly.forEach(field => {
          ok(
            stat.inner[field] !== undefined,
            stat.type + " has field " + field + " when kind is video"
          );
        });
        // discardedPackets
        ok(
          stat.discardedPackets < 100,
          `${stat.type}.discardedPackets is a sane number for a short test. ` +
            `value=${stat.discardedPackets}`
        );
        // framesPerSecond
        ok(
          stat.framesPerSecond > 0 && stat.framesPerSecond < 70,
          `${stat.type}.framesPerSecond is a sane number for a short ` +
            `${stat.kind} test. value=${stat.framesPerSecond}`
        );

        // framesDecoded
        ok(
          stat.framesDecoded > 0 && stat.framesDecoded < 1000000,
          `${stat.type}.framesDecoded is a sane number for a short ` +
            `${stat.kind} test. value=${stat.framesDecoded}`
        );

        // framesDropped
        ok(
          stat.framesDropped >= 0 && stat.framesDropped < 100,
          `${stat.type}.framesDropped is a sane number for a short ` +
            `${stat.kind} test. value=${stat.framesDropped}`
        );

        // frameWidth
        ok(
          stat.frameWidth > 0 && stat.frameWidth < 100000,
          `${stat.type}.frameWidth is a sane number for a short ` +
            `${stat.kind} test. value=${stat.frameWidth}`
        );

        // frameHeight
        ok(
          stat.frameHeight > 0 && stat.frameHeight < 100000,
          `${stat.type}.frameHeight is a sane number for a short ` +
            `${stat.kind} test. value=${stat.frameHeight}`
        );

        // totalDecodeTime
        ok(
          stat.totalDecodeTime >= 0 && stat.totalDecodeTime < 300,
          `${stat.type}.totalDecodeTime is sane for a short test. ` +
            `value=${stat.totalDecodeTime}`
        );

        // totalProcessingDelay
        ok(
          stat.totalProcessingDelay < 1000,
          `${stat.type}.totalProcessingDelay is sane number for a short test ` +
            `local only test. value=${stat.totalProcessingDelay}`
        );

        // totalInterFrameDelay
        ok(
          stat.totalInterFrameDelay >= 0 && stat.totalInterFrameDelay < 1000,
          `${stat.type}.totalInterFrameDelay is sane for a short test. ` +
            `value=${stat.totalInterFrameDelay}`
        );

        // totalSquaredInterFrameDelay
        ok(
          stat.totalSquaredInterFrameDelay >= 0 &&
            stat.totalSquaredInterFrameDelay < 10000,
          `${stat.type}.totalSquaredInterFrameDelay is sane for a short test. ` +
            `value=${stat.totalSquaredInterFrameDelay}`
        );

        // framesReceived
        ok(
          stat.framesReceived >= 0 && stat.framesReceived < 100000,
          `${stat.type}.framesReceived is a sane number for a short ` +
            `${stat.kind} test. value=${stat.framesReceived}`
        );
      }
    } else if (stat.type == "remote-inbound-rtp") {
      // roundTripTime
      ok(
        stat.roundTripTime >= 0,
        `${stat.type}.roundTripTime is sane with` +
          `value of: ${stat.roundTripTime} (${stat.kind})`
      );
      //
      // Required fields
      //

      // packetsLost
      ok(
        stat.packetsLost < 100,
        `${stat.type}.packetsLost is a sane number for a short ` +
          `${stat.kind} test. value=${stat.packetsLost}`
      );

      // jitter
      ok(
        stat.jitter >= 0,
        `${stat.type}.jitter is sane number (${stat.kind}). ` +
          `value=${stat.jitter}`
      );

      //
      // Optional fields
      //

      // packetsReceived
      if (stat.packetsReceived) {
        ok(
          stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5,
          `${stat.type}.packetsReceived is a sane number for a short ` +
            `${stat.kind} test. value=${stat.packetsReceived}`
        );
      }

      // totalRoundTripTime
      ok(
        stat.totalRoundTripTime < 50000,
        `${stat.type}.totalRoundTripTime is a sane number for a short ` +
          `${stat.kind} test. value=${stat.totalRoundTripTime}`
      );

      // fractionLost
      ok(
        stat.fractionLost < 0.2,
        `${stat.type}.fractionLost is a sane number for a short ` +
          `${stat.kind} test. value=${stat.fractionLost}`
      );

      // roundTripTimeMeasurements
      ok(
        stat.roundTripTimeMeasurements >= 1 &&
          stat.roundTripTimeMeasurements < 500,
        `${stat.type}.roundTripTimeMeasurements is a sane number for a short ` +
          `${stat.kind} test. value=${stat.roundTripTimeMeasurements}`
      );
    } else if (stat.type == "outbound-rtp") {
      //
      // Required fields
      //

      // mid
      ok(
        parseInt(stat.mid) >= 0,
        `${stat.type}.mid a positive integer. value=${stat.mid}`
      );

      // packetsSent
      ok(
        stat.packetsSent > 0 && stat.packetsSent < 10000,
        `${stat.type}.packetsSent is a sane number for a short ` +
          `${stat.kind} test. value=${stat.packetsSent}`
      );

      // bytesSent
      const audio1Min = 16000 * 60; // 128kbps
      const video1Min = 250000 * 60; // 2Mbps
      ok(
        stat.bytesSent > 0 &&
          stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min),
        `${stat.type}.bytesSent is a sane number for a short ` +
          `${stat.kind} test. value=${stat.bytesSent}`
      );

      // headerBytesSent
      ok(
        stat.headerBytesSent > 0 &&
          stat.headerBytesSent < (stat.kind == "video" ? video1Min : audio1Min),
        `${stat.type}.headerBytesSent is a sane number for a short ` +
          `${stat.kind} test. value=${stat.headerBytesSent}`
      );

      // retransmittedPacketsSent
      ok(
        stat.retransmittedPacketsSent >= 0 &&
          stat.retransmittedPacketsSent <
            (stat.kind == "video" ? video1Min : audio1Min),
        `${stat.type}.retransmittedPacketsSent is a sane number for a short ` +
          `${stat.kind} test. value=${stat.retransmittedPacketsSent}`
      );

      // retransmittedBytesSent
      ok(
        stat.retransmittedBytesSent >= 0 &&
          stat.retransmittedBytesSent <
            (stat.kind == "video" ? video1Min : audio1Min),
        `${stat.type}.retransmittedBytesSent is a sane number for a short ` +
          `${stat.kind} test. value=${stat.retransmittedBytesSent}`
      );

      //
      // Optional fields
      //

      // rid
      if (stat.kind == "audio") {
        ok(
          stat.rid === undefined,
          `${stat.type}.rid" MUST NOT exist for audio. value=${stat.rid}`
        );
      } else {
        let numSendVideoStreamsForMid = 0;
        report.forEach(r => {
          if (
            r.type == "outbound-rtp" &&
            r.kind == "video" &&
            r.mid == stat.mid
          ) {
            numSendVideoStreamsForMid += 1;
          }
        });
        if (numSendVideoStreamsForMid == 1) {
          is(
            stat.rid,
            undefined,
            `${stat.type}.rid" does not exist for singlecast video. value=${stat.rid}`
          );
        } else {
          isnot(
            stat.rid,
            undefined,
            `${stat.type}.rid" does exist for simulcast video. value=${stat.rid}`
          );
        }
      }

      // qpSum
      // This is supported for all of our vpx codecs and AV1 (on the encode
      // side, see bug 1519590)
      const mimeType = report.get(stat.codecId).mimeType;
      if (mimeType.includes("VP") || mimeType.includes("AV1")) {
        ok(
          stat.qpSum >= 0,
          `${stat.type}.qpSum is a sane number (${stat.kind}) ` +
            `for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}`
        );
      } else if (mimeType.includes("H264")) {
        // OpenH264 encoder records QP so we check for either condition.
        if (!stat.qpSum && !("qpSum" in stat)) {
          ok(
            !stat.qpSum && !("qpSum" in stat),
            `${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}`
          );
        } else {
          ok(
            stat.qpSum >= 0,
            `${stat.type}.qpSum is a sane number (${stat.kind}) ` +
              `for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}`
          );
        }
      } else {
        ok(
          !stat.qpSum && !("qpSum" in stat),
          `${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}`
        );
      }

      //
      // Local video only stats
      //
      if (stat.inner.kind != "video") {
        expectations.localVideoOnly.forEach(field => {
          ok(
            stat[field] === undefined,
            `${stat.type} does not have field ` +
              `${field} when kind is not 'video'`
          );
        });
      } else {
        expectations.localVideoOnly.forEach(field => {
          ok(
            stat.inner[field] !== undefined,
            `${stat.type} has field ` +
              `${field} when kind is video and isRemote is false`
          );
        });

        // framesEncoded
        ok(
          stat.framesEncoded >= 0 && stat.framesEncoded < 100000,
          `${stat.type}.framesEncoded is a sane number for a short ` +
            `${stat.kind} test. value=${stat.framesEncoded}`
        );

        // frameWidth
        ok(
          stat.frameWidth >= 0 && stat.frameWidth < 100000,
          `${stat.type}.frameWidth is a sane number for a short ` +
            `${stat.kind} test. value=${stat.frameWidth}`
        );

        // frameHeight
        ok(
          stat.frameHeight >= 0 && stat.frameHeight < 100000,
          `${stat.type}.frameHeight is a sane number for a short ` +
            `${stat.kind} test. value=${stat.frameHeight}`
        );

        // framesPerSecond
        ok(
          stat.framesPerSecond >= 0 && stat.framesPerSecond < 60,
          `${stat.type}.framesPerSecond is a sane number for a short ` +
            `${stat.kind} test. value=${stat.framesPerSecond}`
        );

        // framesSent
        ok(
          stat.framesSent >= 0 && stat.framesSent < 100000,
          `${stat.type}.framesSent is a sane number for a short ` +
            `${stat.kind} test. value=${stat.framesSent}`
        );

        // hugeFramesSent
        ok(
          stat.hugeFramesSent >= 0 && stat.hugeFramesSent < 100000,
          `${stat.type}.hugeFramesSent is a sane number for a short ` +
            `${stat.kind} test. value=${stat.hugeFramesSent}`
        );

        // totalEncodeTime
        ok(
          stat.totalEncodeTime >= 0,
          `${stat.type}.totalEncodeTime is a sane number for a short ` +
            `${stat.kind} test. value=${stat.totalEncodeTime}`
        );

        // totalEncodedBytesTarget
        ok(
          stat.totalEncodedBytesTarget > 1000,
          `${stat.type}.totalEncodedBytesTarget is a sane number for a short ` +
            `${stat.kind} test. value=${stat.totalEncodedBytesTarget}`
        );
      }
    } else if (stat.type == "remote-outbound-rtp") {
      //
      // Required fields
      //

      // packetsSent
      ok(
        stat.packetsSent > 0 && stat.packetsSent < 10000,
        `${stat.type}.packetsSent is a sane number for a short ` +
          `${stat.kind} test. value=${stat.packetsSent}`
      );

      // bytesSent
      const audio1Min = 16000 * 60; // 128kbps
      const video1Min = 250000 * 60; // 2Mbps
      ok(
        stat.bytesSent > 0 &&
          stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min),
        `${stat.type}.bytesSent is a sane number for a short ` +
          `${stat.kind} test. value=${stat.bytesSent}`
      );

      ok(
        stat.remoteTimestamp !== undefined,
        `${stat.type}.remoteTimestamp ` + `is not undefined (${stat.kind})`
      );
      const ageSeconds = (stat.timestamp - stat.remoteTimestamp) / 1000;
      // remoteTimestamp is exact (so it can be mapped to a packet), whereas
      // timestamp has reduced precision. It is possible that
      // remoteTimestamp occurs a millisecond into the future from
      // timestamp. We also subtract half a millisecond when reducing
      // precision on libwebrtc timestamps, to counteract the potential
      // rounding up that libwebrtc may do since it tends to round its
      // internal timestamps to whole milliseconds. In the worst case
      // remoteTimestamp may therefore occur 2 milliseconds ahead of
      // timestamp.
      ok(
        ageSeconds >= -0.002 && ageSeconds < 30,
        `${stat.type}.remoteTimestamp is on the same timeline as ` +
          `${stat.type}.timestamp, and no older than 30 seconds. ` +
          `difference=${ageSeconds}s`
      );
    } else if (stat.type == "media-source") {
      // trackIdentifier
      is(typeof stat.trackIdentifier, "string");
      isnot(stat.trackIdentifier, "");

      // kind
      is(typeof stat.kind, "string");
      ok(stat.kind == "audio" || stat.kind == "video");
      if (stat.inner.kind == "video") {
        expectations.localVideoOnly.forEach(field => {
          ok(
            stat.inner[field] !== undefined,
            `${stat.type} has field ` +
              `${field} when kind is video and isRemote is false`
          );
        });

        // frames
        ok(
          stat.frames >= 0 && stat.frames < 100000,
          `${stat.type}.frames is a sane number for a short ` +
            `${stat.kind} test. value=${stat.frames}`
        );

        // framesPerSecond
        ok(
          stat.framesPerSecond >= 0 && stat.framesPerSecond < 100,
          `${stat.type}.framesPerSecond is a sane number for a short ` +
            `${stat.kind} test. value=${stat.framesPerSecond}`
        );

        // width
        ok(
          stat.width >= 0 && stat.width < 1000000,
          `${stat.type}.width is a sane number for a ` +
            `${stat.kind} test. value=${stat.width}`
        );

        // height
        ok(
          stat.height >= 0 && stat.height < 1000000,
          `${stat.type}.height is a sane number for a ` +
            `${stat.kind} test. value=${stat.height}`
        );
      } else {
        expectations.localVideoOnly.forEach(field => {
          ok(
            stat[field] === undefined,
            `${stat.type} does not have field ` +
              `${field} when kind is not 'video'`
          );
        });
      }
    } else if (stat.type == "codec") {
      //
      // Required fields
      //

      // mimeType & payloadType
      switch (stat.mimeType) {
        case "audio/opus":
          is(stat.payloadType, 109, "codec.payloadType for opus");
          break;
        case "video/VP8":
          is(stat.payloadType, 120, "codec.payloadType for VP8");
          break;
        case "video/VP9":
          is(stat.payloadType, 121, "codec.payloadType for VP9");
          break;
        case "video/H264":
          ok(
            stat.payloadType == 97 ||
              stat.payloadType == 126 ||
              stat.payloadType == 103 ||
              stat.payloadType == 105,
            `codec.payloadType for H264 was ${stat.payloadType}, exp. 97, 126, 103, or 105`
          );
          break;
        case "video/AV1":
          is(stat.payloadType, 99, "codec.payloadType for AV1");
          break;
        default:
          ok(
            false,
            `Unexpected codec.mimeType ${stat.mimeType} for payloadType ` +
              `${stat.payloadType}`
          );
          break;
      }

      // transportId
      // (no transport stats yet)
      ok(stat.transportId, "codec.transportId is set");

      // clockRate
      if (stat.mimeType.startsWith("audio")) {
        is(stat.clockRate, 48000, "codec.clockRate for audio/opus");
      } else if (stat.mimeType.startsWith("video")) {
        is(stat.clockRate, 90000, "codec.clockRate for video");
      }

      // sdpFmtpLine
      // (not technically mandated by spec, but expected here)
      // AV1 has no required parameters, so don't require sdpFmtpLine for it.
      if (stat.mimeType != "video/AV1") {
        ok(stat.sdpFmtpLine, "codec.sdp FmtpLine is set");
      }
      const opusParams = [
        "maxplaybackrate",
        "maxaveragebitrate",
        "usedtx",
        "stereo",
        "useinbandfec",
        "cbr",
        "ptime",
        "minptime",
        "maxptime",
      ];
      const vpxParams = ["max-fs""max-fr"];
      const h264Params = [
        "packetization-mode",
        "level-asymmetry-allowed",
        "profile-level-id",
        "max-fs",
        "max-cpb",
        "max-dpb",
        "max-br",
        "max-mbps",
      ];
      // AV1 parameters:
      //  https://aomediacodec.github.io/av1-rtp-spec/#721-mapping-of-media-subtype-parameters-to-sdp
      const av1Params = ["profile""level-idx""tier"];
      // Check that the parameters are as expected. AV1 may have no parameters.
      for (const param of (stat.sdpFmtpLine || "").split(";")) {
        const [key, value] = param.split("=");
        if (stat.payloadType == 99) {
          // AV1 might not have any parameters, if it does make sure they are as expected.
          if (key) {
            ok(
              av1Params.includes(key),
              `codec.sdpFmtpLine param ${key}=${value} for AV1`
            );
          }
        } else if (stat.payloadType == 109) {
          ok(
            opusParams.includes(key),
            `codec.sdpFmtpLine param ${key}=${value} for opus`
          );
        } else if (stat.payloadType == 120 || stat.payloadType == 121) {
          ok(
            vpxParams.includes(key),
            `codec.sdpFmtpLine param ${key}=${value} for VPx`
          );
        } else if (stat.payloadType == 97 || stat.payloadType == 126) {
          ok(
            h264Params.includes(key),
            `codec.sdpFmtpLine param ${key}=${value} for H264`
          );
          if (key == "packetization-mode") {
            if (stat.payloadType == 97) {
              is(value, "0""codec.sdpFmtpLine: H264 (97) packetization-mode");
            } else if (stat.payloadType == 126) {
              is(
                value,
                "1",
                "codec.sdpFmtpLine: H264 (126) packetization-mode"
              );
            }
          }
          if (key == "profile-level-id") {
            is(value, "42e01f""codec.sdpFmtpLine: H264 profile-level-id");
          }
        }
      }

      //
      // Optional fields
      //

      // codecType
      ok(
        !Object.keys(stat).includes("codecType") ||
          stat.codecType == "encode" ||
          stat.codecType == "decode",
        "codec.codecType (${codec.codecType}) is an expected value or absent"
      );
      let numRecvStreams = 0;
      let numSendStreams = 0;
      const counts = {
        "inbound-rtp": 0,
        "outbound-rtp": 0,
        "remote-inbound-rtp": 0,
        "remote-outbound-rtp": 0,
      };
      const [kind] = stat.mimeType.split("/");
      report.forEach(other => {
        if (other.type == "inbound-rtp" && other.kind == kind) {
          numRecvStreams += 1;
        } else if (other.type == "outbound-rtp" && other.kind == kind) {
          numSendStreams += 1;
        }
        if (other.codecId == stat.id) {
          counts[other.type] += 1;
        }
      });
      const expectedCounts = {
        encode: {
          "inbound-rtp": 0,
          "outbound-rtp": numSendStreams,
          "remote-inbound-rtp": numSendStreams,
          "remote-outbound-rtp": 0,
        },
        decode: {
          "inbound-rtp": numRecvStreams,
          "outbound-rtp": 0,
          "remote-inbound-rtp": 0,
          "remote-outbound-rtp": numRecvStreams,
        },
        absent: {
          "inbound-rtp": numRecvStreams,
          "outbound-rtp": numSendStreams,
          "remote-inbound-rtp": numSendStreams,
          "remote-outbound-rtp": numRecvStreams,
        },
      };
      // Note that the logic above assumes at most one sender and at most one
      // receiver was used to generate this stats report. If more senders or
      // receivers are present, they'd be referring to not only this codec stat,
      // skewing `numSendStreams` and `numRecvStreams` above.
      // This could be fixed when we support `senderId` and `receiverId` in
      // RTCOutboundRtpStreamStats and RTCInboundRtpStreamStats respectively.
      for (const [key, value] of Object.entries(counts)) {
        is(
          value,
          expectedCounts[stat.codecType || "absent"][key],
          `codec.codecType ${stat.codecType || "absent"} ref from ${key} stat`
        );
      }

      // channels
      if (stat.mimeType.startsWith("audio")) {
        ok(stat.channels, "codec.channels should exist for audio");
        if (stat.channels) {
          if (stat.sdpFmtpLine.includes("stereo=1")) {
            is(stat.channels, 2, "codec.channels for stereo audio");
          } else {
            is(stat.channels, 1, "codec.channels for mono audio");
          }
        }
      } else {
        ok(!stat.channels, "codec.channels should not exist for video");
      }
    } else if (stat.type == "candidate-pair") {
      info("candidate-pair is: " + JSON.stringify(stat));
      //
      // Required fields
      //

      // transportId
      ok(
        stat.transportId,
        `${stat.type}.transportId has a value. value=` +
          `${stat.transportId} (${stat.kind})`
      );

      // localCandidateId
      ok(
        stat.localCandidateId,
        `${stat.type}.localCandidateId has a value. value=` +
          `${stat.localCandidateId} (${stat.kind})`
      );

      // remoteCandidateId
      ok(
        stat.remoteCandidateId,
        `${stat.type}.remoteCandidateId has a value. value=` +
          `${stat.remoteCandidateId} (${stat.kind})`
      );

      // priority
      ok(
        stat.priority,
        `${stat.type}.priority has a value. value=` +
          `${stat.priority} (${stat.kind})`
      );

      // readable
      ok(
        stat.readable,
        `${stat.type}.readable is true. value=${stat.readable} ` +
          `(${stat.kind})`
      );

      // writable
      ok(
        stat.writable,
        `${stat.type}.writable is true. value=${stat.writable} ` +
          `(${stat.kind})`
      );

      // state
      if (
        stat.state == "succeeded" &&
        stat.selected !== undefined &&
        stat.selected
      ) {
        info("candidate-pair state is succeeded and selected is true");
        // nominated
        ok(
          stat.nominated,
          `${stat.type}.nominated is true. value=${stat.nominated} ` +
            `(${stat.kind})`
        );

        // bytesSent
        ok(
          stat.bytesSent > 100,
          `${stat.type}.bytesSent is a sane number (>100) if media is flowing. ` +
            `value=${stat.bytesSent}`
        );

        // bytesReceived
        ok(
          stat.bytesReceived > 100,
          `${stat.type}.bytesReceived is a sane number (>100) if media is flowing. ` +
            `value=${stat.bytesReceived}`
        );

        // lastPacketSentTimestamp
        ok(
          stat.lastPacketSentTimestamp,
          `${stat.type}.lastPacketSentTimestamp has a value. value=` +
            `${stat.lastPacketSentTimestamp} (${stat.kind})`
        );

        // lastPacketReceivedTimestamp
        ok(
          stat.lastPacketReceivedTimestamp,
          `${stat.type}.lastPacketReceivedTimestamp has a value. value=` +
            `${stat.lastPacketReceivedTimestamp} (${stat.kind})`
        );
      } else {
        info("candidate-pair is _not_ both state == succeeded and selected");
        // nominated
        ok(
          stat.nominated !== undefined,
          `${stat.type}.nominated exists. value=${stat.nominated} ` +
            `(${stat.kind})`
        );
        ok(
          stat.bytesSent !== undefined,
          `${stat.type}.bytesSent exists. value=${stat.bytesSent} ` +
            `(${stat.kind})`
        );
        ok(
          stat.bytesReceived !== undefined,
          `${stat.type}.bytesReceived exists. value=${stat.bytesReceived} ` +
            `(${stat.kind})`
        );
        ok(
          stat.lastPacketSentTimestamp !== undefined,
          `${stat.type}.lastPacketSentTimestamp exists. value=` +
            `${stat.lastPacketSentTimestamp} (${stat.kind})`
        );
        ok(
          stat.lastPacketReceivedTimestamp !== undefined,
          `${stat.type}.lastPacketReceivedTimestamp exists. value=` +
            `${stat.lastPacketReceivedTimestamp} (${stat.kind})`
        );
      }

      //
      // Optional fields
      //
      // selected
      ok(
        stat.selected === undefined ||
          (stat.state == "succeeded" && stat.selected) ||
          !stat.selected,
        `${stat.type}.selected is undefined, true when state is succeeded, ` +
          `or false. value=${stat.selected} (${stat.kind})`
      );
    } else if (
      stat.type == "local-candidate" ||
      stat.type == "remote-candidate"
    ) {
      info(`candidate is ${JSON.stringify(stat)}`);

      // address
      ok(
        stat.address,
        `${stat.type} has address. value=${stat.address} ` + `(${stat.kind})`
      );

      // protocol
      ok(
        stat.protocol,
        `${stat.type} has protocol. value=${stat.protocol} ` + `(${stat.kind})`
      );

      // port
      ok(
        stat.port >= 0,
        `${stat.type} has port >= 0. value=${stat.port} ` + `(${stat.kind})`
      );
      ok(
        stat.port <= 65535,
        `${stat.type} has port <= 65535. value=${stat.port} ` + `(${stat.kind})`
      );

      // candidateType
      ok(
        stat.candidateType,
        `${stat.type} has candidateType. value=${stat.candidateType} ` +
          `(${stat.kind})`
      );

      // priority
      ok(
        stat.priority > 0 && stat.priority < 2 ** 32 - 1,
        `${stat.type} has priority between 1 and 2^32 - 1 inc. ` +
          `value=${stat.priority} (${stat.kind})`
      );

      // relayProtocol
      if (stat.type == "local-candidate" && stat.candidateType == "relay") {
        ok(
          stat.relayProtocol,
          `relay ${stat.type} has relayProtocol. value=${stat.relayProtocol} ` +
            `(${stat.kind})`
        );
      } else {
        is(
          stat.relayProtocol,
          undefined,
          `relayProtocol is undefined for candidates that are not relay and ` +
            `local. value=${stat.relayProtocol} (${stat.kind})`
        );
      }

      // proxied
      if (stat.proxied) {
        ok(
          stat.proxied == "proxied" || stat.proxied == "non-proxied",
          `${stat.type} has proxied. value=${stat.proxied} (${stat.kind})`
        );
      }
    }

    //
    // Ensure everything was tested
    //
    [...expectations.expected, ...expectations.optional].forEach(field => {
      ok(
        Object.keys(tested).includes(field),
        `${stat.type}.${field} was tested.`
      );
    });
  });
}

function dumpStats(stats) {
  const dict = {};
  for (const [k, v] of stats.entries()) {
    dict[k] = v;
  }
  info(`Got stats: ${JSON.stringify(dict)}`);
}

async function waitForSyncedRtcp(pc) {
  // Ensures that RTCP is present
  let ensureSyncedRtcp = async () => {
    let report = await pc.getStats();
    for (const v of report.values()) {
      if (v.type.endsWith("bound-rtp") && !(v.remoteId || v.localId)) {
        info(`${v.id} is missing remoteId or localId: ${JSON.stringify(v)}`);
        return null;
      }
      if (v.type == "remote-inbound-rtp" && v.roundTripTime === undefined) {
        info(`${v.id} is missing roundTripTime: ${JSON.stringify(v)}`);
        return null;
      }
    }
    return report;
  };
  // Returns true if there is proof in aStats of rtcp flow for all remote stats
  // objects, compared to baseStats.
  const hasAllRtcpUpdated = (baseStats, stats) => {
    let hasRtcpStats = false;
    for (const v of stats.values()) {
      if (v.type == "remote-outbound-rtp") {
        hasRtcpStats = true;
        if (!v.remoteTimestamp) {
          // `remoteTimestamp` is 0 or not present.
          return false;
        }
        if (v.remoteTimestamp <= baseStats.get(v.id)?.remoteTimestamp) {
          // `remoteTimestamp` has not advanced further than the base stats,
          // i.e., no new sender report has been received.
          return false;
        }
      } else if (v.type == "remote-inbound-rtp") {
        hasRtcpStats = true;
        // The ideal thing here would be to check `reportsReceived`, but it's
        // not yet implemented.
        if (!v.packetsReceived) {
          // `packetsReceived` is 0 or not present.
          return false;
        }
        if (v.packetsReceived <= baseStats.get(v.id)?.packetsReceived) {
          // `packetsReceived` has not advanced further than the base stats,
          // i.e., no new receiver report has been received.
          return false;
        }
      }
    }
    return hasRtcpStats;
  };
  let attempts = 0;
  const baseStats = await pc.getStats();
  // Time-units are MS
  const waitPeriod = 100;
  const maxTime = 20000;
  for (let totalTime = maxTime; totalTime > 0; totalTime -= waitPeriod) {
    try {
      let syncedStats = await ensureSyncedRtcp();
      if (syncedStats && hasAllRtcpUpdated(baseStats, syncedStats)) {
        dumpStats(syncedStats);
        return syncedStats;
      }
    } catch (e) {
      info(e);
      info(e.stack);
      throw e;
    }
    attempts += 1;
    info(`waitForSyncedRtcp: no sync on attempt ${attempts}, retrying.`);
    await wait(waitPeriod);
  }
  throw Error(
    "Waiting for synced RTCP timed out after at least " + maxTime + "ms"
  );
}

function checkSendCodecsMimeType(senderStats, mimeType, sdpFmtpLine = null) {
  const codecReports = senderStats.values().filter(s => s.type == "codec");
  isnot(codecReports.length, 0, "Should have send codecs");
  for (const c of codecReports) {
    is(c.codecType, "encode""Send codec is always encode");
    is(c.mimeType, mimeType, "Mime type as expected");
    if (sdpFmtpLine) {
      is(c.sdpFmtpLine, sdpFmtpLine, "Sdp fmtp line as expected");
    }
  }
}

function checkSenderStats(senderStats, streamCount) {
  const outboundRtpReports = [];
  const remoteInboundRtpReports = [];
  for (const v of senderStats.values()) {
    if (v.type == "outbound-rtp") {
      outboundRtpReports.push(v);
    } else if (v.type == "remote-inbound-rtp") {
      remoteInboundRtpReports.push(v);
    }
  }
  is(
    outboundRtpReports.length,
    streamCount,
    `Sender with ${streamCount} simulcast streams has ${streamCount} outbound-rtp reports`
  );
  is(
    remoteInboundRtpReports.length,
    streamCount,
    `Sender with ${streamCount} simulcast streams has ${streamCount} remote-inbound-rtp reports`
  );
  for (const outboundRtpReport of outboundRtpReports) {
    is(
      outboundRtpReports.filter(r => r.ssrc == outboundRtpReport.ssrc).length,
      1,
      "Simulcast send track SSRCs are distinct"
    );
    is(
      outboundRtpReports.filter(r => r.mid == outboundRtpReport.mid).length,
      streamCount,
      "Simulcast send track MIDs are identical"
    );
    if (outboundRtpReport.kind == "video" && streamCount > 1) {
      is(
        outboundRtpReports.filter(r => r.rid == outboundRtpReport.rid).length,
        1,
        "Simulcast send track RIDs are distinct"
      );
    }
    const remoteReports = remoteInboundRtpReports.filter(
      r => r.id == outboundRtpReport.remoteId
    );
    is(
      remoteReports.length,
      1,
      "Simulcast send tracks have exactly one remote counterpart"
    );
    const remoteInboundRtpReport = remoteReports[0];
    is(
      outboundRtpReport.ssrc,
      remoteInboundRtpReport.ssrc,
      "SSRC matches for outbound-rtp and remote-inbound-rtp"
    );
  }
}

function PC_LOCAL_TEST_LOCAL_STATS(test) {
  return waitForSyncedRtcp(test.pcLocal._pc).then(stats => {
    checkExpectedFields(stats);
    pedanticChecks(stats);
    return Promise.all([
      test.pcLocal._pc.getSenders().map(async s => {
        checkSenderStats(
          await s.getStats(),
          Math.max(1, s.getParameters()?.encodings?.length ?? 0)
        );
      }),
    ]);
  });
}

function PC_REMOTE_TEST_REMOTE_STATS(test) {
  return waitForSyncedRtcp(test.pcRemote._pc).then(stats => {
    checkExpectedFields(stats);
    pedanticChecks(stats);
    return Promise.all([
      test.pcRemote._pc.getSenders().map(async s => {
        checkSenderStats(
          await s.getStats(),
          s.track ? Math.max(1, s.getParameters()?.encodings?.length ?? 0) : 0
        );
      }),
    ]);
  });
}

Messung V0.5
C=94 H=87 G=90

¤ Dauer der Verarbeitung: 0.16 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge