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 }