diff options
Diffstat (limited to '')
-rw-r--r-- | dom/media/test/browser/browser.ini | 28 | ||||
-rw-r--r-- | dom/media/test/browser/browser_encrypted_play_time_telemetry.js | 266 | ||||
-rw-r--r-- | dom/media/test/browser/browser_tab_visibility_and_play_time.js | 216 | ||||
-rw-r--r-- | dom/media/test/browser/browser_telemetry_video_hardware_decoding_support.js | 106 | ||||
-rw-r--r-- | dom/media/test/browser/file_empty_page.html | 8 | ||||
-rw-r--r-- | dom/media/test/browser/file_media.html | 10 | ||||
-rw-r--r-- | dom/media/test/browser/wmfme/browser.ini | 11 | ||||
-rw-r--r-- | dom/media/test/browser/wmfme/browser_wmfme_crash.js | 52 | ||||
-rw-r--r-- | dom/media/test/browser/wmfme/browser_wmfme_max_crashes.js | 69 | ||||
-rw-r--r-- | dom/media/test/browser/wmfme/file_video.html | 9 | ||||
-rw-r--r-- | dom/media/test/browser/wmfme/head.js | 200 |
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?"); + } + } + ); +} |