Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!DOCTYPE HTML>
<html>
<head>
<title>Test Video Play Time Related Permanent Telemetry Probes</title>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="application/javascript">
/**
* This test is used to ensure that we accumulate time for video playback
* correctly, and the results would be used in Telemetry probes.
* Currently this test covers following probes
* - VIDEO_PLAY_TIME_MS
* - VIDEO_HDR_PLAY_TIME_MS
* - VIDEO_HIDDEN_PLAY_TIME_MS
* - VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE
* - VIDEO_VISIBLE_PLAY_TIME_MS
* - MEDIA_PLAY_TIME_MS
* - MUTED_PLAY_TIME_PERCENT
* - AUDIBLE_PLAY_TIME_PERCENT
*/
const videoHistNames = [
"VIDEO_PLAY_TIME_MS",
"VIDEO_HIDDEN_PLAY_TIME_MS"
];
const videoHDRHistNames = [
"VIDEO_HDR_PLAY_TIME_MS"
];
const videoKeyedHistNames = [
"VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE",
"VIDEO_VISIBLE_PLAY_TIME_MS"
];
const audioKeyedHistNames = [
"MUTED_PLAY_TIME_PERCENT",
"AUDIBLE_PLAY_TIME_PERCENT"
];
add_task(async function setTestPref() {
await SpecialPowers.pushPrefEnv({
set: [["media.testing-only-events", true],
["media.test.video-suspend", true],
["media.suspend-background-video.enabled", true],
["media.suspend-background-video.delay-ms", 0],
["dom.media.silence_duration_for_audibility", 0.1]
]});
});
add_task(async function testTotalPlayTime() {
const video = document.createElement('video');
video.src = "gizmo.mp4";
document.body.appendChild(video);
info(`all accumulated time should be zero`);
const videoChrome = SpecialPowers.wrap(video);
await new Promise(r => video.onloadeddata = r);
assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
assertValueEqualTo(videoChrome, "invisiblePlayTime", 0);
info(`start accumulating play time after media starts`);
video.autoplay = true;
await Promise.all([
once(video, "playing"),
once(video, "moztotalplaytimestarted"),
]);
await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
info(`should not accumulate time for paused video`);
video.pause();
await once(video, "moztotalplaytimepaused");
assertValueKeptUnchanged(videoChrome, "totalVideoPlayTime");
assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
info(`should start accumulating time again`);
let rv = await Promise.all([
onceWithTrueReturn(video, "moztotalplaytimestarted"),
video.play().then(_ => true, _ => false),
]);
ok(returnTrueWhenAllValuesAreTrue(rv), "video started again");
await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
await cleanUpMediaAndCheckTelemetry(video);
});
// The testHDRPlayTime task will only pass on platforms that accurately report
// color depth in their VideoInfo structures. Presently, that is only true for
// macOS.
const {AppConstants} = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const reportsColorDepthFromVideoData = (AppConstants.platform == "macosx");
if (reportsColorDepthFromVideoData) {
add_task(async function testHDRPlayTime() {
// This task is different from the others because the HTMLMediaElement does
// not expose a chrome property for video hdr play time. But we do capture
// telemety for VIDEO_HDR_PLAY_TIME_MS. To ensure that this telemetry is
// generated, this task follows the same structure as the other tasks, but
// doesn't actually check the properties of the video player, other than to
// confirm that video has played for at least some time.
const video = document.createElement('video');
video.src = "TestPatternHDR.mp4"; // This is an HDR video with no audio.
document.body.appendChild(video);
info(`load the HDR video`);
const videoChrome = SpecialPowers.wrap(video);
await new Promise(r => video.onloadeddata = r);
info(`start accumulating play time after media starts`);
video.autoplay = true;
await Promise.all([
once(video, "playing"),
once(video, "moztotalplaytimestarted"),
]);
// Check that we have at least some video play time, because the
// HDR play time telemetry is emitted by the same process.
await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
await cleanUpMediaAndCheckTelemetry(video, {hasVideo: true, hasAudio: false, hasVideoHDR: true});
});
}
add_task(async function testVisiblePlayTime() {
const video = document.createElement('video');
video.src = "gizmo.mp4";
document.body.appendChild(video);
info(`all accumulated time should be zero`);
const videoChrome = SpecialPowers.wrap(video);
await new Promise(r => video.onloadeddata = r);
assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
assertValueEqualTo(videoChrome, "visiblePlayTime", 0);
assertValueEqualTo(videoChrome, "invisiblePlayTime", 0);
info(`start accumulating play time after media starts`);
video.autoplay = true;
await Promise.all([
once(video, "playing"),
once(video, "moztotalplaytimestarted"),
]);
await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
await assertValueConstantlyIncreases(videoChrome, "visiblePlayTime");
assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
info(`make video invisible`);
video.style.display = "none";
await once(video, "mozinvisibleplaytimestarted");
await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
assertValueKeptUnchanged(videoChrome, "visiblePlayTime");
await cleanUpMediaAndCheckTelemetry(video);
});
add_task(async function testAudibleAudioPlayTime() {
const audio = document.createElement('audio');
audio.src = "tone2s-silence4s-tone2s.opus";
audio.controls = true;
audio.loop = true;
document.body.appendChild(audio);
info(`all accumulated time should be zero`);
const audioChrome = SpecialPowers.wrap(audio);
await new Promise(r => audio.onloadeddata = r);
assertValueEqualTo(audioChrome, "totalVideoPlayTime", 0);
assertValueEqualTo(audioChrome, "totalAudioPlayTime", 0);
assertValueEqualTo(audioChrome, "mutedPlayTime", 0);
assertValueEqualTo(audioChrome, "audiblePlayTime", 0);
info(`start accumulating play time after media starts`);
await Promise.all([
audio.play(),
once(audio, "moztotalplaytimestarted"),
]);
await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
info(`audio becomes inaudible for 4s`);
await once(audio, "mozinaudibleaudioplaytimestarted");
await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
assertValueKeptUnchanged(audioChrome, "audiblePlayTime");
assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
info(`audio becomes audible after 4s`);
await once(audio, "mozinaudibleaudioplaytimepaused");
await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
await cleanUpMediaAndCheckTelemetry(audio, {hasVideo: false});
});
add_task(async function testHiddenPlayTime() {
const invisibleReasons = ["notInTree", "notInConnectedTree", "invisibleInDisplay"];
for (let reason of invisibleReasons) {
const video = document.createElement('video');
video.src = "gizmo.mp4";
video.loop = true;
info(`invisible video due to '${reason}'`);
if (reason == "notInConnectedTree") {
let disconnected = document.createElement("div")
disconnected.appendChild(video);
} else if (reason == "invisibleInDisplay") {
document.body.appendChild(video);
video.style.display = "none";
} else if (reason == "notInTree") {
// video is already created in the `notInTree` situation.
} else {
ok(false, "undefined reason");
}
info(`start invisible video should start accumulating timers`);
const videoChrome = SpecialPowers.wrap(video);
let rv = await Promise.all([
onceWithTrueReturn(video, "mozinvisibleplaytimestarted"),
video.play().then(_ => true, _ => false),
]);
ok(returnTrueWhenAllValuesAreTrue(rv), "video started playing");
await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
info(`should not accumulate time for paused video`);
video.pause();
await once(video, "mozinvisibleplaytimepaused");
assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
info(`should start accumulating time again`);
rv = await Promise.all([
onceWithTrueReturn(video, "mozinvisibleplaytimestarted"),
video.play().then(_ => true, _ => false),
]);
ok(returnTrueWhenAllValuesAreTrue(rv), "video started again");
await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
info(`make video visible should stop accumulating invisible related time`);
if (reason == "notInTree" || reason == "notInConnectedTree") {
document.body.appendChild(video);
} else if (reason == "invisibleInDisplay") {
video.style.display = "block";
} else {
ok(false, "undefined reason");
}
await once(video, "mozinvisibleplaytimepaused");
assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
await cleanUpMediaAndCheckTelemetry(video);
}
});
add_task(async function testAudioProbesWithoutAudio() {
const video = document.createElement('video');
video.src = "gizmo-noaudio.mp4";
video.loop = true;
document.body.appendChild(video);
info(`all accumulated time should be zero`);
const videoChrome = SpecialPowers.wrap(video);
await new Promise(r => video.onloadeddata = r);
assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
assertValueEqualTo(videoChrome, "totalAudioPlayTime", 0);
assertValueEqualTo(videoChrome, "mutedPlayTime", 0);
assertValueEqualTo(videoChrome, "audiblePlayTime", 0);
info(`start accumulating play time after media starts`);
await Promise.all([
video.play(),
once(video, "moztotalplaytimestarted"),
]);
async function checkInvariants() {
await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
assertValueKeptUnchanged(videoChrome, "audiblePlayTime");
assertValueKeptUnchanged(videoChrome, "mutedPlayTime");
assertValueKeptUnchanged(videoChrome, "totalAudioPlayTime");
}
checkInvariants();
video.muted = true;
checkInvariants();
video.currentTime = 0.0;
await once(video, "seeked");
checkInvariants();
video.muted = false;
checkInvariants();
video.volume = 0.0;
checkInvariants();
video.volume = 1.0;
checkInvariants();
video.muted = true;
checkInvariants();
video.currentTime = 0.0;
checkInvariants();
await cleanUpMediaAndCheckTelemetry(video, {hasAudio: false});
});
add_task(async function testMutedAudioPlayTime() {
const audio = document.createElement('audio');
audio.src = "gizmo.mp4";
audio.controls = true;
audio.loop = true;
document.body.appendChild(audio);
info(`all accumulated time should be zero`);
const audioChrome = SpecialPowers.wrap(audio);
await new Promise(r => audio.onloadeddata = r);
assertValueEqualTo(audioChrome, "totalVideoPlayTime", 0);
assertValueEqualTo(audioChrome, "totalAudioPlayTime", 0);
assertValueEqualTo(audioChrome, "mutedPlayTime", 0);
assertValueEqualTo(audioChrome, "audiblePlayTime", 0);
info(`start accumulating play time after media starts`);
await Promise.all([
audio.play(),
once(audio, "moztotalplaytimestarted"),
]);
await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
audio.muted = true;
await once(audio, "mozmutedaudioplaytimestarted");
await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
audio.currentTime = 0.0;
await once(audio, "seeked");
await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
audio.muted = false;
await once(audio, "mozmutedeaudioplaytimepaused");
await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
audio.volume = 0.0;
await once(audio, "mozmutedaudioplaytimestarted");
await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
audio.volume = 1.0;
await once(audio, "mozmutedeaudioplaytimepaused");
await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
audio.muted = true;
await once(audio, "mozmutedaudioplaytimestarted");
await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
audio.currentTime = 0.0;
await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
// The media has a video track, but it's being played back in an
// HTMLAudioElement, without video frame location.
await cleanUpMediaAndCheckTelemetry(audio, {hasVideo: false});
});
// Note that video suspended time is not always align with the invisible play
// time even if `media.suspend-background-video.delay-ms` is `0`, because not all
// invisible videos would be suspended under current strategy.
add_task(async function testDecodeSuspendedTime() {
const video = document.createElement('video');
video.src = "gizmo.mp4";
video.loop = true;
document.body.appendChild(video);
info(`start video should start accumulating timers`);
const videoChrome = SpecialPowers.wrap(video);
let rv = await Promise.all([
onceWithTrueReturn(video, "moztotalplaytimestarted"),
video.play().then(_ => true, _ => false),
]);
ok(returnTrueWhenAllValuesAreTrue(rv), "video started playing");
await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
info(`make it invisible and force to suspend decoding`);
video.setVisible(false);
await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
info(`make it visible and resume decoding`);
video.setVisible(true);
await once(video, "mozinvisibleplaytimepaused");
await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
await cleanUpMediaAndCheckTelemetry(video);
});
add_task(async function reuseSameElementForPlayback() {
const video = document.createElement('video');
video.src = "gizmo.mp4";
document.body.appendChild(video);
info(`start accumulating play time after media starts`);
const videoChrome = SpecialPowers.wrap(video);
let rv = await Promise.all([
onceWithTrueReturn(video, "moztotalplaytimestarted"),
video.play().then(_ => true, _ => false),
]);
ok(returnTrueWhenAllValuesAreTrue(rv), "video started again");
await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
info(`reset its src and all accumulated value should be reset after then`);
// After setting its src to nothing, that would trigger a failed load and set
// the error. If the following step tries to set the new resource and `play()`
// , then they should be done after receving the `error` from that failed load
// first.
await Promise.all([
once(video, "error"),
cleanUpMediaAndCheckTelemetry(video),
]);
// video doesn't have a decoder, so the return value would be -1 (error).
assertValueEqualTo(videoChrome, "totalVideoPlayTime", -1);
assertValueEqualTo(videoChrome, "invisiblePlayTime", -1);
info(`resue same element, make it visible and start playback again`);
video.src = "gizmo.mp4";
rv = await Promise.all([
onceWithTrueReturn(video, "moztotalplaytimestarted"),
video.play().then(_ => true, _ => false),
]);
ok(returnTrueWhenAllValuesAreTrue(rv), "video started");
await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
await cleanUpMediaAndCheckTelemetry(video);
});
add_task(async function testNoReportedTelemetryResult() {
info(`No result for empty video`);
const video = document.createElement('video');
assertAllProbeRelatedAttributesKeptUnchanged(video);
await assertNoReportedTelemetryResult(video);
info(`No result for video which hasn't started playing`);
video.src = "gizmo.mp4";
document.body.appendChild(video);
ok(await once(video, "loadeddata").then(_ => true), "video loaded data");
assertAllProbeRelatedAttributesKeptUnchanged(video);
await assertNoReportedTelemetryResult(video);
info(`No result for video with error`);
video.src = "filedoesnotexist.mp4";
ok(await video.play().then(_ => false, _ => true), "video failed to play");
ok(video.error != undefined, "video got error");
assertAllProbeRelatedAttributesKeptUnchanged(video);
await assertNoReportedTelemetryResult(video);
});
/**
* Following are helper functions
*/
async function cleanUpMediaAndCheckTelemetry(media, { reportExpected = true, hasVideo = true, hasAudio = true, hasVideoHDR = false } = {}) {
media.src = "";
await checkReportedTelemetry(media, reportExpected, hasVideo, hasAudio, hasVideoHDR);
}
async function assertNoReportedTelemetryResult(media) {
await checkReportedTelemetry(media, false, true, true);
}
async function checkReportedTelemetry(media, reportExpected, hasVideo, hasAudio, hasVideoHDR) {
const reportResultPromise = once(media, "mozreportedtelemetry");
info(`check telemetry result, reportExpected=${reportExpected}`);
if (reportExpected) {
await reportResultPromise;
}
for (const name of videoHistNames) {
try {
const hist = SpecialPowers.Services.telemetry.getHistogramById(name);
/**
* Histogram's snapshot looks like that
* {
* "bucket_count": X,
* "histogram_type": Y,
* "sum": Z,
* "range": [min, max],
* "values": { "value1" : "num1", "value2" : "num2", ...}
* }
*/
const entriesNums = Object.entries(hist.snapshot().values).length;
if (reportExpected && hasVideo) {
ok(entriesNums > 0, `Reported result for ${name}`);
} else {
ok(entriesNums == 0, `Reported nothing for ${name}`);
}
hist.clear();
} catch (e) {
ok(false , `histogram '${name}' doesn't exist`);
}
}
// videoHDRHistNames are checked for total time, not for number of samples.
for (const name of videoHDRHistNames) {
try {
const hist = SpecialPowers.Services.telemetry.getHistogramById(name);
const totalTimeMS = hist.snapshot().sum;
if (reportExpected && hasVideoHDR) {
ok(totalTimeMS > 0, `Reported some time for ${name}`);
} else {
ok(totalTimeMS == 0, `Reported no time for ${name}`);
}
hist.clear();
} catch (e) {
ok(false , `histogram '${name}' doesn't exist`);
}
}
for (const name of videoKeyedHistNames) {
try {
const hist = SpecialPowers.Services.telemetry.getKeyedHistogramById(name);
/**
* Keyed Histogram's snapshot looks like that
* {
* "Key1" : {
* "bucket_count": X,
* "histogram_type": Y,
* "sum": Z,
* "range": [min, max],
* "values": { "value1" : "num1", "value2" : "num2", ...}
* },
* "Key2" : {...},
* }
*/
const items = Object.entries(hist.snapshot());
if (items.length) {
for (const [key, value] of items) {
const entriesNums = Object.entries(value.values).length;
ok(reportExpected && entriesNums > 0, `Reported ${key} for ${name}`);
}
} else if (reportExpected) {
ok(!hasVideo, `No video telemetry reported but no video track in the media`);
} else {
ok(true, `No video telemetry expected, none reported`);
}
// Avoid to pollute next test task.
hist.clear();
} catch (e) {
ok(false , `keyed histogram '${name}' doesn't exist`);
}
}
// In any case, the combined probe MEDIA_PLAY_TIME_MS should be reported, if
// expected
{
const hist =
SpecialPowers.Services.telemetry.getKeyedHistogramById("MEDIA_PLAY_TIME_MS");
const items = Object.entries(hist.snapshot());
if (items.length) {
for (const item of items) {
ok(item[0].includes("V") != -1 || !hasVideo, "Video time is reported if video was present");
}
hist.clear();
} else {
ok(!reportExpected, "MEDIA_PLAY_TIME_MS should always be reported if a report is expected");
}
}
for (const name of audioKeyedHistNames) {
try {
const hist = SpecialPowers.Services.telemetry.getKeyedHistogramById(name);
const items = Object.entries(hist.snapshot());
if (items.length) {
for (const [key, value] of items) {
const entriesNums = Object.entries(value.values).length;
ok(reportExpected && entriesNums > 0, `Reported ${key} for ${name}`);
}
} else {
ok(!reportExpected || !hasAudio, `No audio telemetry expected, none reported`);
}
// Avoid to pollute next test task.
hist.clear();
} catch (e) {
ok(false , `keyed histogram '${name}' doesn't exist`);
}
}
}
function once(target, name) {
return new Promise(r => target.addEventListener(name, r, { once: true }));
}
function onceWithTrueReturn(target, name) {
return once(target, name).then(_ => true);
}
function returnTrueWhenAllValuesAreTrue(arr) {
for (let val of arr) {
if (!val) {
return false;
}
}
return true;
}
// Block the main thread for a number of milliseconds
function blockMainThread(durationMS) {
const start = Date.now();
while (Date.now() - start < durationMS) { /* spin */ }
}
// Allows comparing two values from the system clocks that are not gathered
// atomically. Allow up to 1ms of fuzzing when lhs and rhs are seconds.
function timeFuzzyEquals(lhs, rhs, str) {
ok(Math.abs(lhs - rhs) < 1e-3, str);
}
function assertAttributeDefined(mediaChrome, checkType) {
ok(mediaChrome[checkType] != undefined, `${checkType} exists`);
}
function assertValueEqualTo(mediaChrome, checkType, expectedValue) {
assertAttributeDefined(mediaChrome, checkType);
is(mediaChrome[checkType], expectedValue, `${checkType} equals to ${expectedValue}`);
}
async function assertValueConstantlyIncreases(mediaChrome, checkType) {
assertAttributeDefined(mediaChrome, checkType);
const valueSnapshot = mediaChrome[checkType];
// 30ms is long enough to have a low-resolution system clock tick, but short
// enough to not slow the test down.
blockMainThread(30);
const current = mediaChrome[checkType];
ok(current > valueSnapshot, `${checkType} keeps increasing (${current} > ${valueSnapshot})`);
}
function assertValueKeptUnchanged(mediaChrome, checkType) {
assertAttributeDefined(mediaChrome, checkType);
const valueSnapshot = mediaChrome[checkType];
// 30ms is long enough to have a low-resolution system clock tick, but short
// enough to not slow the test down.
blockMainThread(30);
const newValue = mediaChrome[checkType];
timeFuzzyEquals(newValue, valueSnapshot, `${checkType} keeps unchanged (${newValue} vs. ${valueSnapshot})`);
}
function assertAllProbeRelatedAttributesKeptUnchanged(video) {
const videoChrome = SpecialPowers.wrap(video);
assertValueKeptUnchanged(videoChrome, "totalVideoPlayTime");
assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
}
</script>
</head>
<body>
</body>
</html>