tor-browser

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

test_accumulated_play_time.html (25596B)


      1 <!DOCTYPE HTML>
      2 <html>
      3 <head>
      4 <title>Test Video Play Time Related Permanent Telemetry Probes</title>
      5 <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
      6 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
      7 <script type="application/javascript">
      8 
      9 /**
     10 * This test is used to ensure that we accumulate time for video playback
     11 * correctly, and the results would be used in Telemetry probes.
     12 * Currently this test covers following probes
     13 * - VIDEO_PLAY_TIME_MS
     14 * - VIDEO_HDR_PLAY_TIME_MS
     15 * - VIDEO_HIDDEN_PLAY_TIME_MS
     16 * - VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE
     17 * - VIDEO_VISIBLE_PLAY_TIME_MS
     18 * - MEDIA_PLAY_TIME_MS
     19 * - MUTED_PLAY_TIME_PERCENT
     20 * - AUDIBLE_PLAY_TIME_PERCENT
     21 */
     22 const videoHistNames = [
     23  "VIDEO_PLAY_TIME_MS",
     24  "VIDEO_HIDDEN_PLAY_TIME_MS"
     25 ];
     26 const videoHDRHistNames = [
     27  "VIDEO_HDR_PLAY_TIME_MS"
     28 ];
     29 const videoKeyedHistNames = [
     30  "VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE",
     31  "VIDEO_VISIBLE_PLAY_TIME_MS"
     32 ];
     33 const audioKeyedHistNames = [
     34  "MUTED_PLAY_TIME_PERCENT",
     35  "AUDIBLE_PLAY_TIME_PERCENT"
     36 ];
     37 
     38 add_task(async function setTestPref() {
     39  await SpecialPowers.pushPrefEnv({
     40    set: [["media.testing-only-events", true],
     41          ["media.test.video-suspend", true],
     42          ["media.suspend-background-video.enabled", true],
     43          ["media.suspend-background-video.delay-ms", 0],
     44          ["dom.media.silence_duration_for_audibility", 0.1]
     45    ]});
     46 });
     47 
     48 add_task(async function testTotalPlayTime() {
     49  const video = document.createElement('video');
     50  video.src = "gizmo.mp4";
     51  document.body.appendChild(video);
     52 
     53  info(`all accumulated time should be zero`);
     54  const videoChrome = SpecialPowers.wrap(video);
     55  await new Promise(r => video.onloadeddata = r);
     56  assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
     57  assertValueEqualTo(videoChrome, "invisiblePlayTime", 0);
     58 
     59  info(`start accumulating play time after media starts`);
     60  video.autoplay = true;
     61  await Promise.all([
     62    once(video, "playing"),
     63    once(video, "moztotalplaytimestarted"),
     64  ]);
     65  await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
     66  assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
     67 
     68  info(`should not accumulate time for paused video`);
     69  video.pause();
     70  await once(video, "moztotalplaytimepaused");
     71  assertValueKeptUnchanged(videoChrome, "totalVideoPlayTime");
     72  assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
     73 
     74  info(`should start accumulating time again`);
     75  let rv = await Promise.all([
     76    onceWithTrueReturn(video, "moztotalplaytimestarted"),
     77    video.play().then(_ => true, _ => false),
     78  ]);
     79  ok(returnTrueWhenAllValuesAreTrue(rv), "video started again");
     80  await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
     81  await cleanUpMediaAndCheckTelemetry(video);
     82 });
     83 
     84 // The testHDRPlayTime task will only pass on platforms that accurately report
     85 // color depth in their VideoInfo structures. Presently, that is only true for
     86 // macOS.
     87 const {AppConstants} = ChromeUtils.importESModule(
     88  "resource://gre/modules/AppConstants.sys.mjs"
     89 );
     90 const reportsColorDepthFromVideoData =
     91    (AppConstants.platform == "macosx" || AppConstants.platform == "win");
     92 if (reportsColorDepthFromVideoData) {
     93  add_task(async function testHDRPlayTime() {
     94    // This task is different from the others because the HTMLMediaElement does
     95    // not expose a chrome property for video hdr play time. But we do capture
     96    // telemety for VIDEO_HDR_PLAY_TIME_MS. To ensure that this telemetry is
     97    // generated, this task follows the same structure as the other tasks, but
     98    // doesn't actually check the properties of the video player, other than to
     99    // confirm that video has played for at least some time.
    100    const video = document.createElement('video');
    101    if (AppConstants.platform == "macosx") {
    102      video.src = "TestPatternHDR.mp4"; // This is an HDR video with no audio.
    103    } else if (AppConstants.platform == "win") {
    104      video.src = "gizmo_av1_10bit_420.webm";
    105    }
    106    document.body.appendChild(video);
    107 
    108    info(`load the HDR video`);
    109    const videoChrome = SpecialPowers.wrap(video);
    110    await new Promise(r => video.onloadeddata = r);
    111 
    112    info(`start accumulating play time after media starts`);
    113    video.autoplay = true;
    114    await Promise.all([
    115      once(video, "playing"),
    116      once(video, "moztotalplaytimestarted"),
    117    ]);
    118    // Check that we have at least some video play time, because the
    119    // HDR play time telemetry is emitted by the same process.
    120    await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
    121    await cleanUpMediaAndCheckTelemetry(video, {hasVideo: true, hasAudio: false, hasVideoHDR: true});
    122  });
    123 }
    124 
    125 add_task(async function testVisiblePlayTime() {
    126  const video = document.createElement('video');
    127  video.src = "gizmo.mp4";
    128  document.body.appendChild(video);
    129 
    130  info(`all accumulated time should be zero`);
    131  const videoChrome = SpecialPowers.wrap(video);
    132  await new Promise(r => video.onloadeddata = r);
    133  assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
    134  assertValueEqualTo(videoChrome, "visiblePlayTime", 0);
    135  assertValueEqualTo(videoChrome, "invisiblePlayTime", 0);
    136 
    137  info(`start accumulating play time after media starts`);
    138  video.autoplay = true;
    139  await Promise.all([
    140    once(video, "playing"),
    141    once(video, "moztotalplaytimestarted"),
    142  ]);
    143  await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
    144  await assertValueConstantlyIncreases(videoChrome, "visiblePlayTime");
    145  assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
    146 
    147  info(`make video invisible`);
    148  video.style.display = "none";
    149  await once(video, "mozinvisibleplaytimestarted");
    150  await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
    151  await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
    152  assertValueKeptUnchanged(videoChrome, "visiblePlayTime");
    153  await cleanUpMediaAndCheckTelemetry(video);
    154 });
    155 
    156 add_task(async function testAudibleAudioPlayTime() {
    157  const audio = document.createElement('audio');
    158  audio.src = "tone2s-silence4s-tone2s.opus";
    159  audio.controls = true;
    160  audio.loop = true;
    161  document.body.appendChild(audio);
    162 
    163  info(`all accumulated time should be zero`);
    164  const audioChrome = SpecialPowers.wrap(audio);
    165  await new Promise(r => audio.onloadeddata = r);
    166  assertValueEqualTo(audioChrome, "totalVideoPlayTime", 0);
    167  assertValueEqualTo(audioChrome, "totalAudioPlayTime", 0);
    168  assertValueEqualTo(audioChrome, "mutedPlayTime", 0);
    169  assertValueEqualTo(audioChrome, "audiblePlayTime", 0);
    170 
    171  info(`start accumulating play time after media starts`);
    172  await Promise.all([
    173    audio.play(),
    174    once(audio, "moztotalplaytimestarted"),
    175  ]);
    176  await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
    177  await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
    178  assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
    179  assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
    180 
    181  info(`audio becomes inaudible for 4s`);
    182  await once(audio, "mozinaudibleaudioplaytimestarted");
    183 
    184  await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
    185  assertValueKeptUnchanged(audioChrome, "audiblePlayTime");
    186  assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
    187  assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
    188 
    189  info(`audio becomes audible after 4s`);
    190  await once(audio, "mozinaudibleaudioplaytimepaused");
    191 
    192  await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
    193  await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
    194  assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
    195  assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
    196 
    197  await cleanUpMediaAndCheckTelemetry(audio, {hasVideo: false});
    198 });
    199 
    200 add_task(async function testHiddenPlayTime() {
    201  const invisibleReasons = ["notInTree", "notInConnectedTree", "invisibleInDisplay"];
    202  for (let reason of invisibleReasons) {
    203    const video = document.createElement('video');
    204    video.src = "gizmo.mp4";
    205    video.loop = true;
    206    info(`invisible video due to '${reason}'`);
    207 
    208    if (reason == "notInConnectedTree") {
    209      let disconnected = document.createElement("div")
    210      disconnected.appendChild(video);
    211    } else if (reason == "invisibleInDisplay") {
    212      document.body.appendChild(video);
    213      video.style.display = "none";
    214    } else if (reason == "notInTree") {
    215      // video is already created in the `notInTree` situation.
    216    } else {
    217      ok(false, "undefined reason");
    218    }
    219 
    220    info(`start invisible video should start accumulating timers`);
    221    const videoChrome = SpecialPowers.wrap(video);
    222    let rv = await Promise.all([
    223      onceWithTrueReturn(video, "mozinvisibleplaytimestarted"),
    224      video.play().then(_ => true, _ => false),
    225    ]);
    226    ok(returnTrueWhenAllValuesAreTrue(rv), "video started playing");
    227    await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
    228 
    229    info(`should not accumulate time for paused video`);
    230    video.pause();
    231    await once(video, "mozinvisibleplaytimepaused");
    232    assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
    233 
    234    info(`should start accumulating time again`);
    235    rv = await Promise.all([
    236      onceWithTrueReturn(video, "mozinvisibleplaytimestarted"),
    237      video.play().then(_ => true, _ => false),
    238    ]);
    239    ok(returnTrueWhenAllValuesAreTrue(rv), "video started again");
    240    await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
    241 
    242    info(`make video visible should stop accumulating invisible related time`);
    243    if (reason == "notInTree" || reason == "notInConnectedTree") {
    244      document.body.appendChild(video);
    245    } else if (reason == "invisibleInDisplay") {
    246      video.style.display = "block";
    247    } else {
    248      ok(false, "undefined reason");
    249    }
    250    await once(video, "mozinvisibleplaytimepaused");
    251    assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
    252    await cleanUpMediaAndCheckTelemetry(video);
    253  }
    254 });
    255 
    256 add_task(async function testAudioProbesWithoutAudio() {
    257  const video = document.createElement('video');
    258  video.src = "gizmo-noaudio.mp4";
    259  video.loop = true;
    260  document.body.appendChild(video);
    261 
    262  info(`all accumulated time should be zero`);
    263  const videoChrome = SpecialPowers.wrap(video);
    264  await new Promise(r => video.onloadeddata = r);
    265  assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
    266  assertValueEqualTo(videoChrome, "totalAudioPlayTime", 0);
    267  assertValueEqualTo(videoChrome, "mutedPlayTime", 0);
    268  assertValueEqualTo(videoChrome, "audiblePlayTime", 0);
    269 
    270  info(`start accumulating play time after media starts`);
    271  await Promise.all([
    272    video.play(),
    273    once(video, "moztotalplaytimestarted"),
    274  ]);
    275 
    276  async function checkInvariants() {
    277    await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
    278    assertValueKeptUnchanged(videoChrome, "audiblePlayTime");
    279    assertValueKeptUnchanged(videoChrome, "mutedPlayTime");
    280    assertValueKeptUnchanged(videoChrome, "totalAudioPlayTime");
    281  }
    282 
    283  checkInvariants();
    284 
    285  video.muted = true;
    286 
    287  checkInvariants();
    288 
    289  video.currentTime = 0.0;
    290  await once(video, "seeked");
    291 
    292  checkInvariants();
    293 
    294  video.muted = false;
    295 
    296  checkInvariants();
    297 
    298  video.volume = 0.0;
    299 
    300  checkInvariants();
    301 
    302  video.volume = 1.0;
    303 
    304  checkInvariants();
    305 
    306  video.muted = true;
    307 
    308  checkInvariants();
    309 
    310  video.currentTime = 0.0;
    311 
    312  checkInvariants();
    313 
    314  await cleanUpMediaAndCheckTelemetry(video, {hasAudio: false});
    315 });
    316 
    317 add_task(async function testMutedAudioPlayTime() {
    318  const audio = document.createElement('audio');
    319  audio.src = "gizmo.mp4";
    320  audio.controls = true;
    321  audio.loop = true;
    322  document.body.appendChild(audio);
    323 
    324  info(`all accumulated time should be zero`);
    325  const audioChrome = SpecialPowers.wrap(audio);
    326  await new Promise(r => audio.onloadeddata = r);
    327  assertValueEqualTo(audioChrome, "totalVideoPlayTime", 0);
    328  assertValueEqualTo(audioChrome, "totalAudioPlayTime", 0);
    329  assertValueEqualTo(audioChrome, "mutedPlayTime", 0);
    330  assertValueEqualTo(audioChrome, "audiblePlayTime", 0);
    331 
    332  info(`start accumulating play time after media starts`);
    333  await Promise.all([
    334    audio.play(),
    335    once(audio, "moztotalplaytimestarted"),
    336  ]);
    337  await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
    338  await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
    339  assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
    340  assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
    341 
    342  audio.muted = true;
    343  await once(audio, "mozmutedaudioplaytimestarted");
    344 
    345  await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
    346  await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
    347  await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
    348  assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
    349 
    350  audio.currentTime = 0.0;
    351  await once(audio, "seeked");
    352 
    353  await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
    354  await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
    355  await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
    356  assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
    357 
    358  audio.muted = false;
    359  await once(audio, "mozmutedeaudioplaytimepaused");
    360 
    361  await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
    362  await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
    363  assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
    364  assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
    365 
    366  audio.volume = 0.0;
    367  await once(audio, "mozmutedaudioplaytimestarted");
    368 
    369  await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
    370  await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
    371  await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
    372  assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
    373 
    374  audio.volume = 1.0;
    375  await once(audio, "mozmutedeaudioplaytimepaused");
    376 
    377  await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
    378  await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
    379  assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
    380  assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
    381 
    382  audio.muted = true;
    383  await once(audio, "mozmutedaudioplaytimestarted");
    384 
    385  await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
    386  await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
    387  await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
    388  assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
    389 
    390  audio.currentTime = 0.0;
    391 
    392  await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
    393  await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
    394  await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
    395  assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
    396 
    397  // The media has a video track, but it's being played back in an
    398  // HTMLAudioElement, without video frame location.
    399  await cleanUpMediaAndCheckTelemetry(audio, {hasVideo: false});
    400 });
    401 
    402 // Note that video suspended time is not always align with the invisible play
    403 // time even if `media.suspend-background-video.delay-ms` is `0`, because not all
    404 // invisible videos would be suspended under current strategy.
    405 add_task(async function testDecodeSuspendedTime() {
    406  const video = document.createElement('video');
    407  video.src = "gizmo.mp4";
    408  video.loop = true;
    409  document.body.appendChild(video);
    410 
    411  info(`start video should start accumulating timers`);
    412  const videoChrome = SpecialPowers.wrap(video);
    413  let rv = await Promise.all([
    414    onceWithTrueReturn(video, "moztotalplaytimestarted"),
    415    video.play().then(_ => true, _ => false),
    416  ]);
    417  ok(returnTrueWhenAllValuesAreTrue(rv), "video started playing");
    418  await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
    419  assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
    420 
    421  info(`make it invisible and force to suspend decoding`);
    422  video.setVisible(false);
    423  await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
    424  await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
    425 
    426  info(`make it visible and resume decoding`);
    427  video.setVisible(true);
    428  await once(video, "mozinvisibleplaytimepaused");
    429  await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
    430  assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
    431  await cleanUpMediaAndCheckTelemetry(video);
    432 });
    433 
    434 add_task(async function reuseSameElementForPlayback() {
    435  const video = document.createElement('video');
    436  video.src = "gizmo.mp4";
    437  document.body.appendChild(video);
    438 
    439  info(`start accumulating play time after media starts`);
    440  const videoChrome = SpecialPowers.wrap(video);
    441  let rv = await Promise.all([
    442    onceWithTrueReturn(video, "moztotalplaytimestarted"),
    443    video.play().then(_ => true, _ => false),
    444  ]);
    445  ok(returnTrueWhenAllValuesAreTrue(rv), "video started again");
    446  await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
    447 
    448  info(`reset its src and all accumulated value should be reset after then`);
    449  // After setting its src to nothing, that would trigger a failed load and set
    450  // the error. If the following step tries to set the new resource and `play()`
    451  // , then they should be done after receving the `error` from that failed load
    452  // first.
    453  await Promise.all([
    454    once(video, "error"),
    455    cleanUpMediaAndCheckTelemetry(video),
    456  ]);
    457  // video doesn't have a decoder, so the return value would be -1 (error).
    458  assertValueEqualTo(videoChrome, "totalVideoPlayTime", -1);
    459  assertValueEqualTo(videoChrome, "invisiblePlayTime", -1);
    460 
    461  info(`resue same element, make it visible and start playback again`);
    462  video.src = "gizmo.mp4";
    463  rv = await Promise.all([
    464    onceWithTrueReturn(video, "moztotalplaytimestarted"),
    465    video.play().then(_ => true, _ => false),
    466  ]);
    467  ok(returnTrueWhenAllValuesAreTrue(rv), "video started");
    468  await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
    469  await cleanUpMediaAndCheckTelemetry(video);
    470 });
    471 
    472 add_task(async function testNoReportedTelemetryResult() {
    473  info(`No result for empty video`);
    474  const video = document.createElement('video');
    475  assertAllProbeRelatedAttributesKeptUnchanged(video);
    476  await assertNoReportedTelemetryResult(video);
    477 
    478  info(`No result for video which hasn't started playing`);
    479  video.src = "gizmo.mp4";
    480  document.body.appendChild(video);
    481  ok(await once(video, "loadeddata").then(_ => true), "video loaded data");
    482  assertAllProbeRelatedAttributesKeptUnchanged(video);
    483  await assertNoReportedTelemetryResult(video);
    484 
    485  info(`No result for video with error`);
    486  video.src = "filedoesnotexist.mp4";
    487  ok(await video.play().then(_ => false, _ => true), "video failed to play");
    488  ok(video.error != undefined, "video got error");
    489  assertAllProbeRelatedAttributesKeptUnchanged(video);
    490  await assertNoReportedTelemetryResult(video);
    491 });
    492 
    493 /**
    494 * Following are helper functions
    495 */
    496 async function cleanUpMediaAndCheckTelemetry(media, { reportExpected = true, hasVideo = true, hasAudio = true, hasVideoHDR = false } = {}) {
    497  media.src = "";
    498  await checkReportedTelemetry(media, reportExpected, hasVideo, hasAudio, hasVideoHDR);
    499 }
    500 
    501 async function assertNoReportedTelemetryResult(media) {
    502  await checkReportedTelemetry(media, false, true, true);
    503 }
    504 
    505 async function checkReportedTelemetry(media, reportExpected, hasVideo, hasAudio, hasVideoHDR) {
    506  const reportResultPromise = once(media, "mozreportedtelemetry");
    507  info(`check telemetry result, reportExpected=${reportExpected}`);
    508  if (reportExpected) {
    509    await reportResultPromise;
    510  }
    511  for (const name of videoHistNames) {
    512    try {
    513      const hist = SpecialPowers.Services.telemetry.getHistogramById(name);
    514      /**
    515       * Histogram's snapshot looks like that
    516       * {
    517       *    "bucket_count": X,
    518       *    "histogram_type": Y,
    519       *    "sum": Z,
    520       *    "range": [min, max],
    521       *    "values": { "value1" : "num1", "value2" : "num2", ...}
    522       * }
    523       */
    524      const entriesNums = Object.entries(hist.snapshot().values).length;
    525      if (reportExpected && hasVideo) {
    526        ok(entriesNums > 0, `Reported result for ${name}`);
    527      } else {
    528        ok(entriesNums == 0, `Reported nothing for ${name}`);
    529      }
    530      hist.clear();
    531    } catch (e) {
    532      ok(false , `histogram '${name}' doesn't exist`);
    533    }
    534  }
    535  // videoHDRHistNames are checked for total time, not for number of samples.
    536  for (const name of videoHDRHistNames) {
    537    try {
    538      const hist = SpecialPowers.Services.telemetry.getHistogramById(name);
    539      const totalTimeMS = hist.snapshot().sum;
    540      if (reportExpected && hasVideoHDR) {
    541        ok(totalTimeMS > 0, `Reported some time for ${name}`);
    542      } else {
    543        ok(totalTimeMS == 0, `Reported no time for ${name}`);
    544      }
    545      hist.clear();
    546    } catch (e) {
    547      ok(false , `histogram '${name}' doesn't exist`);
    548    }
    549  }
    550  for (const name of videoKeyedHistNames) {
    551    try {
    552      const hist = SpecialPowers.Services.telemetry.getKeyedHistogramById(name);
    553      /**
    554       * Keyed Histogram's snapshot looks like that
    555       * {
    556       *    "Key1" : {
    557       *      "bucket_count": X,
    558       *      "histogram_type": Y,
    559       *      "sum": Z,
    560       *      "range": [min, max],
    561       *      "values": { "value1" : "num1", "value2" : "num2", ...}
    562       *    },
    563       *    "Key2" : {...},
    564       * }
    565       */
    566      const items = Object.entries(hist.snapshot());
    567      if (items.length) {
    568        for (const [key, value] of items) {
    569          const entriesNums = Object.entries(value.values).length;
    570          ok(reportExpected && entriesNums > 0, `Reported ${key} for ${name}`);
    571        }
    572      } else if (reportExpected) {
    573        ok(!hasVideo, `No video telemetry reported but no video track in the media`);
    574      } else {
    575        ok(true, `No video telemetry expected, none reported`);
    576      }
    577      // Avoid to pollute next test task.
    578      hist.clear();
    579    } catch (e) {
    580      ok(false , `keyed histogram '${name}' doesn't exist`);
    581    }
    582  }
    583 
    584  // In any case, the combined probe MEDIA_PLAY_TIME_MS should be reported, if
    585  // expected
    586  {
    587    const hist =
    588      SpecialPowers.Services.telemetry.getKeyedHistogramById("MEDIA_PLAY_TIME_MS");
    589    const items = Object.entries(hist.snapshot());
    590    if (items.length) {
    591      for (const item of items) {
    592        ok(item[0].includes("V") != -1 || !hasVideo, "Video time is reported if video was present");
    593      }
    594      hist.clear();
    595    } else {
    596      ok(!reportExpected, "MEDIA_PLAY_TIME_MS should always be reported if a report is expected");
    597    }
    598  }
    599 
    600  for (const name of audioKeyedHistNames) {
    601    try {
    602      const hist = SpecialPowers.Services.telemetry.getKeyedHistogramById(name);
    603      const items = Object.entries(hist.snapshot());
    604      if (items.length) {
    605        for (const [key, value] of items) {
    606          const entriesNums = Object.entries(value.values).length;
    607          ok(reportExpected && entriesNums > 0, `Reported ${key} for ${name}`);
    608        }
    609      } else {
    610        ok(!reportExpected || !hasAudio, `No audio telemetry expected, none reported`);
    611      }
    612      // Avoid to pollute next test task.
    613      hist.clear();
    614    } catch (e) {
    615      ok(false , `keyed histogram '${name}' doesn't exist`);
    616    }
    617  }
    618 }
    619 
    620 function once(target, name) {
    621  return new Promise(r => target.addEventListener(name, r, { once: true }));
    622 }
    623 
    624 function onceWithTrueReturn(target, name) {
    625  return once(target, name).then(_ => true);
    626 }
    627 
    628 function returnTrueWhenAllValuesAreTrue(arr) {
    629  for (let val of arr) {
    630    if (!val) {
    631      return false;
    632    }
    633  }
    634  return true;
    635 }
    636 
    637 // Block the main thread for a number of milliseconds
    638 function blockMainThread(durationMS) {
    639  const start = Date.now();
    640  while (Date.now() - start < durationMS) { /* spin */ }
    641 }
    642 
    643 // Allows comparing two values from the system clocks that are not gathered
    644 // atomically. Allow up to 1ms of fuzzing when lhs and rhs are seconds.
    645 function timeFuzzyEquals(lhs, rhs, str) {
    646  ok(Math.abs(lhs - rhs) < 1e-3, str);
    647 }
    648 
    649 function assertAttributeDefined(mediaChrome, checkType) {
    650  ok(mediaChrome[checkType] != undefined, `${checkType} exists`);
    651 }
    652 
    653 function assertValueEqualTo(mediaChrome, checkType, expectedValue) {
    654  assertAttributeDefined(mediaChrome, checkType);
    655  is(mediaChrome[checkType], expectedValue, `${checkType} equals to ${expectedValue}`);
    656 }
    657 
    658 async function assertValueConstantlyIncreases(mediaChrome, checkType) {
    659  assertAttributeDefined(mediaChrome, checkType);
    660  const valueSnapshot = mediaChrome[checkType];
    661  // 30ms is long enough to have a low-resolution system clock tick, but short
    662  // enough to not slow the test down.
    663  blockMainThread(30);
    664  const current = mediaChrome[checkType];
    665  ok(current > valueSnapshot, `${checkType} keeps increasing (${current} > ${valueSnapshot})`);
    666 }
    667 
    668 function assertValueKeptUnchanged(mediaChrome, checkType) {
    669  assertAttributeDefined(mediaChrome, checkType);
    670  const valueSnapshot = mediaChrome[checkType];
    671  // 30ms is long enough to have a low-resolution system clock tick, but short
    672  // enough to not slow the test down.
    673  blockMainThread(30);
    674  const newValue = mediaChrome[checkType];
    675  timeFuzzyEquals(newValue, valueSnapshot, `${checkType} keeps unchanged (${newValue} vs. ${valueSnapshot})`);
    676 }
    677 
    678 function assertAllProbeRelatedAttributesKeptUnchanged(video) {
    679  const videoChrome = SpecialPowers.wrap(video);
    680  assertValueKeptUnchanged(videoChrome, "totalVideoPlayTime");
    681  assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
    682 }
    683 
    684 </script>
    685 </head>
    686 <body>
    687 </body>
    688 </html>