tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

stats.js (57374B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const statsExpectedByType = {
      8  "inbound-rtp": {
      9    expected: [
     10      "trackIdentifier",
     11      "id",
     12      "mid",
     13      "timestamp",
     14      "type",
     15      "ssrc",
     16      "mediaType",
     17      "kind",
     18      "codecId",
     19      "packetsReceived",
     20      "packetsLost",
     21      "packetsDiscarded",
     22      "bytesReceived",
     23      "jitter",
     24      "lastPacketReceivedTimestamp",
     25      "headerBytesReceived",
     26      "jitterBufferDelay",
     27      "jitterBufferTargetDelay",
     28      "jitterBufferMinimumDelay",
     29      "jitterBufferEmittedCount",
     30    ],
     31    optional: ["remoteId", "nackCount", "qpSum", "estimatedPlayoutTimestamp"],
     32    localVideoOnly: [
     33      "firCount",
     34      "pliCount",
     35      "framesDecoded",
     36      "keyFramesDecoded",
     37      "framesDropped",
     38      "discardedPackets",
     39      "framesPerSecond",
     40      "frameWidth",
     41      "frameHeight",
     42      "framesReceived",
     43      "framesAssembledFromMultiplePackets",
     44      "totalDecodeTime",
     45      "totalInterFrameDelay",
     46      "totalProcessingDelay",
     47      "totalSquaredInterFrameDelay",
     48      "pauseCount",
     49      "totalPausesDuration",
     50      "freezeCount",
     51      "totalFreezesDuration",
     52      "totalAssemblyTime",
     53    ],
     54    localAudioOnly: [
     55      "totalSamplesReceived",
     56      // libwebrtc doesn't seem to do FEC for video
     57      "fecPacketsReceived",
     58      "fecPacketsDiscarded",
     59      "concealedSamples",
     60      "silentConcealedSamples",
     61      "concealmentEvents",
     62      "insertedSamplesForDeceleration",
     63      "removedSamplesForAcceleration",
     64      "audioLevel",
     65      "totalAudioEnergy",
     66      "totalSamplesDuration",
     67    ],
     68    unimplemented: [
     69      "mediaTrackId",
     70      "transportId",
     71      "associateStatsId",
     72      "sliCount",
     73      "packetsRepaired",
     74      "fractionLost",
     75      "burstPacketsLost",
     76      "burstLossCount",
     77      "burstDiscardCount",
     78      "gapDiscardRate",
     79      "gapLossRate",
     80    ],
     81    deprecated: ["mozRtt", "isRemote"],
     82  },
     83  "outbound-rtp": {
     84    expected: [
     85      "id",
     86      "mid",
     87      "timestamp",
     88      "type",
     89      "ssrc",
     90      "mediaType",
     91      "kind",
     92      "codecId",
     93      "packetsSent",
     94      "bytesSent",
     95      "remoteId",
     96      "headerBytesSent",
     97      "retransmittedPacketsSent",
     98      "retransmittedBytesSent",
     99    ],
    100    optional: ["nackCount", "qpSum", "rid"],
    101    localAudioOnly: [],
    102    localVideoOnly: [
    103      "framesEncoded",
    104      "firCount",
    105      "pliCount",
    106      "frameWidth",
    107      "frameHeight",
    108      "framesPerSecond",
    109      "framesSent",
    110      "hugeFramesSent",
    111      "totalEncodeTime",
    112      "totalEncodedBytesTarget",
    113    ],
    114    unimplemented: ["mediaTrackId", "transportId", "sliCount", "targetBitrate"],
    115    deprecated: ["isRemote"],
    116  },
    117  "remote-inbound-rtp": {
    118    expected: [
    119      "id",
    120      "timestamp",
    121      "type",
    122      "ssrc",
    123      "mediaType",
    124      "kind",
    125      "codecId",
    126      "packetsLost",
    127      "jitter",
    128      "localId",
    129      "totalRoundTripTime",
    130      "fractionLost",
    131      "roundTripTimeMeasurements",
    132    ],
    133    optional: ["roundTripTime", "nackCount", "packetsReceived"],
    134    unimplemented: [
    135      "mediaTrackId",
    136      "transportId",
    137      "packetsDiscarded",
    138      "associateStatsId",
    139      "sliCount",
    140      "packetsRepaired",
    141      "burstPacketsLost",
    142      "burstLossCount",
    143      "burstDiscardCount",
    144      "gapDiscardRate",
    145      "gapLossRate",
    146    ],
    147    deprecated: ["mozRtt", "isRemote"],
    148  },
    149  "remote-outbound-rtp": {
    150    expected: [
    151      "id",
    152      "timestamp",
    153      "type",
    154      "ssrc",
    155      "mediaType",
    156      "kind",
    157      "codecId",
    158      "packetsSent",
    159      "bytesSent",
    160      "localId",
    161      "remoteTimestamp",
    162    ],
    163    optional: ["nackCount"],
    164    unimplemented: ["mediaTrackId", "transportId", "sliCount", "targetBitrate"],
    165    deprecated: ["isRemote"],
    166  },
    167  "media-source": {
    168    expected: ["id", "timestamp", "type", "trackIdentifier", "kind"],
    169    unimplemented: [
    170      "audioLevel",
    171      "totalAudioEnergy",
    172      "totalSamplesDuration",
    173      "echoReturnLoss",
    174      "echoReturnLossEnhancement",
    175      "droppedSamplesDuration",
    176      "droppedSamplesEvents",
    177      "totalCaptureDelay",
    178      "totalSamplesCaptured",
    179    ],
    180    localAudioOnly: [],
    181    localVideoOnly: ["frames", "framesPerSecond", "width", "height"],
    182    optional: [],
    183    deprecated: [],
    184  },
    185  csrc: { skip: true },
    186  codec: {
    187    expected: [
    188      "timestamp",
    189      "type",
    190      "id",
    191      "payloadType",
    192      "transportId",
    193      "mimeType",
    194      "clockRate",
    195    ],
    196    optional: ["codecType", "channels", "sdpFmtpLine"],
    197    unimplemented: [],
    198    deprecated: [],
    199  },
    200  "peer-connection": { skip: true },
    201  "data-channel": { skip: true },
    202  track: { skip: true },
    203  transport: { skip: true },
    204  "candidate-pair": {
    205    expected: [
    206      "id",
    207      "timestamp",
    208      "type",
    209      "transportId",
    210      "localCandidateId",
    211      "remoteCandidateId",
    212      "state",
    213      "priority",
    214      "nominated",
    215      "writable",
    216      "readable",
    217      "bytesSent",
    218      "bytesReceived",
    219      "lastPacketSentTimestamp",
    220      "lastPacketReceivedTimestamp",
    221      "totalRoundTripTime",
    222      "currentRoundTripTime",
    223      "responsesReceived",
    224    ],
    225    optional: ["selected"],
    226    unimplemented: [
    227      "availableOutgoingBitrate",
    228      "availableIncomingBitrate",
    229      "requestsReceived",
    230      "requestsSent",
    231      "responsesSent",
    232      "retransmissionsReceived",
    233      "retransmissionsSent",
    234      "consentRequestsSent",
    235    ],
    236    deprecated: [],
    237  },
    238  "local-candidate": {
    239    expected: [
    240      "id",
    241      "timestamp",
    242      "type",
    243      "address",
    244      "protocol",
    245      "port",
    246      "candidateType",
    247      "priority",
    248    ],
    249    optional: ["relayProtocol", "proxied"],
    250    unimplemented: ["networkType", "url", "transportId"],
    251    deprecated: [
    252      "candidateId",
    253      "portNumber",
    254      "ipAddress",
    255      "componentId",
    256      "mozLocalTransport",
    257      "transport",
    258    ],
    259  },
    260  "remote-candidate": {
    261    expected: [
    262      "id",
    263      "timestamp",
    264      "type",
    265      "address",
    266      "protocol",
    267      "port",
    268      "candidateType",
    269      "priority",
    270    ],
    271    optional: ["relayProtocol", "proxied"],
    272    unimplemented: ["networkType", "url", "transportId"],
    273    deprecated: [
    274      "candidateId",
    275      "portNumber",
    276      "ipAddress",
    277      "componentId",
    278      "mozLocalTransport",
    279      "transport",
    280    ],
    281  },
    282  certificate: { skip: true },
    283 };
    284 
    285 ["inbound-rtp", "outbound-rtp", "media-source"].forEach(type => {
    286  let s = statsExpectedByType[type];
    287  s.optional = [...s.optional, ...s.localVideoOnly, ...s.localAudioOnly];
    288 });
    289 
    290 //
    291 //  Checks that the fields in a report conform to the expectations in
    292 // statExpectedByType
    293 //
    294 function checkExpectedFields(report) {
    295  report.forEach(stat => {
    296    let expectations = statsExpectedByType[stat.type];
    297    ok(expectations, "Stats type " + stat.type + " was expected");
    298    // If the type is not expected or if it is flagged for skipping continue to
    299    // the next
    300    if (!expectations || expectations.skip) {
    301      return;
    302    }
    303    // Check that all required fields exist
    304    expectations.expected.forEach(field => {
    305      ok(
    306        field in stat,
    307        "Expected stat field " + stat.type + "." + field + " exists"
    308      );
    309    });
    310    // Check that each field is either expected or optional
    311    let allowed = [...expectations.expected, ...expectations.optional];
    312    Object.keys(stat).forEach(field => {
    313      ok(
    314        allowed.includes(field),
    315        "Stat field " +
    316          stat.type +
    317          "." +
    318          field +
    319          ` is allowed. ${JSON.stringify(stat)}`
    320      );
    321    });
    322 
    323    //
    324    // Ensure that unimplemented fields are not implemented
    325    //   note: if a field is implemented it should be moved to expected or
    326    //   optional.
    327    //
    328    expectations.unimplemented.forEach(field => {
    329      ok(
    330        !Object.keys(stat).includes(field),
    331        "Unimplemented field " + stat.type + "." + field + " does not exist."
    332      );
    333    });
    334 
    335    //
    336    // Ensure that all deprecated fields are not present
    337    //
    338    expectations.deprecated.forEach(field => {
    339      ok(
    340        !Object.keys(stat).includes(field),
    341        "Deprecated field " + stat.type + "." + field + " does not exist."
    342      );
    343    });
    344  });
    345 }
    346 
    347 function pedanticChecks(report) {
    348  // Check that report is only-maplike
    349  [...report.keys()].forEach(key =>
    350    is(
    351      report[key],
    352      undefined,
    353      `Report is not dictionary like, it lacks a property for key ${key}`
    354    )
    355  );
    356  report.forEach((statObj, mapKey) => {
    357    info(`"${mapKey} = ${JSON.stringify(statObj, null, 2)}`);
    358  });
    359 
    360  // These matter when checking candidate-pair stats for bytes sent/received
    361  let sending = false;
    362  let receiving = false;
    363 
    364  // eslint-disable-next-line complexity
    365  report.forEach((statObj, mapKey) => {
    366    let tested = {};
    367    // Record what fields get tested.
    368    // To access a field foo without marking it as tested use stat.inner.foo
    369    let stat = new Proxy(statObj, {
    370      get(stat, key) {
    371        if (key == "inner") {
    372          return stat;
    373        }
    374        tested[key] = true;
    375        return stat[key];
    376      },
    377    });
    378 
    379    let expectations = statsExpectedByType[stat.type];
    380 
    381    if (expectations.skip) {
    382      return;
    383    }
    384 
    385    // All stats share the following attributes inherited from RTCStats
    386    is(stat.id, mapKey, stat.type + ".id is the same as the report key.");
    387 
    388    // timestamp
    389    ok(stat.timestamp >= 0, stat.type + ".timestamp is not less than 0");
    390    // If the timebase for the timestamp is not properly set the timestamp
    391    // will appear relative to the year 1970; Bug 1495446
    392    const date = new Date(stat.timestamp);
    393    ok(
    394      date.getFullYear() > 1970,
    395      `${stat.type}.timestamp is relative to current time, date=${date}`
    396    );
    397 
    398    //
    399    // RTCStreamStats attributes with common behavior
    400    //
    401    // inbound-rtp, outbound-rtp, remote-inbound-rtp, remote-outbound-rtp
    402    // inherit from RTCStreamStats
    403    if (
    404      [
    405        "inbound-rtp",
    406        "outbound-rtp",
    407        "remote-inbound-rtp",
    408        "remote-outbound-rtp",
    409      ].includes(stat.type)
    410    ) {
    411      const isRemote = stat.type.startsWith("remote-");
    412      //
    413      // Common RTCStreamStats fields
    414      //
    415 
    416      // SSRC
    417      ok(stat.ssrc, stat.type + ".ssrc has a value");
    418 
    419      // kind
    420      ok(
    421        ["audio", "video"].includes(stat.kind),
    422        stat.type + ".kind is 'audio' or 'video'"
    423      );
    424 
    425      // mediaType, renamed to kind but remains for backward compability.
    426      ok(
    427        ["audio", "video"].includes(stat.mediaType),
    428        stat.type + ".mediaType is 'audio' or 'video'"
    429      );
    430 
    431      ok(stat.kind == stat.mediaType, "kind equals legacy mediaType");
    432 
    433      // codecId
    434      ok(stat.codecId, `${stat.type}.codecId has a value`);
    435      ok(report.has(stat.codecId), `codecId ${stat.codecId} exists in report`);
    436      is(
    437        report.get(stat.codecId).type,
    438        "codec",
    439        `codecId ${stat.codecId} in report is codec type`
    440      );
    441      is(
    442        report.get(stat.codecId).mimeType.slice(0, 5),
    443        stat.kind,
    444        `codecId ${stat.codecId} in report is for a mimeType of the same ` +
    445          `media type as the referencing rtp stream stat`
    446      );
    447 
    448      if (isRemote) {
    449        // local id
    450        if (stat.localId) {
    451          ok(
    452            report.has(stat.localId),
    453            `localId ${stat.localId} exists in report.`
    454          );
    455          is(
    456            report.get(stat.localId).ssrc,
    457            stat.ssrc,
    458            "remote ssrc and local ssrc match."
    459          );
    460          is(
    461            report.get(stat.localId).remoteId,
    462            stat.id,
    463            "local object has remote object as it's own remote object."
    464          );
    465        }
    466      } else {
    467        // remote id
    468        if (stat.remoteId) {
    469          ok(
    470            report.has(stat.remoteId),
    471            `remoteId ${stat.remoteId} exists in report.`
    472          );
    473          is(
    474            report.get(stat.remoteId).ssrc,
    475            stat.ssrc,
    476            "remote ssrc and local ssrc match."
    477          );
    478          is(
    479            report.get(stat.remoteId).localId,
    480            stat.id,
    481            "remote object has local object as it's own local object."
    482          );
    483        }
    484      }
    485 
    486      // nackCount
    487      if (stat.nackCount) {
    488        ok(
    489          stat.nackCount >= 0,
    490          `${stat.type}.nackCount is sane (${stat.kind}).`
    491        );
    492      }
    493 
    494      if (!isRemote && stat.inner.kind == "video") {
    495        // firCount
    496        ok(
    497          stat.firCount >= 0 && stat.firCount < 100,
    498          `${stat.type}.firCount is a sane number for a short ` +
    499            `${stat.kind} test. value=${stat.firCount}`
    500        );
    501 
    502        // pliCount
    503        ok(
    504          stat.pliCount >= 0 && stat.pliCount < 200,
    505          `${stat.type}.pliCount is a sane number for a short ` +
    506            `${stat.kind} test. value=${stat.pliCount}`
    507        );
    508 
    509        // qpSum
    510        if (stat.qpSum !== undefined) {
    511          ok(
    512            stat.qpSum >= 0,
    513            `${stat.type}.qpSum is at least 0 ` +
    514              `${stat.kind} test. value=${stat.qpSum}`
    515          );
    516        }
    517      } else {
    518        is(
    519          stat.qpSum,
    520          undefined,
    521          `${stat.type}.qpSum does not exist when stat.kind != video`
    522        );
    523      }
    524    }
    525 
    526    if (stat.type == "inbound-rtp") {
    527      receiving = true;
    528 
    529      //
    530      // Required fields
    531      //
    532 
    533      // trackIdentifier
    534      is(typeof stat.trackIdentifier, "string");
    535      isnot(stat.trackIdentifier, "");
    536 
    537      // mid
    538      ok(
    539        parseInt(stat.mid) >= 0,
    540        `${stat.type}.mid is a positive integer. value=${stat.mid}`
    541      );
    542      let inboundRtpMids = [];
    543      report.forEach(r => {
    544        if (r.type == "inbound-rtp") {
    545          inboundRtpMids.push(r.mid);
    546        }
    547      });
    548      is(
    549        inboundRtpMids.filter(mid => mid == stat.mid).length,
    550        1,
    551        `${stat.type}.mid is distinct. value=${
    552          stat.mid
    553        }, others=${JSON.stringify(inboundRtpMids)}`
    554      );
    555 
    556      // packetsReceived
    557      ok(
    558        stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5,
    559        `${stat.type}.packetsReceived is a sane number for a short ` +
    560          `${stat.kind} test. value=${stat.packetsReceived}`
    561      );
    562 
    563      // packetsDiscarded
    564      ok(
    565        stat.packetsDiscarded >= 0 && stat.packetsDiscarded < 100,
    566        `${stat.type}.packetsDiscarded is sane number for a short test. ` +
    567          `value=${stat.packetsDiscarded}`
    568      );
    569      // bytesReceived
    570      ok(
    571        stat.bytesReceived >= 0 && stat.bytesReceived < 10 ** 9, // Not a magic number, just a guess
    572        `${stat.type}.bytesReceived is a sane number for a short ` +
    573          `${stat.kind} test. value=${stat.bytesReceived}`
    574      );
    575 
    576      // packetsLost
    577      ok(
    578        stat.packetsLost < 100,
    579        `${stat.type}.packetsLost is a sane number for a short ` +
    580          `${stat.kind} test. value=${stat.packetsLost}`
    581      );
    582 
    583      // This should be much lower for audio, TODO: Bug 1330575
    584      let expectedJitter = stat.kind == "video" ? 0.5 : 1;
    585      // jitter
    586      ok(
    587        stat.jitter < expectedJitter,
    588        `${stat.type}.jitter is sane number for a ${stat.kind} ` +
    589          `local only test. value=${stat.jitter}`
    590      );
    591 
    592      // lastPacketReceivedTimestamp
    593      ok(
    594        stat.lastPacketReceivedTimestamp !== undefined,
    595        `${stat.type}.lastPacketReceivedTimestamp has a value`
    596      );
    597 
    598      // headerBytesReceived
    599      ok(
    600        stat.headerBytesReceived >= 0 && stat.headerBytesReceived < 500000,
    601        `${stat.type}.headerBytesReceived is sane for a short test. ` +
    602          `value=${stat.headerBytesReceived}`
    603      );
    604 
    605      // estimatedPlayoutTimestamp
    606      if (stat.estimatedPlayoutTimestamp !== undefined) {
    607        ok(
    608          stat.estimatedPlayoutTimestamp < stat.timestamp + 100000,
    609          `${stat.type}.estimatedPlayoutTimestamp is not too far in the future`
    610        );
    611        ok(
    612          stat.estimatedPlayoutTimestamp > stat.timestamp - 100000,
    613          `${stat.type}.estimatedPlayoutTimestamp is not too far in the past`
    614        );
    615      }
    616 
    617      // jitterBufferEmittedCount
    618      ok(
    619        stat.jitterBufferEmittedCount > 0,
    620        `${stat.type}.jitterBufferEmittedCount is a sane number for a short ` +
    621          `${stat.kind} test. value=${stat.jitterBufferEmittedCount}`
    622      );
    623 
    624      // jitterBufferTargetDelay
    625      ok(
    626        stat.jitterBufferTargetDelay >= 0,
    627        `${stat.type}.jitterBufferTargetDelay is a sane number for a short ` +
    628          `${stat.kind} test. value=${stat.jitterBufferTargetDelay}`
    629      );
    630 
    631      // jitterBufferMinimumDelay
    632      ok(
    633        stat.jitterBufferMinimumDelay >= 0,
    634        `${stat.type}.jitterBufferMinimumDelay is a sane number for a short ` +
    635          `${stat.kind} test. value=${stat.jitterBufferMinimumDelay}`
    636      );
    637 
    638      // jitterBufferDelay
    639      let avgJitterBufferDelay =
    640        stat.jitterBufferDelay / stat.jitterBufferEmittedCount;
    641      ok(
    642        avgJitterBufferDelay > 0 && avgJitterBufferDelay < 10,
    643        `${stat.type}.jitterBufferDelay is a sane number for a short ` +
    644          `${stat.kind} test. value=${stat.jitterBufferDelay}/${stat.jitterBufferEmittedCount}=${avgJitterBufferDelay}`
    645      );
    646 
    647      //
    648      // Optional fields
    649      //
    650 
    651      //
    652      // Local audio only stats
    653      //
    654      if (stat.inner.kind != "audio") {
    655        expectations.localAudioOnly.forEach(field => {
    656          ok(
    657            stat[field] === undefined,
    658            `${stat.type} does not have field ${field}` +
    659              ` when kind is not 'audio'`
    660          );
    661        });
    662      } else {
    663        expectations.localAudioOnly.forEach(field => {
    664          ok(
    665            stat.inner[field] !== undefined,
    666            stat.type + " has field " + field + " when kind is video"
    667          );
    668        });
    669        // totalSamplesReceived
    670        ok(
    671          stat.totalSamplesReceived > 1000,
    672          `${stat.type}.totalSamplesReceived is a sane number for a short ` +
    673            `${stat.kind} test. value=${stat.totalSamplesReceived}`
    674        );
    675 
    676        // fecPacketsReceived
    677        ok(
    678          stat.fecPacketsReceived >= 0 && stat.fecPacketsReceived < 10 ** 5,
    679          `${stat.type}.fecPacketsReceived is a sane number for a short ` +
    680            `${stat.kind} test. value=${stat.fecPacketsReceived}`
    681        );
    682 
    683        // fecPacketsDiscarded
    684        ok(
    685          stat.fecPacketsDiscarded >= 0 && stat.fecPacketsDiscarded < 100,
    686          `${stat.type}.fecPacketsDiscarded is sane number for a short test. ` +
    687            `value=${stat.fecPacketsDiscarded}`
    688        );
    689        // concealedSamples
    690        ok(
    691          stat.concealedSamples >= 0 &&
    692            stat.concealedSamples <= stat.totalSamplesReceived,
    693          `${stat.type}.concealedSamples is a sane number for a short ` +
    694            `${stat.kind} test. value=${stat.concealedSamples}`
    695        );
    696 
    697        // silentConcealedSamples
    698        ok(
    699          stat.silentConcealedSamples >= 0 &&
    700            stat.silentConcealedSamples <= stat.concealedSamples,
    701          `${stat.type}.silentConcealedSamples is a sane number for a short ` +
    702            `${stat.kind} test. value=${stat.silentConcealedSamples}`
    703        );
    704 
    705        // concealmentEvents
    706        ok(
    707          stat.concealmentEvents >= 0 &&
    708            stat.concealmentEvents <= stat.packetsReceived,
    709          `${stat.type}.concealmentEvents is a sane number for a short ` +
    710            `${stat.kind} test. value=${stat.concealmentEvents}`
    711        );
    712 
    713        // insertedSamplesForDeceleration
    714        ok(
    715          stat.insertedSamplesForDeceleration >= 0 &&
    716            stat.insertedSamplesForDeceleration <= stat.totalSamplesReceived,
    717          `${stat.type}.insertedSamplesForDeceleration is a sane number for a short ` +
    718            `${stat.kind} test. value=${stat.insertedSamplesForDeceleration}`
    719        );
    720 
    721        // removedSamplesForAcceleration
    722        ok(
    723          stat.removedSamplesForAcceleration >= 0 &&
    724            stat.removedSamplesForAcceleration <= stat.totalSamplesReceived,
    725          `${stat.type}.removedSamplesForAcceleration is a sane number for a short ` +
    726            `${stat.kind} test. value=${stat.removedSamplesForAcceleration}`
    727        );
    728 
    729        // audioLevel
    730        ok(
    731          stat.audioLevel >= 0 && stat.audioLevel <= 128,
    732          `${stat.type}.bytesReceived is a sane number for a short ` +
    733            `${stat.kind} test. value=${stat.audioLevel}`
    734        );
    735 
    736        // totalAudioEnergy
    737        ok(
    738          stat.totalAudioEnergy >= 0 && stat.totalAudioEnergy <= 128,
    739          `${stat.type}.totalAudioEnergy is a sane number for a short ` +
    740            `${stat.kind} test. value=${stat.totalAudioEnergy}`
    741        );
    742 
    743        // totalSamplesDuration
    744        ok(
    745          stat.totalSamplesDuration >= 0 && stat.totalSamplesDuration <= 300,
    746          `${stat.type}.totalSamplesDuration is a sane number for a short ` +
    747            `${stat.kind} test. value=${stat.totalSamplesDuration}`
    748        );
    749      }
    750 
    751      //
    752      // Local video only stats
    753      //
    754      if (stat.inner.kind != "video") {
    755        expectations.localVideoOnly.forEach(field => {
    756          ok(
    757            stat[field] === undefined,
    758            `${stat.type} does not have field ${field}` +
    759              ` when kind is not 'video'`
    760          );
    761        });
    762      } else {
    763        expectations.localVideoOnly.forEach(field => {
    764          ok(
    765            stat.inner[field] !== undefined,
    766            stat.type + " has field " + field + " when kind is video"
    767          );
    768        });
    769        // discardedPackets
    770        ok(
    771          stat.discardedPackets < 100,
    772          `${stat.type}.discardedPackets is a sane number for a short test. ` +
    773            `value=${stat.discardedPackets}`
    774        );
    775        // framesPerSecond
    776        ok(
    777          stat.framesPerSecond > 0 && stat.framesPerSecond < 70,
    778          `${stat.type}.framesPerSecond is a sane number for a short ` +
    779            `${stat.kind} test. value=${stat.framesPerSecond}`
    780        );
    781 
    782        // framesDecoded
    783        ok(
    784          stat.framesDecoded > 0 && stat.framesDecoded < 1000000,
    785          `${stat.type}.framesDecoded is a sane number for a short ` +
    786            `${stat.kind} test. value=${stat.framesDecoded}`
    787        );
    788 
    789        // keyFramesDecoded
    790        ok(
    791          stat.keyFramesDecoded >= 0 && stat.keyFramesDecoded < 1000000,
    792          `${stat.type}.keyFramesDecoded is a sane number for a short ` +
    793            `${stat.kind} test. value=${stat.keyFramesDecoded}`
    794        );
    795 
    796        // framesDropped
    797        ok(
    798          stat.framesDropped >= 0 && stat.framesDropped < 100,
    799          `${stat.type}.framesDropped is a sane number for a short ` +
    800            `${stat.kind} test. value=${stat.framesDropped}`
    801        );
    802 
    803        // frameWidth
    804        ok(
    805          stat.frameWidth > 0 && stat.frameWidth < 100000,
    806          `${stat.type}.frameWidth is a sane number for a short ` +
    807            `${stat.kind} test. value=${stat.frameWidth}`
    808        );
    809 
    810        // frameHeight
    811        ok(
    812          stat.frameHeight > 0 && stat.frameHeight < 100000,
    813          `${stat.type}.frameHeight is a sane number for a short ` +
    814            `${stat.kind} test. value=${stat.frameHeight}`
    815        );
    816 
    817        // totalDecodeTime
    818        ok(
    819          stat.totalDecodeTime >= 0 && stat.totalDecodeTime < 300,
    820          `${stat.type}.totalDecodeTime is sane for a short test. ` +
    821            `value=${stat.totalDecodeTime}`
    822        );
    823 
    824        // totalProcessingDelay
    825        ok(
    826          stat.totalProcessingDelay <
    827            (navigator.userAgent.includes("Android") ? 2000 : 1000),
    828          `${stat.type}.totalProcessingDelay is sane number for a short test ` +
    829            `local only test. value=${stat.totalProcessingDelay}`
    830        );
    831 
    832        // totalInterFrameDelay
    833        ok(
    834          stat.totalInterFrameDelay >= 0 && stat.totalInterFrameDelay < 1000,
    835          `${stat.type}.totalInterFrameDelay is sane for a short test. ` +
    836            `value=${stat.totalInterFrameDelay}`
    837        );
    838 
    839        // totalSquaredInterFrameDelay
    840        ok(
    841          stat.totalSquaredInterFrameDelay >= 0 &&
    842            stat.totalSquaredInterFrameDelay < 10000,
    843          `${stat.type}.totalSquaredInterFrameDelay is sane for a short test. ` +
    844            `value=${stat.totalSquaredInterFrameDelay}`
    845        );
    846 
    847        // pauseCount
    848        ok(
    849          stat.pauseCount >= 0 && stat.pauseCount < 100,
    850          `${stat.type}.pauseCount is a sane number for a short ` +
    851            `${stat.kind} test. value=${stat.pauseCount}`
    852        );
    853 
    854        // totalPausesDuration
    855        ok(
    856          stat.totalPausesDuration >= 0 && stat.totalPausesDuration < 10000,
    857          `${stat.type}.totalPausesDuration is sane for a short test. ` +
    858            `value=${stat.totalPausesDuration}`
    859        );
    860 
    861        // freezeCount
    862        ok(
    863          stat.freezeCount >= 0 && stat.freezeCount < 100,
    864          `${stat.type}.freezeCount is a sane number for a short ` +
    865            `${stat.kind} test. value=${stat.freezeCount}`
    866        );
    867 
    868        // totalFreezesDuration
    869        ok(
    870          stat.totalFreezesDuration >= 0 && stat.totalFreezesDuration < 10000,
    871          `${stat.type}.totalFreezesDuration is sane for a short test. ` +
    872            `value=${stat.totalFreezesDuration}`
    873        );
    874 
    875        // framesReceived
    876        ok(
    877          stat.framesReceived >= 0 && stat.framesReceived < 100000,
    878          `${stat.type}.framesReceived is a sane number for a short ` +
    879            `${stat.kind} test. value=${stat.framesReceived}`
    880        );
    881 
    882        // framesAssembledFromMultiplePackets
    883        ok(
    884          stat.framesAssembledFromMultiplePackets >= 0 &&
    885            stat.framesAssembledFromMultiplePackets < 100,
    886          `${stat.type}.framesAssembledFromMultiplePackets is a sane number ` +
    887            `for a short ${stat.kind} test.` +
    888            `value=${stat.framesAssembledFromMultiplePackets}`
    889        );
    890 
    891        // totalAssemblyTime
    892        ok(
    893          stat.totalAssemblyTime >= 0 && stat.totalAssemblyTime < 10000,
    894          `${stat.type}.totalAssemblyTime is sane for a short test. ` +
    895            `value=${stat.totalAssemblyTime}`
    896        );
    897      }
    898    } else if (stat.type == "remote-inbound-rtp") {
    899      // roundTripTime
    900      ok(
    901        stat.roundTripTime >= 0,
    902        `${stat.type}.roundTripTime is sane with` +
    903          `value of: ${stat.roundTripTime} (${stat.kind})`
    904      );
    905      //
    906      // Required fields
    907      //
    908 
    909      // packetsLost
    910      ok(
    911        stat.packetsLost < 100,
    912        `${stat.type}.packetsLost is a sane number for a short ` +
    913          `${stat.kind} test. value=${stat.packetsLost}`
    914      );
    915 
    916      // jitter
    917      ok(
    918        stat.jitter >= 0,
    919        `${stat.type}.jitter is sane number (${stat.kind}). ` +
    920          `value=${stat.jitter}`
    921      );
    922 
    923      //
    924      // Optional fields
    925      //
    926 
    927      // packetsReceived
    928      if (stat.packetsReceived) {
    929        ok(
    930          stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5,
    931          `${stat.type}.packetsReceived is a sane number for a short ` +
    932            `${stat.kind} test. value=${stat.packetsReceived}`
    933        );
    934      }
    935 
    936      // totalRoundTripTime
    937      ok(
    938        stat.totalRoundTripTime < 50000,
    939        `${stat.type}.totalRoundTripTime is a sane number for a short ` +
    940          `${stat.kind} test. value=${stat.totalRoundTripTime}`
    941      );
    942 
    943      // fractionLost
    944      ok(
    945        stat.fractionLost < 0.2,
    946        `${stat.type}.fractionLost is a sane number for a short ` +
    947          `${stat.kind} test. value=${stat.fractionLost}`
    948      );
    949 
    950      // roundTripTimeMeasurements
    951      ok(
    952        stat.roundTripTimeMeasurements >= 1 &&
    953          stat.roundTripTimeMeasurements < 500,
    954        `${stat.type}.roundTripTimeMeasurements is a sane number for a short ` +
    955          `${stat.kind} test. value=${stat.roundTripTimeMeasurements}`
    956      );
    957    } else if (stat.type == "outbound-rtp") {
    958      sending = true;
    959 
    960      //
    961      // Required fields
    962      //
    963 
    964      // mid
    965      ok(
    966        parseInt(stat.mid) >= 0,
    967        `${stat.type}.mid a positive integer. value=${stat.mid}`
    968      );
    969 
    970      // packetsSent
    971      ok(
    972        stat.packetsSent > 0 && stat.packetsSent < 10000,
    973        `${stat.type}.packetsSent is a sane number for a short ` +
    974          `${stat.kind} test. value=${stat.packetsSent}`
    975      );
    976 
    977      // bytesSent
    978      const audio1Min = 16000 * 60; // 128kbps
    979      const video1Min = 250000 * 60; // 2Mbps
    980      ok(
    981        stat.bytesSent > 0 &&
    982          stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min),
    983        `${stat.type}.bytesSent is a sane number for a short ` +
    984          `${stat.kind} test. value=${stat.bytesSent}`
    985      );
    986 
    987      // headerBytesSent
    988      ok(
    989        stat.headerBytesSent > 0 &&
    990          stat.headerBytesSent < (stat.kind == "video" ? video1Min : audio1Min),
    991        `${stat.type}.headerBytesSent is a sane number for a short ` +
    992          `${stat.kind} test. value=${stat.headerBytesSent}`
    993      );
    994 
    995      // retransmittedPacketsSent
    996      ok(
    997        stat.retransmittedPacketsSent >= 0 &&
    998          stat.retransmittedPacketsSent <
    999            (stat.kind == "video" ? video1Min : audio1Min),
   1000        `${stat.type}.retransmittedPacketsSent is a sane number for a short ` +
   1001          `${stat.kind} test. value=${stat.retransmittedPacketsSent}`
   1002      );
   1003 
   1004      // retransmittedBytesSent
   1005      ok(
   1006        stat.retransmittedBytesSent >= 0 &&
   1007          stat.retransmittedBytesSent <
   1008            (stat.kind == "video" ? video1Min : audio1Min),
   1009        `${stat.type}.retransmittedBytesSent is a sane number for a short ` +
   1010          `${stat.kind} test. value=${stat.retransmittedBytesSent}`
   1011      );
   1012 
   1013      //
   1014      // Optional fields
   1015      //
   1016 
   1017      // rid
   1018      if (stat.kind == "audio") {
   1019        ok(
   1020          stat.rid === undefined,
   1021          `${stat.type}.rid" MUST NOT exist for audio. value=${stat.rid}`
   1022        );
   1023      } else {
   1024        let numSendVideoStreamsForMid = 0;
   1025        report.forEach(r => {
   1026          if (
   1027            r.type == "outbound-rtp" &&
   1028            r.kind == "video" &&
   1029            r.mid == stat.mid
   1030          ) {
   1031            numSendVideoStreamsForMid += 1;
   1032          }
   1033        });
   1034        if (numSendVideoStreamsForMid == 1) {
   1035          is(
   1036            stat.rid,
   1037            undefined,
   1038            `${stat.type}.rid" does not exist for singlecast video. value=${stat.rid}`
   1039          );
   1040        } else {
   1041          isnot(
   1042            stat.rid,
   1043            undefined,
   1044            `${stat.type}.rid" does exist for simulcast video. value=${stat.rid}`
   1045          );
   1046        }
   1047      }
   1048 
   1049      // qpSum
   1050      // This is supported for all of our vpx codecs and AV1 (on the encode
   1051      // side, see bug 1519590)
   1052      const mimeType = report.get(stat.codecId).mimeType;
   1053      if (mimeType.includes("VP") || mimeType.includes("AV1")) {
   1054        ok(
   1055          stat.qpSum >= 0,
   1056          `${stat.type}.qpSum is a sane number (${stat.kind}) ` +
   1057            `for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}`
   1058        );
   1059      } else if (mimeType.includes("H264")) {
   1060        // OpenH264 encoder records QP so we check for either condition.
   1061        if (!stat.qpSum && !("qpSum" in stat)) {
   1062          ok(
   1063            !stat.qpSum && !("qpSum" in stat),
   1064            `${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}`
   1065          );
   1066        } else {
   1067          ok(
   1068            stat.qpSum >= 0,
   1069            `${stat.type}.qpSum is a sane number (${stat.kind}) ` +
   1070              `for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}`
   1071          );
   1072        }
   1073      } else {
   1074        ok(
   1075          !stat.qpSum && !("qpSum" in stat),
   1076          `${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}`
   1077        );
   1078      }
   1079 
   1080      //
   1081      // Local video only stats
   1082      //
   1083      if (stat.inner.kind != "video") {
   1084        expectations.localVideoOnly.forEach(field => {
   1085          ok(
   1086            stat[field] === undefined,
   1087            `${stat.type} does not have field ` +
   1088              `${field} when kind is not 'video'`
   1089          );
   1090        });
   1091      } else {
   1092        expectations.localVideoOnly.forEach(field => {
   1093          ok(
   1094            stat.inner[field] !== undefined,
   1095            `${stat.type} has field ` +
   1096              `${field} when kind is video and isRemote is false`
   1097          );
   1098        });
   1099 
   1100        // framesEncoded
   1101        ok(
   1102          stat.framesEncoded >= 0 && stat.framesEncoded < 100000,
   1103          `${stat.type}.framesEncoded is a sane number for a short ` +
   1104            `${stat.kind} test. value=${stat.framesEncoded}`
   1105        );
   1106 
   1107        // frameWidth
   1108        ok(
   1109          stat.frameWidth >= 0 && stat.frameWidth < 100000,
   1110          `${stat.type}.frameWidth is a sane number for a short ` +
   1111            `${stat.kind} test. value=${stat.frameWidth}`
   1112        );
   1113 
   1114        // frameHeight
   1115        ok(
   1116          stat.frameHeight >= 0 && stat.frameHeight < 100000,
   1117          `${stat.type}.frameHeight is a sane number for a short ` +
   1118            `${stat.kind} test. value=${stat.frameHeight}`
   1119        );
   1120 
   1121        // framesPerSecond
   1122        ok(
   1123          stat.framesPerSecond >= 0 && stat.framesPerSecond < 60,
   1124          `${stat.type}.framesPerSecond is a sane number for a short ` +
   1125            `${stat.kind} test. value=${stat.framesPerSecond}`
   1126        );
   1127 
   1128        // framesSent
   1129        ok(
   1130          stat.framesSent >= 0 && stat.framesSent < 100000,
   1131          `${stat.type}.framesSent is a sane number for a short ` +
   1132            `${stat.kind} test. value=${stat.framesSent}`
   1133        );
   1134 
   1135        // hugeFramesSent
   1136        ok(
   1137          stat.hugeFramesSent >= 0 && stat.hugeFramesSent < 100000,
   1138          `${stat.type}.hugeFramesSent is a sane number for a short ` +
   1139            `${stat.kind} test. value=${stat.hugeFramesSent}`
   1140        );
   1141 
   1142        // totalEncodeTime
   1143        ok(
   1144          stat.totalEncodeTime >= 0,
   1145          `${stat.type}.totalEncodeTime is a sane number for a short ` +
   1146            `${stat.kind} test. value=${stat.totalEncodeTime}`
   1147        );
   1148 
   1149        // totalEncodedBytesTarget
   1150        ok(
   1151          stat.totalEncodedBytesTarget > 1000,
   1152          `${stat.type}.totalEncodedBytesTarget is a sane number for a short ` +
   1153            `${stat.kind} test. value=${stat.totalEncodedBytesTarget}`
   1154        );
   1155      }
   1156    } else if (stat.type == "remote-outbound-rtp") {
   1157      //
   1158      // Required fields
   1159      //
   1160 
   1161      // packetsSent
   1162      ok(
   1163        stat.packetsSent > 0 && stat.packetsSent < 10000,
   1164        `${stat.type}.packetsSent is a sane number for a short ` +
   1165          `${stat.kind} test. value=${stat.packetsSent}`
   1166      );
   1167 
   1168      // bytesSent
   1169      const audio1Min = 16000 * 60; // 128kbps
   1170      const video1Min = 250000 * 60; // 2Mbps
   1171      ok(
   1172        stat.bytesSent > 0 &&
   1173          stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min),
   1174        `${stat.type}.bytesSent is a sane number for a short ` +
   1175          `${stat.kind} test. value=${stat.bytesSent}`
   1176      );
   1177 
   1178      ok(
   1179        stat.remoteTimestamp !== undefined,
   1180        `${stat.type}.remoteTimestamp ` + `is not undefined (${stat.kind})`
   1181      );
   1182      const ageSeconds = (stat.timestamp - stat.remoteTimestamp) / 1000;
   1183      // remoteTimestamp is exact (so it can be mapped to a packet), whereas
   1184      // timestamp has reduced precision. It is possible that
   1185      // remoteTimestamp occurs a millisecond into the future from
   1186      // timestamp. We also subtract half a millisecond when reducing
   1187      // precision on libwebrtc timestamps, to counteract the potential
   1188      // rounding up that libwebrtc may do since it tends to round its
   1189      // internal timestamps to whole milliseconds. In the worst case
   1190      // remoteTimestamp may therefore occur 2 milliseconds ahead of
   1191      // timestamp.
   1192      ok(
   1193        ageSeconds >= -0.002 && ageSeconds < 30,
   1194        `${stat.type}.remoteTimestamp is on the same timeline as ` +
   1195          `${stat.type}.timestamp, and no older than 30 seconds. ` +
   1196          `difference=${ageSeconds}s`
   1197      );
   1198    } else if (stat.type == "media-source") {
   1199      // trackIdentifier
   1200      is(typeof stat.trackIdentifier, "string");
   1201      isnot(stat.trackIdentifier, "");
   1202 
   1203      // kind
   1204      is(typeof stat.kind, "string");
   1205      ok(stat.kind == "audio" || stat.kind == "video");
   1206      if (stat.inner.kind == "video") {
   1207        expectations.localVideoOnly.forEach(field => {
   1208          ok(
   1209            stat.inner[field] !== undefined,
   1210            `${stat.type} has field ` +
   1211              `${field} when kind is video and isRemote is false`
   1212          );
   1213        });
   1214 
   1215        // frames
   1216        ok(
   1217          stat.frames >= 0 && stat.frames < 100000,
   1218          `${stat.type}.frames is a sane number for a short ` +
   1219            `${stat.kind} test. value=${stat.frames}`
   1220        );
   1221 
   1222        // framesPerSecond
   1223        ok(
   1224          stat.framesPerSecond >= 0 && stat.framesPerSecond < 100,
   1225          `${stat.type}.framesPerSecond is a sane number for a short ` +
   1226            `${stat.kind} test. value=${stat.framesPerSecond}`
   1227        );
   1228 
   1229        // width
   1230        ok(
   1231          stat.width >= 0 && stat.width < 1000000,
   1232          `${stat.type}.width is a sane number for a ` +
   1233            `${stat.kind} test. value=${stat.width}`
   1234        );
   1235 
   1236        // height
   1237        ok(
   1238          stat.height >= 0 && stat.height < 1000000,
   1239          `${stat.type}.height is a sane number for a ` +
   1240            `${stat.kind} test. value=${stat.height}`
   1241        );
   1242      } else {
   1243        expectations.localVideoOnly.forEach(field => {
   1244          ok(
   1245            stat[field] === undefined,
   1246            `${stat.type} does not have field ` +
   1247              `${field} when kind is not 'video'`
   1248          );
   1249        });
   1250      }
   1251    } else if (stat.type == "codec") {
   1252      //
   1253      // Required fields
   1254      //
   1255 
   1256      // mimeType & payloadType
   1257      switch (stat.mimeType) {
   1258        case "audio/opus":
   1259          is(stat.payloadType, 109, "codec.payloadType for opus");
   1260          break;
   1261        case "video/VP8":
   1262          is(stat.payloadType, 120, "codec.payloadType for VP8");
   1263          break;
   1264        case "video/VP9":
   1265          is(stat.payloadType, 121, "codec.payloadType for VP9");
   1266          break;
   1267        case "video/H264":
   1268          ok(
   1269            stat.payloadType == 97 ||
   1270              stat.payloadType == 126 ||
   1271              stat.payloadType == 103 ||
   1272              stat.payloadType == 105,
   1273            `codec.payloadType for H264 was ${stat.payloadType}, exp. 97, 126, 103, or 105`
   1274          );
   1275          break;
   1276        case "video/AV1":
   1277          is(stat.payloadType, 99, "codec.payloadType for AV1");
   1278          break;
   1279        default:
   1280          ok(
   1281            false,
   1282            `Unexpected codec.mimeType ${stat.mimeType} for payloadType ` +
   1283              `${stat.payloadType}`
   1284          );
   1285          break;
   1286      }
   1287 
   1288      // transportId
   1289      // (no transport stats yet)
   1290      ok(stat.transportId, "codec.transportId is set");
   1291 
   1292      // clockRate
   1293      if (stat.mimeType.startsWith("audio")) {
   1294        is(stat.clockRate, 48000, "codec.clockRate for audio/opus");
   1295      } else if (stat.mimeType.startsWith("video")) {
   1296        is(stat.clockRate, 90000, "codec.clockRate for video");
   1297      }
   1298 
   1299      // sdpFmtpLine
   1300      // (not technically mandated by spec, but expected here)
   1301      // AV1 has no required parameters, so don't require sdpFmtpLine for it.
   1302      if (stat.mimeType != "video/AV1") {
   1303        ok(stat.sdpFmtpLine, "codec.sdp FmtpLine is set");
   1304      }
   1305      const opusParams = [
   1306        "maxplaybackrate",
   1307        "maxaveragebitrate",
   1308        "usedtx",
   1309        "stereo",
   1310        "useinbandfec",
   1311        "cbr",
   1312        "ptime",
   1313        "minptime",
   1314        "maxptime",
   1315      ];
   1316      const vpxParams = ["max-fs", "max-fr"];
   1317      const h264Params = [
   1318        "packetization-mode",
   1319        "level-asymmetry-allowed",
   1320        "profile-level-id",
   1321        "max-fs",
   1322        "max-cpb",
   1323        "max-dpb",
   1324        "max-br",
   1325        "max-mbps",
   1326      ];
   1327      // AV1 parameters:
   1328      //  https://aomediacodec.github.io/av1-rtp-spec/#721-mapping-of-media-subtype-parameters-to-sdp
   1329      const av1Params = ["profile", "level-idx", "tier"];
   1330      // Check that the parameters are as expected. AV1 may have no parameters.
   1331      for (const param of (stat.sdpFmtpLine || "").split(";")) {
   1332        const [key, value] = param.split("=");
   1333        if (stat.payloadType == 99) {
   1334          // AV1 might not have any parameters, if it does make sure they are as expected.
   1335          if (key) {
   1336            ok(
   1337              av1Params.includes(key),
   1338              `codec.sdpFmtpLine param ${key}=${value} for AV1`
   1339            );
   1340          }
   1341        } else if (stat.payloadType == 109) {
   1342          ok(
   1343            opusParams.includes(key),
   1344            `codec.sdpFmtpLine param ${key}=${value} for opus`
   1345          );
   1346        } else if (stat.payloadType == 120 || stat.payloadType == 121) {
   1347          ok(
   1348            vpxParams.includes(key),
   1349            `codec.sdpFmtpLine param ${key}=${value} for VPx`
   1350          );
   1351        } else if (stat.payloadType == 97 || stat.payloadType == 126) {
   1352          ok(
   1353            h264Params.includes(key),
   1354            `codec.sdpFmtpLine param ${key}=${value} for H264`
   1355          );
   1356          if (key == "packetization-mode") {
   1357            if (stat.payloadType == 97) {
   1358              is(value, "0", "codec.sdpFmtpLine: H264 (97) packetization-mode");
   1359            } else if (stat.payloadType == 126) {
   1360              is(
   1361                value,
   1362                "1",
   1363                "codec.sdpFmtpLine: H264 (126) packetization-mode"
   1364              );
   1365            }
   1366          }
   1367          if (key == "profile-level-id") {
   1368            is(value, "42e01f", "codec.sdpFmtpLine: H264 profile-level-id");
   1369          }
   1370        }
   1371      }
   1372 
   1373      //
   1374      // Optional fields
   1375      //
   1376 
   1377      // codecType
   1378      ok(
   1379        !Object.keys(stat).includes("codecType") ||
   1380          stat.codecType == "encode" ||
   1381          stat.codecType == "decode",
   1382        "codec.codecType (${codec.codecType}) is an expected value or absent"
   1383      );
   1384      let numRecvStreams = 0;
   1385      let numSendStreams = 0;
   1386      const counts = {
   1387        "inbound-rtp": 0,
   1388        "outbound-rtp": 0,
   1389        "remote-inbound-rtp": 0,
   1390        "remote-outbound-rtp": 0,
   1391      };
   1392      const [kind] = stat.mimeType.split("/");
   1393      report.forEach(other => {
   1394        if (other.type == "inbound-rtp" && other.kind == kind) {
   1395          numRecvStreams += 1;
   1396        } else if (other.type == "outbound-rtp" && other.kind == kind) {
   1397          numSendStreams += 1;
   1398        }
   1399        if (other.codecId == stat.id) {
   1400          counts[other.type] += 1;
   1401        }
   1402      });
   1403      const expectedCounts = {
   1404        encode: {
   1405          "inbound-rtp": 0,
   1406          "outbound-rtp": numSendStreams,
   1407          "remote-inbound-rtp": numSendStreams,
   1408          "remote-outbound-rtp": 0,
   1409        },
   1410        decode: {
   1411          "inbound-rtp": numRecvStreams,
   1412          "outbound-rtp": 0,
   1413          "remote-inbound-rtp": 0,
   1414          "remote-outbound-rtp": numRecvStreams,
   1415        },
   1416        absent: {
   1417          "inbound-rtp": numRecvStreams,
   1418          "outbound-rtp": numSendStreams,
   1419          "remote-inbound-rtp": numSendStreams,
   1420          "remote-outbound-rtp": numRecvStreams,
   1421        },
   1422      };
   1423      // Note that the logic above assumes at most one sender and at most one
   1424      // receiver was used to generate this stats report. If more senders or
   1425      // receivers are present, they'd be referring to not only this codec stat,
   1426      // skewing `numSendStreams` and `numRecvStreams` above.
   1427      // This could be fixed when we support `senderId` and `receiverId` in
   1428      // RTCOutboundRtpStreamStats and RTCInboundRtpStreamStats respectively.
   1429      for (const [key, value] of Object.entries(counts)) {
   1430        is(
   1431          value,
   1432          expectedCounts[stat.codecType || "absent"][key],
   1433          `codec.codecType ${stat.codecType || "absent"} ref from ${key} stat`
   1434        );
   1435      }
   1436 
   1437      // channels
   1438      if (stat.mimeType.startsWith("audio")) {
   1439        ok(stat.channels, "codec.channels should exist for audio");
   1440        if (stat.channels) {
   1441          if (stat.sdpFmtpLine.includes("stereo=1")) {
   1442            is(stat.channels, 2, "codec.channels for stereo audio");
   1443          } else {
   1444            is(stat.channels, 1, "codec.channels for mono audio");
   1445          }
   1446        }
   1447      } else {
   1448        ok(!stat.channels, "codec.channels should not exist for video");
   1449      }
   1450    } else if (stat.type == "candidate-pair") {
   1451      info("candidate-pair is: " + JSON.stringify(stat));
   1452      //
   1453      // Required fields
   1454      //
   1455 
   1456      // transportId
   1457      ok(
   1458        stat.transportId,
   1459        `${stat.type}.transportId has a value. value=` +
   1460          `${stat.transportId} (${stat.kind})`
   1461      );
   1462 
   1463      // localCandidateId
   1464      ok(
   1465        stat.localCandidateId,
   1466        `${stat.type}.localCandidateId has a value. value=` +
   1467          `${stat.localCandidateId} (${stat.kind})`
   1468      );
   1469 
   1470      // remoteCandidateId
   1471      ok(
   1472        stat.remoteCandidateId,
   1473        `${stat.type}.remoteCandidateId has a value. value=` +
   1474          `${stat.remoteCandidateId} (${stat.kind})`
   1475      );
   1476 
   1477      // priority
   1478      ok(
   1479        stat.priority,
   1480        `${stat.type}.priority has a value. value=` +
   1481          `${stat.priority} (${stat.kind})`
   1482      );
   1483 
   1484      // readable
   1485      ok(
   1486        stat.readable,
   1487        `${stat.type}.readable is true. value=${stat.readable} ` +
   1488          `(${stat.kind})`
   1489      );
   1490 
   1491      // writable
   1492      ok(
   1493        stat.writable,
   1494        `${stat.type}.writable is true. value=${stat.writable} ` +
   1495          `(${stat.kind})`
   1496      );
   1497 
   1498      // totalRoundTripTime
   1499      ok(
   1500        stat.totalRoundTripTime !== undefined && stat.totalRoundTripTime >= 0,
   1501        `${stat.type}.totalRoundTripTime has a value. value=` +
   1502          `${stat.totalRoundTripTime} (${stat.kind})`
   1503      );
   1504 
   1505      // currentRoundTripTime
   1506      ok(
   1507        stat.currentRoundTripTime !== undefined &&
   1508          stat.currentRoundTripTime >= 0,
   1509        `${stat.type}.currentRoundTripTime has a value. value=` +
   1510          `${stat.currentRoundTripTime} (${stat.kind})`
   1511      );
   1512 
   1513      // responsesReceived
   1514      ok(
   1515        stat.responsesReceived !== undefined && stat.responsesReceived >= 0,
   1516        `${stat.type}.responsesReceived has a value. value=` +
   1517          `${stat.responsesReceived} (${stat.kind})`
   1518      );
   1519 
   1520      const stateValues = [
   1521        "frozen",
   1522        "waiting",
   1523        "in-progress",
   1524        "failed",
   1525        "succeeded",
   1526        "cancelled",
   1527      ];
   1528      ok(
   1529        stateValues.includes(stat.state),
   1530        `${stat.type}.state '${stat.state}' not in ${stateValues}`
   1531      );
   1532 
   1533      // state
   1534      if (
   1535        stat.state == "succeeded" &&
   1536        stat.selected !== undefined &&
   1537        stat.selected
   1538      ) {
   1539        info("candidate-pair state is succeeded and selected is true");
   1540        // nominated
   1541        ok(
   1542          stat.nominated,
   1543          `${stat.type}.nominated is true. value=${stat.nominated} ` +
   1544            `(${stat.kind})`
   1545        );
   1546 
   1547        const sentExpectation = sending ? 100 : 0;
   1548 
   1549        // bytesSent
   1550        ok(
   1551          stat.bytesSent >= sentExpectation,
   1552          `${stat.type}.bytesSent is a sane number (>${sentExpectation}) if media is flowing. ` +
   1553            `value=${stat.bytesSent}`
   1554        );
   1555 
   1556        const recvExpectation = receiving ? 100 : 0;
   1557 
   1558        // bytesReceived
   1559        ok(
   1560          stat.bytesReceived >= recvExpectation,
   1561          `${stat.type}.bytesReceived is a sane number (>${recvExpectation}) if media is flowing. ` +
   1562            `value=${stat.bytesReceived}`
   1563        );
   1564 
   1565        // lastPacketSentTimestamp
   1566        ok(
   1567          stat.lastPacketSentTimestamp,
   1568          `${stat.type}.lastPacketSentTimestamp has a value. value=` +
   1569            `${stat.lastPacketSentTimestamp} (${stat.kind})`
   1570        );
   1571 
   1572        // lastPacketReceivedTimestamp
   1573        ok(
   1574          stat.lastPacketReceivedTimestamp,
   1575          `${stat.type}.lastPacketReceivedTimestamp has a value. value=` +
   1576            `${stat.lastPacketReceivedTimestamp} (${stat.kind})`
   1577        );
   1578      } else {
   1579        info("candidate-pair is _not_ both state == succeeded and selected");
   1580        // nominated
   1581        ok(
   1582          stat.nominated !== undefined,
   1583          `${stat.type}.nominated exists. value=${stat.nominated} ` +
   1584            `(${stat.kind})`
   1585        );
   1586        ok(
   1587          stat.bytesSent !== undefined,
   1588          `${stat.type}.bytesSent exists. value=${stat.bytesSent} ` +
   1589            `(${stat.kind})`
   1590        );
   1591        ok(
   1592          stat.bytesReceived !== undefined,
   1593          `${stat.type}.bytesReceived exists. value=${stat.bytesReceived} ` +
   1594            `(${stat.kind})`
   1595        );
   1596        ok(
   1597          stat.lastPacketSentTimestamp !== undefined,
   1598          `${stat.type}.lastPacketSentTimestamp exists. value=` +
   1599            `${stat.lastPacketSentTimestamp} (${stat.kind})`
   1600        );
   1601        ok(
   1602          stat.lastPacketReceivedTimestamp !== undefined,
   1603          `${stat.type}.lastPacketReceivedTimestamp exists. value=` +
   1604            `${stat.lastPacketReceivedTimestamp} (${stat.kind})`
   1605        );
   1606      }
   1607 
   1608      //
   1609      // Optional fields
   1610      //
   1611      // selected
   1612      ok(
   1613        stat.selected === undefined ||
   1614          (stat.state == "succeeded" && stat.selected) ||
   1615          !stat.selected,
   1616        `${stat.type}.selected is undefined, true when state is succeeded, ` +
   1617          `or false. value=${stat.selected} (${stat.kind})`
   1618      );
   1619    } else if (
   1620      stat.type == "local-candidate" ||
   1621      stat.type == "remote-candidate"
   1622    ) {
   1623      info(`candidate is ${JSON.stringify(stat)}`);
   1624 
   1625      // address
   1626      ok(
   1627        stat.address,
   1628        `${stat.type} has address. value=${stat.address} ` + `(${stat.kind})`
   1629      );
   1630 
   1631      // protocol
   1632      ok(
   1633        stat.protocol,
   1634        `${stat.type} has protocol. value=${stat.protocol} ` + `(${stat.kind})`
   1635      );
   1636 
   1637      // port
   1638      ok(
   1639        stat.port >= 0,
   1640        `${stat.type} has port >= 0. value=${stat.port} ` + `(${stat.kind})`
   1641      );
   1642      ok(
   1643        stat.port <= 65535,
   1644        `${stat.type} has port <= 65535. value=${stat.port} ` + `(${stat.kind})`
   1645      );
   1646 
   1647      // candidateType
   1648      ok(
   1649        stat.candidateType,
   1650        `${stat.type} has candidateType. value=${stat.candidateType} ` +
   1651          `(${stat.kind})`
   1652      );
   1653 
   1654      // priority
   1655      ok(
   1656        stat.priority > 0 && stat.priority < 2 ** 32 - 1,
   1657        `${stat.type} has priority between 1 and 2^32 - 1 inc. ` +
   1658          `value=${stat.priority} (${stat.kind})`
   1659      );
   1660 
   1661      // relayProtocol
   1662      if (stat.type == "local-candidate" && stat.candidateType == "relay") {
   1663        ok(
   1664          stat.relayProtocol,
   1665          `relay ${stat.type} has relayProtocol. value=${stat.relayProtocol} ` +
   1666            `(${stat.kind})`
   1667        );
   1668      } else {
   1669        is(
   1670          stat.relayProtocol,
   1671          undefined,
   1672          `relayProtocol is undefined for candidates that are not relay and ` +
   1673            `local. value=${stat.relayProtocol} (${stat.kind})`
   1674        );
   1675      }
   1676 
   1677      // proxied
   1678      if (stat.proxied) {
   1679        ok(
   1680          stat.proxied == "proxied" || stat.proxied == "non-proxied",
   1681          `${stat.type} has proxied. value=${stat.proxied} (${stat.kind})`
   1682        );
   1683      }
   1684    }
   1685 
   1686    //
   1687    // Ensure everything was tested
   1688    //
   1689    [...expectations.expected, ...expectations.optional].forEach(field => {
   1690      ok(
   1691        Object.keys(tested).includes(field),
   1692        `${stat.type}.${field} was tested.`
   1693      );
   1694    });
   1695  });
   1696 }
   1697 
   1698 function dumpStats(stats) {
   1699  const dict = {};
   1700  for (const [k, v] of stats.entries()) {
   1701    dict[k] = v;
   1702  }
   1703  info(`Got stats: ${JSON.stringify(dict)}`);
   1704 }
   1705 
   1706 async function waitForSyncedRtcp(pc) {
   1707  // Ensures that RTCP is present
   1708  let ensureSyncedRtcp = async () => {
   1709    let report = await pc.getStats();
   1710    for (const v of report.values()) {
   1711      if (v.type.endsWith("bound-rtp") && !(v.remoteId || v.localId)) {
   1712        info(`${v.id} is missing remoteId or localId: ${JSON.stringify(v)}`);
   1713        return null;
   1714      }
   1715      if (v.type == "remote-inbound-rtp" && v.roundTripTime === undefined) {
   1716        info(`${v.id} is missing roundTripTime: ${JSON.stringify(v)}`);
   1717        return null;
   1718      }
   1719    }
   1720    return report;
   1721  };
   1722  // Returns true if there is proof in aStats of rtcp flow for all remote stats
   1723  // objects, compared to baseStats.
   1724  const hasAllRtcpUpdated = (baseStats, stats) => {
   1725    let hasRtcpStats = false;
   1726    for (const v of stats.values()) {
   1727      if (v.type == "remote-outbound-rtp") {
   1728        hasRtcpStats = true;
   1729        if (!v.remoteTimestamp) {
   1730          // `remoteTimestamp` is 0 or not present.
   1731          return false;
   1732        }
   1733        if (v.remoteTimestamp <= baseStats.get(v.id)?.remoteTimestamp) {
   1734          // `remoteTimestamp` has not advanced further than the base stats,
   1735          // i.e., no new sender report has been received.
   1736          return false;
   1737        }
   1738      } else if (v.type == "remote-inbound-rtp") {
   1739        hasRtcpStats = true;
   1740        // The ideal thing here would be to check `reportsReceived`, but it's
   1741        // not yet implemented.
   1742        if (!v.packetsReceived) {
   1743          // `packetsReceived` is 0 or not present.
   1744          return false;
   1745        }
   1746        if (v.packetsReceived <= baseStats.get(v.id)?.packetsReceived) {
   1747          // `packetsReceived` has not advanced further than the base stats,
   1748          // i.e., no new receiver report has been received.
   1749          return false;
   1750        }
   1751      }
   1752    }
   1753    return hasRtcpStats;
   1754  };
   1755  let attempts = 0;
   1756  const baseStats = await pc.getStats();
   1757  // Time-units are MS
   1758  const waitPeriod = 100;
   1759  const maxTime = 20000;
   1760  for (let totalTime = maxTime; totalTime > 0; totalTime -= waitPeriod) {
   1761    try {
   1762      let syncedStats = await ensureSyncedRtcp();
   1763      if (syncedStats && hasAllRtcpUpdated(baseStats, syncedStats)) {
   1764        dumpStats(syncedStats);
   1765        return syncedStats;
   1766      }
   1767    } catch (e) {
   1768      info(e);
   1769      info(e.stack);
   1770      throw e;
   1771    }
   1772    attempts += 1;
   1773    info(`waitForSyncedRtcp: no sync on attempt ${attempts}, retrying.`);
   1774    await wait(waitPeriod);
   1775  }
   1776  throw Error(
   1777    "Waiting for synced RTCP timed out after at least " + maxTime + "ms"
   1778  );
   1779 }
   1780 
   1781 function checkSendCodecsMimeType(senderStats, mimeType, sdpFmtpLine = null) {
   1782  const codecReports = senderStats.values().filter(s => s.type == "codec");
   1783  isnot(codecReports.length, 0, "Should have send codecs");
   1784  for (const c of codecReports) {
   1785    is(c.codecType, "encode", "Send codec is always encode");
   1786    is(c.mimeType, mimeType, "Mime type as expected");
   1787    if (sdpFmtpLine) {
   1788      is(c.sdpFmtpLine, sdpFmtpLine, "Sdp fmtp line as expected");
   1789    }
   1790  }
   1791 }
   1792 
   1793 function checkSenderStats(senderStats, streamCount) {
   1794  const outboundRtpReports = [];
   1795  const remoteInboundRtpReports = [];
   1796  for (const v of senderStats.values()) {
   1797    if (v.type == "outbound-rtp") {
   1798      outboundRtpReports.push(v);
   1799    } else if (v.type == "remote-inbound-rtp") {
   1800      remoteInboundRtpReports.push(v);
   1801    }
   1802  }
   1803  is(
   1804    outboundRtpReports.length,
   1805    streamCount,
   1806    `Sender with ${streamCount} simulcast streams has ${streamCount} outbound-rtp reports`
   1807  );
   1808  is(
   1809    remoteInboundRtpReports.length,
   1810    streamCount,
   1811    `Sender with ${streamCount} simulcast streams has ${streamCount} remote-inbound-rtp reports`
   1812  );
   1813  for (const outboundRtpReport of outboundRtpReports) {
   1814    is(
   1815      outboundRtpReports.filter(r => r.ssrc == outboundRtpReport.ssrc).length,
   1816      1,
   1817      "Simulcast send track SSRCs are distinct"
   1818    );
   1819    is(
   1820      outboundRtpReports.filter(r => r.mid == outboundRtpReport.mid).length,
   1821      streamCount,
   1822      "Simulcast send track MIDs are identical"
   1823    );
   1824    if (outboundRtpReport.kind == "video" && streamCount > 1) {
   1825      is(
   1826        outboundRtpReports.filter(r => r.rid == outboundRtpReport.rid).length,
   1827        1,
   1828        "Simulcast send track RIDs are distinct"
   1829      );
   1830    }
   1831    const remoteReports = remoteInboundRtpReports.filter(
   1832      r => r.id == outboundRtpReport.remoteId
   1833    );
   1834    is(
   1835      remoteReports.length,
   1836      1,
   1837      "Simulcast send tracks have exactly one remote counterpart"
   1838    );
   1839    const remoteInboundRtpReport = remoteReports[0];
   1840    is(
   1841      outboundRtpReport.ssrc,
   1842      remoteInboundRtpReport.ssrc,
   1843      "SSRC matches for outbound-rtp and remote-inbound-rtp"
   1844    );
   1845  }
   1846 }
   1847 
   1848 function PC_LOCAL_TEST_LOCAL_STATS(test) {
   1849  return waitForSyncedRtcp(test.pcLocal._pc).then(stats => {
   1850    checkExpectedFields(stats);
   1851    pedanticChecks(stats);
   1852    return Promise.all([
   1853      test.pcLocal._pc.getSenders().map(async s => {
   1854        checkSenderStats(
   1855          await s.getStats(),
   1856          Math.max(1, s.getParameters()?.encodings?.length ?? 0)
   1857        );
   1858      }),
   1859    ]);
   1860  });
   1861 }
   1862 
   1863 function PC_REMOTE_TEST_REMOTE_STATS(test) {
   1864  return waitForSyncedRtcp(test.pcRemote._pc).then(stats => {
   1865    checkExpectedFields(stats);
   1866    pedanticChecks(stats);
   1867    return Promise.all([
   1868      test.pcRemote._pc.getSenders().map(async s => {
   1869        checkSenderStats(
   1870          await s.getStats(),
   1871          s.track ? Math.max(1, s.getParameters()?.encodings?.length ?? 0) : 0
   1872        );
   1873      }),
   1874    ]);
   1875  });
   1876 }