summaryrefslogtreecommitdiffstats
path: root/dom/media/test/browser
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/media/test/browser/browser.ini28
-rw-r--r--dom/media/test/browser/browser_encrypted_play_time_telemetry.js266
-rw-r--r--dom/media/test/browser/browser_tab_visibility_and_play_time.js216
-rw-r--r--dom/media/test/browser/browser_telemetry_video_hardware_decoding_support.js106
-rw-r--r--dom/media/test/browser/file_empty_page.html8
-rw-r--r--dom/media/test/browser/file_media.html10
-rw-r--r--dom/media/test/browser/wmfme/browser.ini11
-rw-r--r--dom/media/test/browser/wmfme/browser_wmfme_crash.js52
-rw-r--r--dom/media/test/browser/wmfme/browser_wmfme_max_crashes.js69
-rw-r--r--dom/media/test/browser/wmfme/file_video.html9
-rw-r--r--dom/media/test/browser/wmfme/head.js200
11 files changed, 975 insertions, 0 deletions
diff --git a/dom/media/test/browser/browser.ini b/dom/media/test/browser/browser.ini
new file mode 100644
index 0000000000..11ff9bad1c
--- /dev/null
+++ b/dom/media/test/browser/browser.ini
@@ -0,0 +1,28 @@
+[DEFAULT]
+subsuite = media-bc
+prefs =
+ gfx.font_loader.delay=0
+
+support-files =
+ file_empty_page.html
+ file_media.html
+ ../av1.mp4
+ ../bipbop_short_vp8.webm
+ ../bunny_hd_5s.mp4
+ ../eme_standalone.js
+ ../gizmo.mp4
+ ../gizmo.webm
+ ../sintel-short-clearkey-subsample-encrypted-video.webm
+ ../small-shot.flac
+ ../small-shot.m4a
+ ../small-shot.mp3
+ ../small-shot.ogg
+ ../TestPatternHDR.mp4
+
+[browser_encrypted_play_time_telemetry.js]
+skip-if =
+ apple_silicon # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+[browser_tab_visibility_and_play_time.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_telemetry_video_hardware_decoding_support.js]
diff --git a/dom/media/test/browser/browser_encrypted_play_time_telemetry.js b/dom/media/test/browser/browser_encrypted_play_time_telemetry.js
new file mode 100644
index 0000000000..aebc386e6b
--- /dev/null
+++ b/dom/media/test/browser/browser_encrypted_play_time_telemetry.js
@@ -0,0 +1,266 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test verifies that telemetry gathered around encrypted media playtime
+// is gathered as expected.
+
+"use strict";
+
+/* import-globals-from ../eme_standalone.js */
+
+// Clears any existing telemetry data that has been accumulated. Returns a
+// promise the will be resolved once the telemetry store is clear.
+async function clearTelemetry() {
+ // There's an arbitrary interval of 2 seconds in which the content
+ // processes sync their event data with the parent process, we wait
+ // this out to ensure that we clear everything that is left over from
+ // previous tests and don't receive random events in the middle of our tests.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ Services.telemetry.clearEvents();
+ return TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_ALL_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ });
+}
+
+// Opens a tab containing a blank page, returns a promise that will resolve
+// to that tab.
+async function openTab() {
+ const emptyPageUri =
+ "https://example.com/browser/dom/media/test/browser/file_empty_page.html";
+ return BrowserTestUtils.openNewForegroundTab(window.gBrowser, emptyPageUri);
+}
+
+// Creates and configures a video element for EME playback in `tab`. Does not
+// start playback for the element. Returns a promise that will resolve once
+// the element is setup and ready for playback.
+async function loadEmeVideo(tab) {
+ const emeHelperUri =
+ gTestPath.substr(0, gTestPath.lastIndexOf("/")) + "/eme_standalone.js";
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [emeHelperUri],
+ async _emeHelperUri => {
+ // Begin helper functions.
+ async function once(target, name) {
+ return new Promise(r =>
+ target.addEventListener(name, r, { once: true })
+ );
+ }
+
+ // Helper to clone data into content so the EME helper can use the data.
+ function cloneIntoContent(data) {
+ return Cu.cloneInto(data, content.wrappedJSObject);
+ }
+ // End helper functions.
+
+ // Load the EME helper into content.
+ Services.scriptloader.loadSubScript(_emeHelperUri, content);
+ // Setup EME with the helper.
+ let video = content.document.createElement("video");
+ video.id = "media";
+ content.document.body.appendChild(video);
+ let emeHelper = new content.wrappedJSObject.EmeHelper();
+ emeHelper.SetKeySystem(
+ content.wrappedJSObject.EmeHelper.GetClearkeyKeySystemString()
+ );
+ emeHelper.SetInitDataTypes(cloneIntoContent(["webm"]));
+ emeHelper.SetVideoCapabilities(
+ cloneIntoContent([{ contentType: 'video/webm; codecs="vp9"' }])
+ );
+ emeHelper.AddKeyIdAndKey(
+ "2cdb0ed6119853e7850671c3e9906c3c",
+ "808b9adac384de1e4f56140f4ad76194"
+ );
+ emeHelper.onerror = error => {
+ is(false, `Got unexpected error from EME helper: ${error}`);
+ };
+ await emeHelper.ConfigureEme(video);
+ // Done setting up EME.
+
+ // Setup MSE.
+ const ms = new content.wrappedJSObject.MediaSource();
+ video.src = content.wrappedJSObject.URL.createObjectURL(ms);
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+ const videoFile = "sintel-short-clearkey-subsample-encrypted-video.webm";
+ let fetchResponse = await content.fetch(videoFile);
+ sb.appendBuffer(await fetchResponse.arrayBuffer());
+ await once(sb, "updateend");
+ ms.endOfStream();
+ await once(ms, "sourceended");
+ }
+ );
+}
+
+// Plays the media in `tab` until the 'ended' event is fire. Returns a promise
+// that resolves once that state has been reached.
+async function playMediaThrough(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ let video = content.document.getElementById("media");
+ await Promise.all([new Promise(r => (video.onended = r)), video.play()]);
+ });
+}
+
+// Plays the media in `tab` until the 'timeupdate' event is fire. Returns a
+// promise that resolves once that state has been reached.
+async function playMediaToTimeUpdate(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ let video = content.document.getElementById("media");
+ await Promise.all([
+ new Promise(r => (video.ontimeupdate = r)),
+ video.play(),
+ ]);
+ });
+}
+
+// Aborts existing loads and replaces the media on the media element with an
+// unencrypted file.
+async function replaceMediaWithUnencrypted(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ let video = content.document.getElementById("media");
+ video.src = "gizmo.mp4";
+ video.load();
+ });
+}
+
+// Clears/nulls the media keys on the media in `tab`.
+async function clearMediaKeys(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ let video = content.document.getElementById("media");
+ await video.setMediaKeys(null);
+ });
+}
+
+// Wait for telemetry information to be received from the content process
+// then get the relevant histograms for the tests and return the sums of
+// those histograms. If a histogram does not exist this will return a 0
+// sum. Returns a promise the resolves to an object with sums for
+// - VIDEO_PLAY_TIME_MS
+// - VIDEO_ENCRYPTED_PLAY_TIME_MS
+// - VIDEO_CLEARKEY_PLAY_TIME_MS
+// This function clears the histograms as it gets them.
+async function getTelemetrySums() {
+ // The telemetry was gathered in the content process, so we have to wait
+ // until is arrived in the parent to check it. At time of writing there's
+ // not a more elegant way of doing this than polling.
+ return TestUtils.waitForCondition(() => {
+ let histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ true
+ ).content;
+ // All the histogram data should come at the same time, so we just check
+ // for playtime here as we always expect it in these tests, but we'll
+ // grab other values if present.
+ if (histograms.VIDEO_PLAY_TIME_MS) {
+ // We only expect to have one value for each histogram, so returning the
+ // sums is a short hand for returning that one value.
+ return {
+ VIDEO_PLAY_TIME_MS: histograms.VIDEO_PLAY_TIME_MS.sum,
+ VIDEO_ENCRYPTED_PLAY_TIME_MS: histograms.VIDEO_ENCRYPTED_PLAY_TIME_MS
+ ? histograms.VIDEO_ENCRYPTED_PLAY_TIME_MS.sum
+ : 0,
+ VIDEO_CLEARKEY_PLAY_TIME_MS: histograms.VIDEO_CLEARKEY_PLAY_TIME_MS
+ ? histograms.VIDEO_CLEARKEY_PLAY_TIME_MS.sum
+ : 0,
+ };
+ }
+ return null;
+ }, "recorded telemetry from playing media");
+}
+
+// Clear telemetry before other tests. Internally the tests clear the telemetry
+// when they check it, so we shouldn't need to do this between tests.
+add_task(clearTelemetry);
+
+add_task(async function testEncryptedMediaPlayback() {
+ let testTab = await openTab();
+
+ await loadEmeVideo(testTab);
+ await playMediaThrough(testTab);
+
+ BrowserTestUtils.removeTab(testTab);
+
+ let telemetrySums = await getTelemetrySums();
+
+ ok(telemetrySums, "Should get play time telemetry");
+ is(
+ telemetrySums.VIDEO_PLAY_TIME_MS,
+ telemetrySums.VIDEO_ENCRYPTED_PLAY_TIME_MS,
+ "Play time should be the same as encrypted play time"
+ );
+ is(
+ telemetrySums.VIDEO_PLAY_TIME_MS,
+ telemetrySums.VIDEO_CLEARKEY_PLAY_TIME_MS,
+ "Play time should be the same as clearkey play time"
+ );
+ ok(
+ telemetrySums.VIDEO_PLAY_TIME_MS > 0,
+ "Should have a play time greater than zero"
+ );
+});
+
+add_task(async function testChangingFromEncryptedToUnencrypted() {
+ let testTab = await openTab();
+
+ await loadEmeVideo(testTab);
+ await replaceMediaWithUnencrypted(testTab);
+ await playMediaToTimeUpdate(testTab);
+
+ BrowserTestUtils.removeTab(testTab);
+
+ let telemetrySums = await getTelemetrySums();
+
+ ok(telemetrySums, "Should get play time telemetry");
+ is(
+ telemetrySums.VIDEO_ENCRYPTED_PLAY_TIME_MS,
+ 0,
+ "Encrypted play time should be 0"
+ );
+ is(
+ telemetrySums.VIDEO_PLAY_TIME_MS,
+ telemetrySums.VIDEO_CLEARKEY_PLAY_TIME_MS,
+ "Play time should be the same as clearkey play time because the media element still has a media keys attached"
+ );
+ ok(
+ telemetrySums.VIDEO_PLAY_TIME_MS > 0,
+ "Should have a play time greater than zero"
+ );
+});
+
+add_task(
+ async function testChangingFromEncryptedToUnencryptedAndClearingMediaKeys() {
+ let testTab = await openTab();
+
+ await loadEmeVideo(testTab);
+ await replaceMediaWithUnencrypted(testTab);
+ await clearMediaKeys(testTab);
+ await playMediaToTimeUpdate(testTab);
+
+ BrowserTestUtils.removeTab(testTab);
+
+ let telemetrySums = await getTelemetrySums();
+
+ ok(telemetrySums, "Should get play time telemetry");
+ is(
+ telemetrySums.VIDEO_ENCRYPTED_PLAY_TIME_MS,
+ 0,
+ "Encrypted play time should be 0"
+ );
+ is(
+ telemetrySums.VIDEO_CLEARKEY_PLAY_TIME_MS,
+ 0,
+ "Clearkey play time should be 0"
+ );
+ ok(
+ telemetrySums.VIDEO_PLAY_TIME_MS > 0,
+ "Should have a play time greater than zero"
+ );
+ }
+);
diff --git a/dom/media/test/browser/browser_tab_visibility_and_play_time.js b/dom/media/test/browser/browser_tab_visibility_and_play_time.js
new file mode 100644
index 0000000000..66fae40889
--- /dev/null
+++ b/dom/media/test/browser/browser_tab_visibility_and_play_time.js
@@ -0,0 +1,216 @@
+/**
+ * This test is used to ensure that invisible play time would be accumulated
+ * when tab is in background. It also checks the HDR video accumulation time.
+ * However, this test won't directly check the reported telemetry result,
+ * because we can't check the snapshot histogram in the content process.
+ * The actual probe checking happens in `test_accumulated_play_time.html`.
+ */
+"use strict";
+
+const PAGE_URL =
+ "https://example.com/browser/dom/media/test/browser/file_media.html";
+
+// This HDR tests will only pass on platforms that accurately report color
+// depth in their VideoInfo structures. Presently, that is only true for
+// macOS.
+
+const reportsColorDepthFromVideoData = AppConstants.platform == "macosx";
+
+add_task(async function testChangingTabVisibilityAffectsInvisiblePlayTime() {
+ const originalTab = gBrowser.selectedTab;
+ const mediaTab = await openMediaTab(PAGE_URL);
+
+ info(`measuring play time when tab is in foreground`);
+ await startMedia({
+ mediaTab,
+ shouldAccumulateTime: true,
+ shouldAccumulateInvisibleTime: false,
+ shouldAccumulateHDRTime: reportsColorDepthFromVideoData,
+ });
+ await pauseMedia(mediaTab);
+
+ info(`measuring play time when tab is in background`);
+ await BrowserTestUtils.switchTab(window.gBrowser, originalTab);
+ await startMedia({
+ mediaTab,
+ shouldAccumulateTime: true,
+ shouldAccumulateInvisibleTime: true,
+ shouldAccumulateHDRTime: reportsColorDepthFromVideoData,
+ });
+ await pauseMedia(mediaTab);
+
+ BrowserTestUtils.removeTab(mediaTab);
+});
+
+/**
+ * Following are helper functions.
+ */
+async function openMediaTab(url) {
+ info(`open tab for media playback`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, url);
+ info(`add content helper functions and variables`);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content.waitForOnTimeUpdate = element => {
+ return new Promise(resolve => {
+ element.addEventListener(
+ "timeupdate",
+ e => {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ };
+
+ content.sleep = ms => {
+ return new Promise(resolve => content.setTimeout(resolve, ms));
+ };
+
+ content.assertAttributeDefined = (videoChrome, checkType) => {
+ ok(videoChrome[checkType] != undefined, `${checkType} exists`);
+ };
+ content.assertValueEqualTo = (videoChrome, checkType, expectedValue) => {
+ content.assertAttributeDefined(videoChrome, checkType);
+ is(
+ videoChrome[checkType],
+ expectedValue,
+ `${checkType} equals to ${expectedValue}`
+ );
+ };
+ content.assertValueConstantlyIncreases = async (videoChrome, checkType) => {
+ content.assertAttributeDefined(videoChrome, checkType);
+ const valueSnapshot = videoChrome[checkType];
+ await content.waitForOnTimeUpdate(videoChrome);
+ ok(
+ videoChrome[checkType] > valueSnapshot,
+ `${checkType} keeps increasing`
+ );
+ };
+ content.assertValueKeptUnchanged = async (videoChrome, checkType) => {
+ content.assertAttributeDefined(videoChrome, checkType);
+ const valueSnapshot = videoChrome[checkType];
+ await content.sleep(1000);
+ ok(
+ videoChrome[checkType] == valueSnapshot,
+ `${checkType} keeps unchanged`
+ );
+ };
+ });
+ return tab;
+}
+
+function startMedia({
+ mediaTab,
+ shouldAccumulateTime,
+ shouldAccumulateInvisibleTime,
+ shouldAccumulateHDRTime,
+}) {
+ return SpecialPowers.spawn(
+ mediaTab.linkedBrowser,
+ [
+ shouldAccumulateTime,
+ shouldAccumulateInvisibleTime,
+ shouldAccumulateHDRTime,
+ ],
+ async (accumulateTime, accumulateInvisibleTime, accumulateHDRTime) => {
+ const video = content.document.getElementById("video");
+ ok(
+ await video.play().then(
+ () => true,
+ () => false
+ ),
+ "video started playing"
+ );
+ const videoChrome = SpecialPowers.wrap(video);
+ if (accumulateTime) {
+ await content.assertValueConstantlyIncreases(
+ videoChrome,
+ "totalVideoPlayTime"
+ );
+ } else {
+ await content.assertValueKeptUnchanged(
+ videoChrome,
+ "totalVideoPlayTime"
+ );
+ }
+ if (accumulateInvisibleTime) {
+ await content.assertValueConstantlyIncreases(
+ videoChrome,
+ "invisiblePlayTime"
+ );
+ } else {
+ await content.assertValueKeptUnchanged(
+ videoChrome,
+ "invisiblePlayTime"
+ );
+ }
+
+ const videoHDR = content.document.getElementById("videoHDR");
+
+ // HDR test video might not decode on all platforms, so catch
+ // the play() command and exit early in such a case. Failure to
+ // decode might manifest as a timeout, so add a rejection race
+ // to catch that.
+ let didDecode = true;
+ const playPromise = videoHDR.play().then(
+ () => true,
+ () => false
+ );
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ const tooSlowPromise = new Promise(resolve =>
+ setTimeout(() => {
+ info("videoHDR timed out.");
+ didDecode = false;
+ resolve(false);
+ }, 1000)
+ );
+ /* eslint-enable mozilla/no-arbitrary-setTimeout */
+
+ let didPlay = await Promise.race(playPromise, tooSlowPromise).catch(
+ err => {
+ info("videoHDR failed to decode with error: " + err.message);
+ didDecode = false;
+ return false;
+ }
+ );
+
+ if (!didDecode) {
+ return;
+ }
+
+ ok(didPlay, "videoHDR started playing");
+ const videoHDRChrome = SpecialPowers.wrap(videoHDR);
+ if (accumulateHDRTime) {
+ await content.assertValueConstantlyIncreases(
+ videoHDRChrome,
+ "totalVideoHDRPlayTime"
+ );
+ } else {
+ await content.assertValueKeptUnchanged(
+ videoHDRChrome,
+ "totalVideoHDRPlayTime"
+ );
+ }
+ }
+ );
+}
+
+function pauseMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ const video = content.document.getElementById("video");
+ video.pause();
+ ok(true, "video paused");
+ const videoChrome = SpecialPowers.wrap(video);
+ await content.assertValueKeptUnchanged(videoChrome, "totalVideoPlayTime");
+ await content.assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
+
+ const videoHDR = content.document.getElementById("videoHDR");
+ videoHDR.pause();
+ ok(true, "videoHDR paused");
+ const videoHDRChrome = SpecialPowers.wrap(videoHDR);
+ await content.assertValueKeptUnchanged(
+ videoHDRChrome,
+ "totalVideoHDRPlayTime"
+ );
+ });
+}
diff --git a/dom/media/test/browser/browser_telemetry_video_hardware_decoding_support.js b/dom/media/test/browser/browser_telemetry_video_hardware_decoding_support.js
new file mode 100644
index 0000000000..3b1b41c03f
--- /dev/null
+++ b/dom/media/test/browser/browser_telemetry_video_hardware_decoding_support.js
@@ -0,0 +1,106 @@
+/**
+ * This test is used to ensure that the scalar which indicates whether hardware
+ * decoding is supported for a specific video codec type can be recorded
+ * correctly.
+ */
+"use strict";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // In order to test av1 in the chrome process, see https://bit.ly/3oF0oan
+ ["media.rdd-process.enabled", false],
+ ],
+ });
+});
+
+const ALL_SCALAR = "media.video_hardware_decoding_support";
+const HD_SCALAR = "media.video_hd_hardware_decoding_support";
+
+add_task(async function testVideoCodecs() {
+ // There are still other video codecs, but we only care about these popular
+ // codec types.
+ const testFiles = [
+ { fileName: "gizmo.mp4", type: "video/avc" },
+ { fileName: "gizmo.webm", type: "video/vp9" },
+ { fileName: "bipbop_short_vp8.webm", type: "video/vp8" },
+ { fileName: "av1.mp4", type: "video/av1" },
+ { fileName: "bunny_hd_5s.mp4", type: "video/avc", hd: true },
+ ];
+
+ for (const file of testFiles) {
+ const { fileName, type, hd } = file;
+ let video = document.createElement("video");
+ video.src = GetTestWebBasedURL(fileName);
+ await video.play();
+ let snapshot = Services.telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false
+ ).parent;
+ ok(
+ snapshot.hasOwnProperty(ALL_SCALAR),
+ `Found stored scalar '${ALL_SCALAR}'`
+ );
+ ok(
+ snapshot[ALL_SCALAR].hasOwnProperty(type),
+ `Found key '${type}' in '${ALL_SCALAR}'`
+ );
+ if (hd) {
+ ok(
+ snapshot.hasOwnProperty(HD_SCALAR),
+ `HD video '${fileName}' should record a scalar '${HD_SCALAR}'`
+ );
+ ok(
+ snapshot[HD_SCALAR].hasOwnProperty(type),
+ `Found key '${type}' in '${HD_SCALAR}'`
+ );
+ } else {
+ ok(
+ !snapshot.hasOwnProperty(HD_SCALAR),
+ `SD video won't store a scalar '${HD_SCALAR}'`
+ );
+ }
+ video.src = "";
+ Services.telemetry.clearScalars();
+ }
+});
+
+add_task(async function testAudioCodecs() {
+ const testFiles = [
+ "small-shot.ogg",
+ "small-shot.m4a",
+ "small-shot.mp3",
+ "small-shot.flac",
+ ];
+ for (const file of testFiles) {
+ let audio = document.createElement("audio");
+ info(GetTestWebBasedURL(file));
+ audio.src = GetTestWebBasedURL(file);
+ await audio.play();
+ let snapshot = Services.telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false
+ ).parent;
+ ok(
+ !snapshot ||
+ (!snapshot.hasOwnProperty(ALL_SCALAR) &&
+ !snapshot.hasOwnProperty(HD_SCALAR)),
+ `Did not record scalar for ${file}`
+ );
+ audio.src = "";
+ }
+});
+
+/**
+ * Return a web-based URL for a given file based on the testing directory.
+ * @param {String} fileName
+ * file that caller wants its web-based url
+ */
+function GetTestWebBasedURL(fileName) {
+ return (
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.org"
+ ) + fileName
+ );
+}
diff --git a/dom/media/test/browser/file_empty_page.html b/dom/media/test/browser/file_empty_page.html
new file mode 100644
index 0000000000..cd1b7830be
--- /dev/null
+++ b/dom/media/test/browser/file_empty_page.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>An empty page</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/browser/file_media.html b/dom/media/test/browser/file_media.html
new file mode 100644
index 0000000000..36dca8d01c
--- /dev/null
+++ b/dom/media/test/browser/file_media.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Non-Autoplay page</title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+<video id="videoHDR" src="TestPatternHDR.mp4" loop></video>
+</body>
+</html>
diff --git a/dom/media/test/browser/wmfme/browser.ini b/dom/media/test/browser/wmfme/browser.ini
new file mode 100644
index 0000000000..9913bdb19e
--- /dev/null
+++ b/dom/media/test/browser/wmfme/browser.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+subsuite = media-bc
+tags = media-engine-compatible
+run-if = wmfme
+support-files =
+ head.js
+ file_video.html
+ ../../gizmo.mp4
+
+[browser_wmfme_crash.js]
+[browser_wmfme_max_crashes.js]
diff --git a/dom/media/test/browser/wmfme/browser_wmfme_crash.js b/dom/media/test/browser/wmfme/browser_wmfme_crash.js
new file mode 100644
index 0000000000..8ebe582595
--- /dev/null
+++ b/dom/media/test/browser/wmfme/browser_wmfme_crash.js
@@ -0,0 +1,52 @@
+"use strict";
+
+/**
+ * This test aims to ensure that the media engine playback will recover from a
+ * crash and keep playing without any problem.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.wmf.media-engine.enabled", true],
+ ["media.wmf.media-engine.channel-decoder.enabled", true],
+ ],
+ });
+});
+
+const VIDEO_PAGE = GetTestWebBasedURL("file_video.html");
+
+add_task(async function testPlaybackRecoveryFromCrash() {
+ info(`Create a tab and load test page`);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ await playVideo(tab);
+
+ info("Ensure video is running via the media engine framework");
+ await assertRunningProcessAndDecoderName(tab, {
+ expectedProcess: "Utility MF Media Engine CDM",
+ expectedDecoder: "media engine video stream",
+ });
+
+ const pidBeforeCrash = await getMFCDMProcessId();
+ await crashUtilityProcess(pidBeforeCrash);
+
+ info("The CDM process should be recreated which makes media keep playing");
+ await assertRunningProcessAndDecoderName(tab, {
+ expectedProcess: "Utility MF Media Engine CDM",
+ expectedDecoder: "media engine video stream",
+ });
+
+ const pidAfterCrash = await getMFCDMProcessId();
+ isnot(
+ pidBeforeCrash,
+ pidAfterCrash,
+ `new process ${pidAfterCrash} is not previous crashed one ${pidBeforeCrash}`
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/media/test/browser/wmfme/browser_wmfme_max_crashes.js b/dom/media/test/browser/wmfme/browser_wmfme_max_crashes.js
new file mode 100644
index 0000000000..4472814171
--- /dev/null
+++ b/dom/media/test/browser/wmfme/browser_wmfme_max_crashes.js
@@ -0,0 +1,69 @@
+"use strict";
+
+/**
+ * This test aims to ensure that the MFCDM process won't be recovered once the
+ * amount of crashes has exceeded the amount of value which we tolerate.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.wmf.media-engine.enabled", true],
+ ["media.wmf.media-engine.channel-decoder.enabled", true],
+ ],
+ });
+});
+
+const VIDEO_PAGE = GetTestWebBasedURL("file_video.html");
+
+add_task(async function testPlaybackRecoveryFromCrash() {
+ const maxCrashes = Services.prefs.getIntPref(
+ "media.wmf.media-engine.max-crashes"
+ );
+ info(`The amount of tolerable crashes=${maxCrashes}`);
+
+ info(`Create a tab and load test page`);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ await playVideo(tab);
+
+ info("Ensure video is running via the media engine framework");
+ await assertRunningProcessAndDecoderName(tab, {
+ expectedProcess: "Utility MF Media Engine CDM",
+ expectedDecoder: "media engine video stream",
+ });
+
+ let pidBeforeCrash, pidAfterCrash;
+ for (let idx = 0; idx < maxCrashes; idx++) {
+ pidBeforeCrash = await getMFCDMProcessId();
+ await crashUtilityProcess(pidBeforeCrash);
+
+ info("The CDM process should be recreated which makes media keep playing");
+ await assertRunningProcessAndDecoderName(tab, {
+ expectedProcess: "Utility MF Media Engine CDM",
+ expectedDecoder: "media engine video stream",
+ });
+
+ pidAfterCrash = await getMFCDMProcessId();
+ isnot(
+ pidBeforeCrash,
+ pidAfterCrash,
+ `new process ${pidAfterCrash} is not previous crashed one ${pidBeforeCrash}`
+ );
+ }
+
+ info("This crash should result in not spawning MFCDM process again");
+ pidBeforeCrash = await getMFCDMProcessId();
+ await crashUtilityProcess(pidBeforeCrash);
+
+ await assertNotEqualRunningProcessAndDecoderName(tab, {
+ givenProcess: "Utility MF Media Engine CDM",
+ givenDecoder: "media engine video stream",
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/media/test/browser/wmfme/file_video.html b/dom/media/test/browser/wmfme/file_video.html
new file mode 100644
index 0000000000..3c70268fbb
--- /dev/null
+++ b/dom/media/test/browser/wmfme/file_video.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>video</title>
+</head>
+<body>
+<video id="v" src="gizmo.mp4" controls loop></video>
+</body>
+</html>
diff --git a/dom/media/test/browser/wmfme/head.js b/dom/media/test/browser/wmfme/head.js
new file mode 100644
index 0000000000..2524287870
--- /dev/null
+++ b/dom/media/test/browser/wmfme/head.js
@@ -0,0 +1,200 @@
+"use strict";
+
+/**
+ * Return a web-based URL for a given file based on the testing directory.
+ * @param {String} fileName
+ * file that caller wants its web-based url
+ * @param {Boolean} cors [optional]
+ * if set, then return a url with different origin
+ */
+function GetTestWebBasedURL(fileName) {
+ const origin = "https://example.com";
+ return (
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) +
+ fileName
+ );
+}
+
+/**
+ * Return current process Id for the Media Foundation CDM process.
+ */
+async function getMFCDMProcessId() {
+ const process = (await ChromeUtils.requestProcInfo()).children.find(
+ p =>
+ p.type === "utility" &&
+ p.utilityActors.find(a => a.actorName === "mfMediaEngineCDM")
+ );
+ return process.pid;
+}
+
+/**
+ * Make the utility process with given process id crash.
+ * @param {int} pid
+ * the process id for the process which is going to crash
+ */
+async function crashUtilityProcess(utilityPid) {
+ info(`Crashing process ${utilityPid}`);
+ SimpleTest.expectChildProcessCrash();
+
+ const crashMan = Services.crashmanager;
+ const utilityProcessGone = TestUtils.topicObserved(
+ "ipc:utility-shutdown",
+ (subject, data) => {
+ info(`ipc:utility-shutdown: data=${data} subject=${subject}`);
+ return parseInt(data, 10) === utilityPid;
+ }
+ );
+
+ info("Prune any previous crashes");
+ const future = new Date(Date.now() + 1000 * 60 * 60 * 24);
+ await crashMan.pruneOldCrashes(future);
+
+ info("Crash Utility Process");
+ const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+ );
+
+ info(`Crash Utility Process ${utilityPid}`);
+ ProcessTools.crash(utilityPid);
+
+ info(`Waiting for utility process ${utilityPid} to go away.`);
+ let [subject, data] = await utilityProcessGone;
+ ok(
+ parseInt(data, 10) === utilityPid,
+ `Should match the crashed PID ${utilityPid} with ${data}`
+ );
+ ok(
+ subject instanceof Ci.nsIPropertyBag2,
+ "Subject needs to be a nsIPropertyBag2 to clean up properly"
+ );
+
+ const dumpID = subject.getPropertyAsAString("dumpID");
+ ok(dumpID, "There should be a dumpID");
+
+ await crashMan.ensureCrashIsPresent(dumpID);
+ await crashMan.getCrashes().then(crashes => {
+ is(crashes.length, 1, "There should be only one record");
+ const crash = crashes[0];
+ ok(
+ crash.isOfType(
+ crashMan.processTypes[Ci.nsIXULRuntime.PROCESS_TYPE_UTILITY],
+ crashMan.CRASH_TYPE_CRASH
+ ),
+ "Record should be a utility process crash"
+ );
+ ok(crash.id === dumpID, "Record should have an ID");
+ });
+
+ let minidumpDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ minidumpDirectory.append("minidumps");
+
+ let dumpfile = minidumpDirectory.clone();
+ dumpfile.append(dumpID + ".dmp");
+ if (dumpfile.exists()) {
+ info(`Removal of ${dumpfile.path}`);
+ dumpfile.remove(false);
+ }
+
+ let extrafile = minidumpDirectory.clone();
+ extrafile.append(dumpID + ".extra");
+ info(`Removal of ${extrafile.path}`);
+ if (extrafile.exists()) {
+ extrafile.remove(false);
+ }
+}
+
+/**
+ * Make video in the tab play.
+ * @param {object} tab
+ * the tab contains at least one video element
+ */
+async function playVideo(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ const video = content.document.querySelector("video");
+ ok(
+ await video.play().then(
+ () => true,
+ () => false
+ ),
+ "video started playing"
+ );
+ });
+}
+
+/**
+ * Check whether the video playback is performed in the right process and right decoder.
+ * @param {object} tab
+ * the tab which has a playing video
+ * @param {string} expectedProcess
+ * the expected process name
+ * @param {string} expectedDecoder
+ * the expected decoder name
+ */
+async function assertRunningProcessAndDecoderName(
+ tab,
+ { expectedProcess, expectedDecoder } = {}
+) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expectedProcess, expectedDecoder],
+ // eslint-disable-next-line no-shadow
+ async (expectedProcess, expectedDecoder) => {
+ const video = content.document.querySelector("video");
+ ok(!video.paused, "checking a playing video");
+
+ const debugInfo = await SpecialPowers.wrap(video).mozRequestDebugInfo();
+ const videoDecoderName = debugInfo.decoder.reader.videoDecoderName;
+
+ const isExpectedDecoder =
+ videoDecoderName.indexOf(`${expectedDecoder}`) == 0;
+ ok(
+ isExpectedDecoder,
+ `Playback running by decoder '${videoDecoderName}', expected '${expectedDecoder}'`
+ );
+
+ const isExpectedProcess =
+ videoDecoderName.indexOf(`(${expectedProcess} remote)`) > 0;
+ ok(
+ isExpectedProcess,
+ `Playback running in process '${videoDecoderName}', expected '${expectedProcess}'`
+ );
+ }
+ );
+}
+
+/**
+ * Check whether the video playback is not performed in the given process and given decoder.
+ * @param {object} tab
+ * the tab which has a playing video
+ * @param {string} givenProcess
+ * the process name on which the video playback should not be running
+ * @param {string} givenDecoder
+ * the decoder name with which the video playback should not be running
+ */
+async function assertNotEqualRunningProcessAndDecoderName(
+ tab,
+ { givenProcess, givenDecoder } = {}
+) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [givenProcess, givenDecoder],
+ // eslint-disable-next-line no-shadow
+ async (givenProcess, givenDecoder) => {
+ const video = content.document.querySelector("video");
+ ok(!video.paused, "checking a playing video");
+
+ const debugInfo = await SpecialPowers.wrap(video).mozRequestDebugInfo();
+ const videoDecoderName = debugInfo.decoder.reader.videoDecoderName;
+ const pattern = /(.+?)\s+\((\S+)\s+remote\)/;
+ const match = videoDecoderName.match(pattern);
+ if (match) {
+ const decoder = match[1];
+ const process = match[2];
+ isnot(decoder, givenDecoder, `Decoder name is not equal`);
+ isnot(process, givenProcess, `Process name is not equal`);
+ } else {
+ ok(false, "failed to match decoder/process name?");
+ }
+ }
+ );
+}