summaryrefslogtreecommitdiffstats
path: root/dom/media/mediacontrol/tests
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/mediacontrol/tests')
-rw-r--r--dom/media/mediacontrol/tests/browser/browser.ini53
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_audio_focus_management.js179
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_control_page_with_audible_and_inaudible_media.js94
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_default_action_handler.js422
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_audio_focus_within_a_page.js358
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_before_media_starts.js205
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_captured_audio.js45
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_keys_event.js62
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_main_controller.js341
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_metadata.js416
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_non_eligible_media.js204
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_playback_state.js116
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js150
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_seekto.js89
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_stop_timer.js79
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_supported_keys.js130
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_nosrc_and_error_media.js102
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_only_control_non_real_time_media.js76
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_remove_controllable_media_for_active_controller.js108
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_resume_latest_paused_media.js189
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_seek_captured_audio.js59
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_stop_control_after_media_reaches_to_end.js108
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_suspend_inactive_tab.js131
-rw-r--r--dom/media/mediacontrol/tests/browser/file_audio_and_inaudible_media.html10
-rw-r--r--dom/media/mediacontrol/tests/browser/file_autoplay.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/file_empty_title.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/file_error_media.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/file_iframe_media.html94
-rw-r--r--dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html11
-rw-r--r--dom/media/mediacontrol/tests/browser/file_multiple_audible_media.html11
-rw-r--r--dom/media/mediacontrol/tests/browser/file_muted_autoplay.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/file_no_src_media.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/file_non_autoplay.html11
-rw-r--r--dom/media/mediacontrol/tests/browser/file_non_eligible_media.html14
-rw-r--r--dom/media/mediacontrol/tests/browser/file_non_looping_media.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/head.js402
-rw-r--r--dom/media/mediacontrol/tests/gtest/MediaKeyListenerTest.h39
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestAudioFocusManager.cpp163
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestMediaControlService.cpp64
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestMediaController.cpp204
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestMediaKeysEvent.cpp49
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMac.mm136
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMediaCenter.mm166
-rw-r--r--dom/media/mediacontrol/tests/gtest/moz.build23
44 files changed, 5167 insertions, 0 deletions
diff --git a/dom/media/mediacontrol/tests/browser/browser.ini b/dom/media/mediacontrol/tests/browser/browser.ini
new file mode 100644
index 0000000000..d6338e598b
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser.ini
@@ -0,0 +1,53 @@
+[DEFAULT]
+subsuite = media-bc
+tags = mediacontrol
+skip-if = os == "linux" # Bug 1673527
+support-files =
+ file_autoplay.html
+ file_audio_and_inaudible_media.html
+ file_empty_title.html
+ file_error_media.html
+ file_iframe_media.html
+ file_main_frame_with_multiple_child_session_frames.html
+ file_multiple_audible_media.html
+ file_muted_autoplay.html
+ file_no_src_media.html
+ file_non_autoplay.html
+ file_non_eligible_media.html
+ file_non_looping_media.html
+ head.js
+ ../../../test/bogus.ogv
+ ../../../test/gizmo.mp4
+ ../../../test/gizmo-noaudio.webm
+ ../../../test/gizmo-short.mp4
+ !/toolkit/components/pictureinpicture/tests/head.js
+ ../../../../../toolkit/content/tests/browser/silentAudioTrack.webm
+
+[browser_audio_focus_management.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_control_page_with_audible_and_inaudible_media.js]
+[browser_default_action_handler.js]
+[browser_only_control_non_real_time_media.js]
+[browser_media_control_audio_focus_within_a_page.js]
+[browser_media_control_before_media_starts.js]
+[browser_media_control_captured_audio.js]
+[browser_media_control_metadata.js]
+[browser_media_control_keys_event.js]
+[browser_media_control_main_controller.js]
+[browser_media_control_non_eligible_media.js]
+skip-if =
+ verify && os == 'mac' # bug 1673509
+[browser_media_control_playback_state.js]
+[browser_media_control_position_state.js]
+[browser_media_control_seekto.js]
+[browser_media_control_supported_keys.js]
+[browser_media_control_stop_timer.js]
+[browser_nosrc_and_error_media.js]
+skip-if =
+ verify && os == 'mac' # bug 1673509
+[browser_seek_captured_audio.js]
+[browser_stop_control_after_media_reaches_to_end.js]
+[browser_suspend_inactive_tab.js]
+[browser_remove_controllable_media_for_active_controller.js]
+[browser_resume_latest_paused_media.js]
diff --git a/dom/media/mediacontrol/tests/browser/browser_audio_focus_management.js b/dom/media/mediacontrol/tests/browser/browser_audio_focus_management.js
new file mode 100644
index 0000000000..980281243d
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_audio_focus_management.js
@@ -0,0 +1,179 @@
+const PAGE_AUDIBLE =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_autoplay.html";
+const PAGE_INAUDIBLE =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_muted_autoplay.html";
+
+const testVideoId = "autoplay";
+
+/**
+ * These tests are used to ensure that the audio focus management works correctly
+ * amongs different tabs no matter the pref is on or off. If the pref is on,
+ * there is only one tab which is allowed to play audio at a time, the last tab
+ * starting audio will immediately stop other tabs which own audio focus. But
+ * notice that playing inaudible media won't gain audio focus. If the pref is
+ * off, all audible tabs can own audio focus at the same time without
+ * interfering each others.
+ */
+add_task(async function testDisableAudioFocusManagement() {
+ await switchAudioFocusManagerment(false);
+
+ info(`open audible autoplay media in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(`open same page on another tab, which shouldn't cause audio competing`);
+ const tab2 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ info(`media in tab1 should be playing still`);
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(`remove tabs`);
+ await clearTabsAndResetPref([tab1, tab2]);
+});
+
+add_task(async function testEnableAudioFocusManagement() {
+ await switchAudioFocusManagerment(true);
+
+ info(`open audible autoplay media in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(`open same page on another tab, which should cause audio competing`);
+ const tab2 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ info(`media in tab1 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab1, testVideoId);
+
+ info(`remove tabs`);
+ await clearTabsAndResetPref([tab1, tab2]);
+});
+
+add_task(async function testCheckAudioCompetingMultipleTimes() {
+ await switchAudioFocusManagerment(true);
+
+ info(`open audible autoplay media in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(`open same page on another tab, which should cause audio competing`);
+ const tab2 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ info(`media in tab1 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab1, testVideoId);
+
+ info(`play media in tab1 again`);
+ await playMedia(tab1);
+
+ info(`media in tab2 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab2, testVideoId);
+
+ info(`play media in tab2 again`);
+ await playMedia(tab2);
+
+ info(`media in tab1 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab1, testVideoId);
+
+ info(`remove tabs`);
+ await clearTabsAndResetPref([tab1, tab2]);
+});
+
+add_task(async function testMutedMediaWontInvolveAudioCompeting() {
+ await switchAudioFocusManagerment(true);
+
+ info(`open audible autoplay media in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(
+ `open inaudible media page on another tab, which shouldn't cause audio competing`
+ );
+ const tab2 = await createLoadedTabWrapper(PAGE_INAUDIBLE, {
+ needCheck: false,
+ });
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ info(`media in tab1 should be playing still`);
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(
+ `open audible media page on the third tab, which should cause audio competing`
+ );
+ const tab3 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab3, testVideoId);
+
+ info(`media in tab1 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab1, testVideoId);
+
+ info(`media in tab2 should not be affected because it's inaudible.`);
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ info(`remove tabs`);
+ await clearTabsAndResetPref([tab1, tab2, tab3]);
+});
+
+add_task(async function testStopMultipleTabsWhenSwitchingPrefDynamically() {
+ await switchAudioFocusManagerment(false);
+
+ info(`open audible autoplay media in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(`open same page on another tab, which shouldn't cause audio competing`);
+ const tab2 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ await switchAudioFocusManagerment(true);
+
+ info(`open same page on the third tab, which should cause audio competing`);
+ const tab3 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab3, testVideoId);
+
+ info(`media in tab1 and tab2 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab1, testVideoId);
+ await checkOrWaitUntilMediaStoppedPlaying(tab2, testVideoId);
+
+ info(`remove tabs`);
+ await clearTabsAndResetPref([tab1, tab2, tab3]);
+});
+
+/**
+ * The following are helper funcions.
+ */
+async function switchAudioFocusManagerment(enable) {
+ const state = enable ? "Enable" : "Disable";
+ info(`${state} audio focus management`);
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.audioFocus.management", enable]],
+ });
+}
+
+async function playMedia(tab) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return new Promise(resolve => {
+ const video = content.document.getElementById("autoplay");
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+
+ ok(video.paused, `media has not started yet`);
+ info(`wait until media starts playing`);
+ video.play();
+ video.onplaying = () => {
+ video.onplaying = null;
+ ok(true, `media started playing`);
+ resolve();
+ };
+ });
+ });
+}
+
+async function clearTabsAndResetPref(tabs) {
+ info(`clear tabs and reset pref`);
+ for (let tab of tabs) {
+ await tab.close();
+ }
+ await switchAudioFocusManagerment(false);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_control_page_with_audible_and_inaudible_media.js b/dom/media/mediacontrol/tests/browser/browser_control_page_with_audible_and_inaudible_media.js
new file mode 100644
index 0000000000..6d9bc38b04
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_control_page_with_audible_and_inaudible_media.js
@@ -0,0 +1,94 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_audio_and_inaudible_media.html";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * When a page has audible media and inaudible media playing at the same time,
+ * only audible media should be controlled by media control keys. However, once
+ * inaudible media becomes audible, then it should be able to be controlled.
+ */
+add_task(async function testSetPositionState() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`play video1 (audible) and video2 (inaudible)`);
+ await playBothAudibleAndInaudibleMedia(tab);
+
+ info(`pressing 'pause' should only affect video1 (audible)`);
+ await generateMediaControlKeyEvent("pause");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: false,
+ });
+
+ info(`make video2 become audible, then it would be able to be controlled`);
+ await unmuteInaudibleMedia(tab);
+
+ info(`pressing 'pause' should affect video2 (audible`);
+ await generateMediaControlKeyEvent("pause");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: true,
+ });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+async function playBothAudibleAndInaudibleMedia(tab) {
+ const playbackStateChangedPromise = waitUntilDisplayedPlaybackChanged();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const videos = content.document.getElementsByTagName("video");
+ let promises = [];
+ for (let video of videos) {
+ info(`play ${video.id} video`);
+ promises.push(video.play());
+ }
+ return Promise.all(promises);
+ });
+ await playbackStateChangedPromise;
+}
+
+function checkMediaPausedState(
+ tab,
+ { shouldVideo1BePaused, shouldVideo2BePaused }
+) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [shouldVideo1BePaused, shouldVideo2BePaused],
+ (shouldVideo1BePaused, shouldVideo2BePaused) => {
+ const video1 = content.document.getElementById("video1");
+ const video2 = content.document.getElementById("video2");
+ is(
+ video1.paused,
+ shouldVideo1BePaused,
+ "Correct paused state for video1"
+ );
+ is(
+ video2.paused,
+ shouldVideo2BePaused,
+ "Correct paused state for video2"
+ );
+ }
+ );
+}
+
+function unmuteInaudibleMedia(tab) {
+ const unmutePromise = SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const video2 = content.document.getElementById("video2");
+ video2.muted = false;
+ });
+ // Inaudible media was not controllable, so it won't affect the controller's
+ // playback state. However, when it becomes audible, which means being able to
+ // be controlled by media controller, it would make the playback state chanege
+ // to `playing` because now we have an audible playinng media in the page.
+ return Promise.all([unmutePromise, waitUntilDisplayedPlaybackChanged()]);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_default_action_handler.js b/dom/media/mediacontrol/tests/browser/browser_default_action_handler.js
new file mode 100644
index 0000000000..c33c08b0c2
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_default_action_handler.js
@@ -0,0 +1,422 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const PAGE2_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html";
+const IFRAME_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+const CORS_IFRAME_URL =
+ "https://example.org/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+const CORS_IFRAME2_URL =
+ "https://test1.example.org/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+const videoId = "video";
+
+/**
+ * This test is used to check the scenario when we should use the customized
+ * action handler and the the default action handler (play/pause/stop).
+ * If a frame (DOM Window, it could be main frame or an iframe) has active media
+ * session, then it should use the customized action handler it it has one.
+ * Otherwise, the default action handler should be used.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.media.mediasession.enabled", true],
+ ["media.mediacontrol.testingevents.enabled", true],
+ ],
+ });
+});
+
+add_task(async function triggerDefaultActionHandler() {
+ // Default handler should be triggered no matter if media session exists or not.
+ const kCreateMediaSession = [true, false];
+ for (const shouldCreateSession of kCreateMediaSession) {
+ info(`open page and start media`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ await playMedia(tab, videoId);
+
+ if (shouldCreateSession) {
+ info(
+ `media has started, so created session should become active session`
+ );
+ await Promise.all([
+ waitUntilActiveMediaSessionChanged(),
+ createMediaSession(tab),
+ ]);
+ }
+
+ info(`test 'pause' action`);
+ await simulateMediaAction(tab, "pause");
+
+ info(`default action handler should pause media`);
+ await checkOrWaitUntilMediaPauses(tab, { videoId });
+
+ info(`test 'play' action`);
+ await simulateMediaAction(tab, "play");
+
+ info(`default action handler should resume media`);
+ await checkOrWaitUntilMediaPlays(tab, { videoId });
+
+ info(`test 'stop' action`);
+ await simulateMediaAction(tab, "stop");
+
+ info(`default action handler should pause media`);
+ await checkOrWaitUntilMediaPauses(tab, { videoId });
+
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ ok(
+ !controller.isActive,
+ `controller should be deactivated after receiving stop`
+ );
+
+ info(`remove tab`);
+ await tab.close();
+ }
+});
+
+add_task(async function triggerNonDefaultHandlerWhenSetCustomizedHandler() {
+ info(`open page and start media`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ await Promise.all([
+ new Promise(r => (tab.controller.onactivated = r)),
+ startMedia(tab, { videoId }),
+ ]);
+
+ const kActions = ["play", "pause", "stop"];
+ for (const action of kActions) {
+ info(`set action handler for '${action}'`);
+ await setActionHandler(tab, action);
+
+ info(`press '${action}' should trigger action handler (not a default one)`);
+ await simulateMediaAction(tab, action);
+ await waitUntilActionHandlerIsTriggered(tab, action);
+
+ info(`action handler doesn't pause media, media should keep playing`);
+ await checkOrWaitUntilMediaPlays(tab, { videoId });
+ }
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(
+ async function triggerDefaultHandlerToPausePlaybackOnInactiveSession() {
+ const kIframeUrls = [IFRAME_URL, CORS_IFRAME_URL];
+ for (const url of kIframeUrls) {
+ const kActions = ["play", "pause", "stop"];
+ for (const action of kActions) {
+ info(`open page and load iframe`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ const frameId = "iframe";
+ await loadIframe(tab, frameId, url);
+
+ info(`start media from iframe would make it become active session`);
+ await Promise.all([
+ new Promise(r => (tab.controller.onactivated = r)),
+ startMedia(tab, { frameId }),
+ ]);
+
+ info(`press '${action}' should trigger iframe's action handler`);
+ await setActionHandler(tab, action, frameId);
+ await simulateMediaAction(tab, action);
+ await waitUntilActionHandlerIsTriggered(tab, action, frameId);
+
+ info(`start media from main frame so iframe would become inactive`);
+ // When action is `play`, controller is already playing, because above
+ // code won't pause media. So we need to wait for the active session
+ // changed to ensure the following tests can be executed on the right
+ // browsing context.
+ let waitForControllerStatusChanged =
+ action == "play"
+ ? waitUntilActiveMediaSessionChanged()
+ : ensureControllerIsPlaying(tab.controller);
+ await Promise.all([
+ waitForControllerStatusChanged,
+ startMedia(tab, { videoId }),
+ ]);
+
+ if (action == "play") {
+ info(`pause media first in order to test 'play'`);
+ await pauseAllMedia(tab);
+
+ info(
+ `press '${action}' would trigger default andler on main frame because it doesn't set action handler`
+ );
+ await simulateMediaAction(tab, action);
+ await checkOrWaitUntilMediaPlays(tab, { videoId });
+
+ info(
+ `default handler should also be triggered on inactive iframe, which would resume media`
+ );
+ await checkOrWaitUntilMediaPlays(tab, { frameId });
+ } else {
+ info(
+ `press '${action}' would trigger default andler on main frame because it doesn't set action handler`
+ );
+ await simulateMediaAction(tab, action);
+ await checkOrWaitUntilMediaPauses(tab, { videoId });
+
+ info(
+ `default handler should also be triggered on inactive iframe, which would pause media`
+ );
+ await checkOrWaitUntilMediaPauses(tab, { frameId });
+ }
+
+ info(`remove tab`);
+ await tab.close();
+ }
+ }
+ }
+);
+
+add_task(async function onlyResumeActiveMediaSession() {
+ info(`open page and load iframes`);
+ const tab = await createLoadedTabWrapper(PAGE2_URL);
+ const frame1Id = "frame1";
+ const frame2Id = "frame2";
+ await loadIframe(tab, frame1Id, CORS_IFRAME_URL);
+ await loadIframe(tab, frame2Id, CORS_IFRAME2_URL);
+
+ info(`start media from iframe1 would make it become active session`);
+ await createMediaSession(tab, frame1Id);
+ await Promise.all([
+ waitUntilActiveMediaSessionChanged(),
+ startMedia(tab, { frameId: frame1Id }),
+ ]);
+
+ info(`start media from iframe2 would make it become active session`);
+ await createMediaSession(tab, frame2Id);
+ await Promise.all([
+ waitUntilActiveMediaSessionChanged(),
+ startMedia(tab, { frameId: frame2Id }),
+ ]);
+
+ info(`press 'pause' should pause both iframes`);
+ await simulateMediaAction(tab, "pause");
+ await checkOrWaitUntilMediaPauses(tab, { frameId: frame1Id });
+ await checkOrWaitUntilMediaPauses(tab, { frameId: frame2Id });
+
+ info(
+ `press 'play' should only resume iframe2 which has active media session`
+ );
+ await simulateMediaAction(tab, "play");
+ await checkOrWaitUntilMediaPauses(tab, { frameId: frame1Id });
+ await checkOrWaitUntilMediaPlays(tab, { frameId: frame2Id });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function startMedia(tab, { videoId, frameId }) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [videoId, frameId],
+ (videoId, frameId) => {
+ if (frameId) {
+ return content.messageHelper(
+ content.document.getElementById(frameId),
+ "play",
+ "played"
+ );
+ }
+ return content.document.getElementById(videoId).play();
+ }
+ );
+}
+
+function pauseAllMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.messageHelper(
+ content.document.getElementById("iframe"),
+ "pause",
+ "paused"
+ );
+ const videos = content.document.getElementsByTagName("video");
+ for (let video of videos) {
+ video.pause();
+ }
+ });
+}
+
+function createMediaSession(tab, frameId = null) {
+ info(`create media session`);
+ return SpecialPowers.spawn(tab.linkedBrowser, [frameId], async frameId => {
+ if (frameId) {
+ await content.messageHelper(
+ content.document.getElementById(frameId),
+ "create-media-session",
+ "created-media-session"
+ );
+ return;
+ }
+ // simply calling a media session would create an instance.
+ content.navigator.mediaSession;
+ });
+}
+
+function checkOrWaitUntilMediaPauses(tab, { videoId, frameId }) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [videoId, frameId],
+ (videoId, frameId) => {
+ if (frameId) {
+ return content.messageHelper(
+ content.document.getElementById(frameId),
+ "check-pause",
+ "checked-pause"
+ );
+ }
+ return new Promise(r => {
+ const video = content.document.getElementById(videoId);
+ if (video.paused) {
+ ok(true, `media stopped playing`);
+ r();
+ } else {
+ info(`wait until media stops playing`);
+ video.onpause = () => {
+ video.onpause = null;
+ ok(true, `media stopped playing`);
+ r();
+ };
+ }
+ });
+ }
+ );
+}
+
+function checkOrWaitUntilMediaPlays(tab, { videoId, frameId }) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [videoId, frameId],
+ (videoId, frameId) => {
+ if (frameId) {
+ return content.messageHelper(
+ content.document.getElementById(frameId),
+ "check-playing",
+ "checked-playing"
+ );
+ }
+ return new Promise(r => {
+ const video = content.document.getElementById(videoId);
+ if (!video.paused) {
+ ok(true, `media is playing`);
+ r();
+ } else {
+ info(`wait until media starts playing`);
+ video.onplay = () => {
+ video.onplay = null;
+ ok(true, `media starts playing`);
+ r();
+ };
+ }
+ });
+ }
+ );
+}
+
+function setActionHandler(tab, action, frameId = null) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [action, frameId],
+ async (action, frameId) => {
+ if (frameId) {
+ await content.messageHelper(
+ content.document.getElementById(frameId),
+ {
+ cmd: "setActionHandler",
+ action,
+ },
+ "setActionHandler-done"
+ );
+ return;
+ }
+ // Create this on the first function call
+ if (content.actionHandlerPromises === undefined) {
+ content.actionHandlerPromises = {};
+ }
+ content.actionHandlerPromises[action] = new Promise(r => {
+ content.navigator.mediaSession.setActionHandler(action, () => {
+ info(`receive ${action}`);
+ r();
+ });
+ });
+ }
+ );
+}
+
+async function waitUntilActionHandlerIsTriggered(tab, action, frameId = null) {
+ info(`wait until '${action}' action handler is triggered`);
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [action, frameId],
+ (action, frameId) => {
+ if (frameId) {
+ return content.messageHelper(
+ content.document.getElementById(frameId),
+ {
+ cmd: "checkActionHandler",
+ action,
+ },
+ "checkActionHandler-done"
+ );
+ }
+ const actionTriggerPromise = content.actionHandlerPromises[action];
+ ok(actionTriggerPromise, `Has created promise for ${action}`);
+ return actionTriggerPromise;
+ }
+ );
+}
+
+async function simulateMediaAction(tab, action) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ if (!controller.isActive) {
+ await new Promise(r => (controller.onactivated = r));
+ }
+ MediaControlService.generateMediaControlKey(action);
+}
+
+function loadIframe(tab, iframeId, url) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [iframeId, url],
+ async (iframeId, url) => {
+ const iframe = content.document.getElementById(iframeId);
+ info(`load iframe with url '${url}'`);
+ iframe.src = url;
+ await new Promise(r => (iframe.onload = r));
+ // create a helper to simplify communication process with iframe
+ content.messageHelper = (target, sentMessage, expectedResponse) => {
+ target.contentWindow.postMessage(sentMessage, "*");
+ return new Promise(r => {
+ content.onmessage = event => {
+ if (event.data == expectedResponse) {
+ ok(true, `Received response ${expectedResponse}`);
+ content.onmessage = null;
+ r();
+ }
+ };
+ });
+ };
+ }
+ );
+}
+
+function waitUntilActiveMediaSessionChanged() {
+ return BrowserUtils.promiseObserved("active-media-session-changed");
+}
+
+function ensureControllerIsPlaying(controller) {
+ return new Promise(r => {
+ if (controller.isPlaying) {
+ r();
+ return;
+ }
+ controller.onplaybackstatechange = () => {
+ if (controller.isPlaying) {
+ r();
+ }
+ };
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_audio_focus_within_a_page.js b/dom/media/mediacontrol/tests/browser/browser_media_control_audio_focus_within_a_page.js
new file mode 100644
index 0000000000..5db37059dd
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_audio_focus_within_a_page.js
@@ -0,0 +1,358 @@
+const mainPageURL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html";
+const frameURL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+
+const frame1 = "frame1";
+const frame2 = "frame2";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test is used to check the behaviors when we play media from different
+ * frames. When a page contains multiple frames, if those frames are using the
+ * media session and set the metadata, then we have to know which frame owns the
+ * audio focus that would be the last tab playing media. When the frame owns
+ * audio focus, it means its metadata would be displayed on the virtual control
+ * interface if it has a media session.
+ */
+add_task(async function testAudioFocusChangesAmongMultipleFrames() {
+ /**
+ * Play the media from the main frame, so it would own the audio focus and
+ * its metadata should be shown on the virtual control interface. As the main
+ * frame doesn't use media session, the current metadata would be the default
+ * metadata.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await playAndWaitUntilMetadataChanged(tab);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Play media for frame1, so the audio focus switches to frame1 because it's
+ * the last tab playing media and frame1's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadata = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Play media for frame2, so the audio focus switches to frame2 because it's
+ * the last tab playing media and frame2's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame2, frameURL);
+ metadata = await setMetadataAndGetReturnResult(tab, frame2);
+ await playAndWaitUntilMetadataChanged(tab, frame2);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+});
+
+add_task(async function testAudioFocusChangesAfterPausingAudioFocusOwner() {
+ /**
+ * Play the media from the main frame, so it would own the audio focus and
+ * its metadata should be shown on the virtual control interface. As the main
+ * frame doesn't use media session, the current metadata would be the default
+ * metadata.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await playAndWaitUntilMetadataChanged(tab);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Play media for frame1, so the audio focus switches to frame1 because it's
+ * the last tab playing media and frame1's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadata = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Pause media for frame1, so the audio focus switches back to the main frame
+ * which is still playing media.
+ */
+ await pauseAndWaitUntilMetadataChangedFrom(tab, frame1);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+});
+
+add_task(async function testAudioFocusUnchangesAfterPausingAudioFocusOwner() {
+ /**
+ * Play the media from the main frame, so it would own the audio focus and
+ * its metadata should be shown on the virtual control interface. As the main
+ * frame doesn't use media session, the current metadata would be the default
+ * metadata.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await playAndWaitUntilMetadataChanged(tab);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Play media for frame1, so the audio focus switches to frame1 because it's
+ * the last tab playing media and frame1's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadata = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Pause main frame's media first. When pausing frame1's media, there are not
+ * other frames playing media, so frame1 still owns the audio focus and its
+ * metadata should be displayed.
+ */
+ await pauseMediaFrom(tab);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+});
+
+add_task(
+ async function testSwitchAudioFocusToMainFrameAfterRemovingAudioFocusOwner() {
+ /**
+ * Play the media from the main frame, so it would own the audio focus and
+ * its metadata should be displayed on the virtual control interface. As the
+ * main frame doesn't use media session, the current metadata would be the
+ * default metadata.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await playAndWaitUntilMetadataChanged(tab);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Play media for frame1, so the audio focus switches to frame1 because it's
+ * the last tab playing media and frame1's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadata = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Remove frame1, the audio focus would switch to the main frame which
+ * metadata should be displayed.
+ */
+ await Promise.all([
+ waitUntilDisplayedMetadataChanged(),
+ removeFrame(tab, frame1),
+ ]);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+ }
+);
+
+add_task(
+ async function testSwitchAudioFocusToIframeAfterRemovingAudioFocusOwner() {
+ /**
+ * Play media for frame1, so frame1 owns the audio focus and frame1's metadata
+ * should be displayed.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadataFrame1 = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadataFrame1);
+
+ /**
+ * Play media for frame2, so the audio focus switches to frame2 because it's
+ * the last tab playing media and frame2's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame2, frameURL);
+ let metadataFrame2 = await setMetadataAndGetReturnResult(tab, frame2);
+ await playAndWaitUntilMetadataChanged(tab, frame2);
+ isCurrentMetadataEqualTo(metadataFrame2);
+
+ /**
+ * Remove frame2, the audio focus would switch to frame1 which metadata should
+ * be displayed.
+ */
+ await Promise.all([
+ waitUntilDisplayedMetadataChanged(),
+ removeFrame(tab, frame2),
+ ]);
+ isCurrentMetadataEqualTo(metadataFrame1);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+ }
+);
+
+add_task(async function testNoAudioFocusAfterRemovingAudioFocusOwner() {
+ /**
+ * Play the media from the main frame, so it would own the audio focus and
+ * its metadata should be shown on the virtual control interface. As the main
+ * frame doesn't use media session, the current metadata would be the default
+ * metadata.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await playAndWaitUntilMetadataChanged(tab);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Play media for frame1, so the audio focus switches to frame1 because it's
+ * the last tab playing media and frame1's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadata = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Pause media in main frame and then remove frame1. As the frame which owns
+ * the audio focus is removed and no frame is still playing media, the current
+ * metadata would be the default metadata.
+ */
+ await pauseMediaFrom(tab);
+ await Promise.all([
+ waitUntilDisplayedMetadataChanged(),
+ removeFrame(tab, frame1),
+ ]);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function loadPageForFrame(tab, frameId, pageUrl) {
+ info(`start to load page for ${frameId}`);
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [frameId, pageUrl],
+ async (id, url) => {
+ const iframe = content.document.getElementById(id);
+ if (!iframe) {
+ ok(false, `can not get iframe '${id}'`);
+ }
+ iframe.src = url;
+ await new Promise(r => (iframe.onload = r));
+ // Set the document title that would be used as the value for properties
+ // in frame's medadata.
+ iframe.contentDocument.title = id;
+ }
+ );
+}
+
+function playMediaFrom(tab, frameId = undefined) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => {
+ if (id == undefined) {
+ info(`start to play media from main frame`);
+ const video = content.document.getElementById("video");
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ return video.play();
+ }
+
+ info(`start to play media from ${id}`);
+ const iframe = content.document.getElementById(id);
+ if (!iframe) {
+ ok(false, `can not get ${id}`);
+ }
+ iframe.contentWindow.postMessage("play", "*");
+ return new Promise(r => {
+ content.onmessage = event => {
+ is(event.data, "played", `media started playing in ${id}`);
+ r();
+ };
+ });
+ });
+}
+
+function playAndWaitUntilMetadataChanged(tab, frameId = undefined) {
+ const metadataChanged = frameId
+ ? new Promise(r => (tab.controller.onmetadatachange = r))
+ : waitUntilDisplayedMetadataChanged();
+ return Promise.all([metadataChanged, playMediaFrom(tab, frameId)]);
+}
+
+function pauseMediaFrom(tab, frameId = undefined) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => {
+ if (id == undefined) {
+ info(`start to pause media from in frame`);
+ const video = content.document.getElementById("video");
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ return video.pause();
+ }
+
+ info(`start to pause media in ${id}`);
+ const iframe = content.document.getElementById(id);
+ if (!iframe) {
+ ok(false, `can not get ${id}`);
+ }
+ iframe.contentWindow.postMessage("pause", "*");
+ return new Promise(r => {
+ content.onmessage = event => {
+ is(event.data, "paused", `media paused in ${id}`);
+ r();
+ };
+ });
+ });
+}
+
+function pauseAndWaitUntilMetadataChangedFrom(tab, frameId = undefined) {
+ const metadataChanged = waitUntilDisplayedMetadataChanged();
+ return Promise.all([metadataChanged, pauseMediaFrom(tab, frameId)]);
+}
+
+function setMetadataAndGetReturnResult(tab, frameId) {
+ info(`start to set metadata for ${frameId}`);
+ return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => {
+ const iframe = content.document.getElementById(id);
+ if (!iframe) {
+ ok(false, `can not get ${id}`);
+ }
+ iframe.contentWindow.postMessage("setMetadata", "*");
+ info(`wait until we get metadata for ${id}`);
+ return new Promise(r => {
+ content.onmessage = event => {
+ ok(
+ event.data.title && event.data.artist && event.data.album,
+ "correct return format"
+ );
+ r(event.data);
+ };
+ });
+ });
+}
+
+function removeFrame(tab, frameId) {
+ info(`remove ${frameId}`);
+ return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => {
+ const iframe = content.document.getElementById(id);
+ if (!iframe) {
+ ok(false, `can not get ${id}`);
+ }
+ content.document.body.removeChild(iframe);
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_before_media_starts.js b/dom/media/mediacontrol/tests/browser/browser_media_control_before_media_starts.js
new file mode 100644
index 0000000000..292c2f521f
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_before_media_starts.js
@@ -0,0 +1,205 @@
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const IFRAME_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ],
+ });
+});
+
+/**
+ * Usually we would only start controlling media after media starts, but if
+ * media has entered Picture-in-Picture mode or fullscreen, then we would allow
+ * users to control them directly without prior to starting media.
+ */
+add_task(async function testMediaEntersPIPMode() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`trigger PIP mode`);
+ const winPIP = await triggerPictureInPicture(tab.linkedBrowser, testVideoId);
+
+ info(`press 'play' and wait until media starts`);
+ await generateMediaControlKeyEvent("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ await tab.close();
+});
+
+add_task(async function testMutedMediaEntersPIPMode() {
+ info(`open media page and mute video`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ await muteMedia(tab, testVideoId);
+
+ info(`trigger PIP mode`);
+ const winPIP = await triggerPictureInPicture(tab.linkedBrowser, testVideoId);
+
+ info(`press 'play' and wait until media starts`);
+ await generateMediaControlKeyEvent("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ await tab.close();
+});
+
+add_task(async function testMediaEntersFullScreen() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`make video fullscreen`);
+ await enableFullScreen(tab, testVideoId);
+
+ info(`press 'play' and wait until media starts`);
+ await generateMediaControlKeyEvent("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testMutedMediaEntersFullScreen() {
+ info(`open media page and mute video`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ await muteMedia(tab, testVideoId);
+
+ info(`make video fullscreen`);
+ await enableFullScreen(tab, testVideoId);
+
+ info(`press 'play' and wait until media starts`);
+ await generateMediaControlKeyEvent("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testNonMediaEntersFullScreen() {
+ info(`open media page which won't have an activated controller`);
+ // As we won't activate controller in this test case, not need to
+ // check controller's status.
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY, {
+ needCheck: false,
+ });
+
+ info(`make non-media element fullscreen`);
+ const nonMediaElementId = "image";
+ await enableFullScreen(tab, nonMediaElementId);
+
+ info(`press 'play' which should not start media`);
+ // Use `generateMediaControlKey()` directly because `play` won't affect the
+ // controller's playback state (don't need to wait for the change).
+ MediaControlService.generateMediaControlKey("play");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testMediaInIframeEntersFullScreen() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`make video in iframe fullscreen`);
+ await enableMediaFullScreenInIframe(tab, testVideoId);
+
+ info(`press 'play' and wait until media starts`);
+ await generateMediaControlKeyEvent("play");
+
+ info(`full screen media in inframe should be started`);
+ await waitUntilIframeMediaStartedPlaying(tab);
+
+ info(`media not in fullscreen should keep paused`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`remove iframe that contains fullscreen video`);
+ await removeIframeFromDocument(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function muteMedia(tab, videoId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [videoId], videoId => {
+ content.document.getElementById(videoId).muted = true;
+ });
+}
+
+function enableFullScreen(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], elementId => {
+ return new Promise(r => {
+ const element = content.document.getElementById(elementId);
+ element.requestFullscreen();
+ element.onfullscreenchange = () => {
+ element.onfullscreenchange = null;
+ element.onfullscreenerror = null;
+ r();
+ };
+ element.onfullscreenerror = () => {
+ // Retry until the element successfully enters fullscreen.
+ element.requestFullscreen();
+ };
+ });
+ });
+}
+
+function enableMediaFullScreenInIframe(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [IFRAME_URL], async url => {
+ info(`create iframe and wait until it finishes loading`);
+ const iframe = content.document.getElementById("iframe");
+ iframe.src = url;
+ await new Promise(r => (iframe.onload = r));
+
+ info(`trigger media in iframe entering into fullscreen`);
+ iframe.contentWindow.postMessage("fullscreen", "*");
+ info(`wait until media in frame enters fullscreen`);
+ return new Promise(r => {
+ content.onmessage = event => {
+ is(
+ event.data,
+ "entered-fullscreen",
+ `media in iframe entered fullscreen`
+ );
+ r();
+ };
+ });
+ });
+}
+
+function waitUntilIframeMediaStartedPlaying(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [IFRAME_URL], async url => {
+ info(`check if media in iframe starts playing`);
+ const iframe = content.document.getElementById("iframe");
+ iframe.contentWindow.postMessage("check-playing", "*");
+ return new Promise(r => {
+ content.onmessage = event => {
+ is(event.data, "checked-playing", `media in iframe is playing`);
+ r();
+ };
+ });
+ });
+}
+
+function removeIframeFromDocument(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ info(`remove iframe from document`);
+ content.document.getElementById("iframe").remove();
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_captured_audio.js b/dom/media/mediacontrol/tests/browser/browser_media_control_captured_audio.js
new file mode 100644
index 0000000000..0da8acd62b
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_captured_audio.js
@@ -0,0 +1,45 @@
+const PAGE_NON_AUTOPLAY_MEDIA =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * When we capture audio from an media element to the web audio, if the media
+ * is audible, it should be controlled by media keys as well.
+ */
+add_task(async function testAudibleCapturedMedia() {
+ info(`open new non autoplay media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY_MEDIA);
+
+ info(`capture audio and start playing`);
+ await captureAudio(tab, testVideoId);
+ await playMedia(tab, testVideoId);
+
+ info(`pressing 'pause' key, captured media should be paused`);
+ await generateMediaControlKeyEvent("pause");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function captureAudio(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ const context = new content.AudioContext();
+ // Capture audio from the media element to a MediaElementAudioSourceNode.
+ context.createMediaElementSource(video);
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_keys_event.js b/dom/media/mediacontrol/tests/browser/browser_media_control_keys_event.js
new file mode 100644
index 0000000000..a8525e61c5
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_keys_event.js
@@ -0,0 +1,62 @@
+const PAGE =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const testVideoId = "video";
+
+/**
+ * This test is used to generate platform-independent media control keys event
+ * and see if media can be controlled correctly and current we only support
+ * `play`, `pause`, `playPause` and `stop` events.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+add_task(async function testPlayPauseAndStop() {
+ info(`open page and start media`);
+ const tab = await createLoadedTabWrapper(PAGE);
+ await playMedia(tab, testVideoId);
+
+ info(`pressing 'pause' key`);
+ MediaControlService.generateMediaControlKey("pause");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`pressing 'play' key`);
+ MediaControlService.generateMediaControlKey("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`pressing 'stop' key`);
+ MediaControlService.generateMediaControlKey("stop");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`Has stopped controlling media, pressing 'play' won't resume it`);
+ MediaControlService.generateMediaControlKey("play");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testPlayPause() {
+ info(`open page and start media`);
+ const tab = await createLoadedTabWrapper(PAGE);
+ await playMedia(tab, testVideoId);
+
+ info(`pressing 'playPause' key, media should stop`);
+ MediaControlService.generateMediaControlKey("playpause");
+ await Promise.all([
+ new Promise(r => (tab.controller.onplaybackstatechange = r)),
+ checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId),
+ ]);
+
+ info(`pressing 'playPause' key, media should start`);
+ MediaControlService.generateMediaControlKey("playpause");
+ await Promise.all([
+ new Promise(r => (tab.controller.onplaybackstatechange = r)),
+ checkOrWaitUntilMediaStartedPlaying(tab, testVideoId),
+ ]);
+
+ info(`remove tab`);
+ await tab.close();
+});
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_main_controller.js b/dom/media/mediacontrol/tests/browser/browser_media_control_main_controller.js
new file mode 100644
index 0000000000..9ddd3d7a18
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_main_controller.js
@@ -0,0 +1,341 @@
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test is used to check in different situaition if we can determine the
+ * main controller correctly that is the controller which can receive media
+ * control keys and show its metadata on the virtual control interface.
+ *
+ * We will assign different metadata for each tab and know which tab is the main
+ * controller by checking main controller's metadata.
+ *
+ * We will always choose the last tab which plays media as the main controller,
+ * and maintain a list by the order of playing media. If the top element in the
+ * list has been removed, then we will use the last element in the list as the
+ * main controller.
+ *
+ * Eg. tab1 plays first, then tab2 plays, then tab3 plays, the list would be
+ * like [tab1, tab2, tab3] and the main controller would be tab3. If tab3 has
+ * been closed, then the list would become [tab1, tab2] and the tab2 would be
+ * the main controller.
+ */
+add_task(async function testDeterminingMainController() {
+ info(`open three different tabs`);
+ const tab0 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab1 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab2 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ /**
+ * part1 : [] -> [tab0] -> [tab0, tab1] -> [tab0, tab1, tab2]
+ */
+ info(`# [] -> [tab0] -> [tab0, tab1] -> [tab0, tab1, tab2] #`);
+ info(`set different metadata for each tab`);
+ await setMediaMetadataForTabs([tab0, tab1, tab2]);
+
+ info(`start media for tab0, main controller should become tab0`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab0);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`start media for tab1, main controller should become tab1`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab1);
+
+ info(`currrent metadata should be equal to tab1's metadata`);
+ await isCurrentMetadataEqualTo(tab1.metadata);
+
+ info(`start media for tab2, main controller should become tab2`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab2);
+
+ info(`currrent metadata should be equal to tab2's metadata`);
+ await isCurrentMetadataEqualTo(tab2.metadata);
+
+ /**
+ * part2 : [tab0, tab1, tab2] -> [tab0, tab2, tab1] -> [tab2, tab1, tab0]
+ */
+ info(`# [tab0, tab1, tab2] -> [tab0, tab2, tab1] -> [tab2, tab1, tab0] #`);
+ info(`start media for tab1, main controller should become tab1`);
+ await makeTabBecomeMainController(tab1);
+
+ info(`currrent metadata should be equal to tab1's metadata`);
+ await isCurrentMetadataEqualTo(tab1.metadata);
+
+ info(`start media for tab0, main controller should become tab0`);
+ await makeTabBecomeMainController(tab0);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ /**
+ * part3 : [tab2, tab1, tab0] -> [tab2, tab1] -> [tab2] -> []
+ */
+ info(`# [tab2, tab1, tab0] -> [tab2, tab1] -> [tab2] -> [] #`);
+ info(`remove tab0 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab0.close()]);
+
+ info(`currrent metadata should be equal to tab1's metadata`);
+ await isCurrentMetadataEqualTo(tab1.metadata);
+
+ info(`remove tab1 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab1.close()]);
+
+ info(`currrent metadata should be equal to tab2's metadata`);
+ await isCurrentMetadataEqualTo(tab2.metadata);
+
+ info(`remove tab2 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab2.close()]);
+ isCurrentMetadataEmpty();
+});
+
+add_task(async function testPIPControllerWontBeReplacedByNormalController() {
+ info(`open two different tabs`);
+ const tab0 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab1 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`set different metadata for each tab`);
+ await setMediaMetadataForTabs([tab0, tab1]);
+
+ info(`start media for tab0, main controller should become tab0`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab0);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`trigger Picture-in-Picture mode for tab0`);
+ const winPIP = await triggerPictureInPicture(tab0.linkedBrowser, testVideoId);
+
+ info(`start media for tab1, main controller should still be tab0`);
+ await playMediaAndWaitUntilRegisteringController(tab1, testVideoId);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`remove tab0 and wait until main controller changes`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab0.close()]);
+
+ info(`currrent metadata should be equal to tab1's metadata`);
+ await isCurrentMetadataEqualTo(tab1.metadata);
+
+ info(`remove tab1 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab1.close()]);
+ isCurrentMetadataEmpty();
+});
+
+add_task(
+ async function testFullscreenControllerWontBeReplacedByNormalController() {
+ info(`open two different tabs`);
+ const tab0 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab1 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`set different metadata for each tab`);
+ await setMediaMetadataForTabs([tab0, tab1]);
+
+ info(`start media for tab0, main controller should become tab0`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab0);
+
+ info(`current metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`video in tab0 enters fullscreen`);
+ await switchTabToForegroundAndEnableFullScreen(tab0, testVideoId);
+
+ info(
+ `normal controller won't become the main controller, ` +
+ `which is still fullscreen controller`
+ );
+ await playMediaAndWaitUntilRegisteringController(tab1, testVideoId);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`remove tabs`);
+ await Promise.all([tab0.close(), tab1.close()]);
+ }
+);
+
+add_task(async function testFullscreenAndPIPControllers() {
+ info(`open three different tabs`);
+ const tab0 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab1 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab2 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`set different metadata for each tab`);
+ await setMediaMetadataForTabs([tab0, tab1, tab2]);
+
+ /**
+ * Current controller list : [tab0 (fullscreen)]
+ */
+ info(`start media for tab0, main controller should become tab0`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab0);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`video in tab0 enters fullscreen`);
+ await switchTabToForegroundAndEnableFullScreen(tab0, testVideoId);
+
+ /**
+ * Current controller list : [tab1, tab0 (fullscreen)]
+ */
+ info(`start media for tab1, main controller should still be tab0`);
+ await playMediaAndWaitUntilRegisteringController(tab1, testVideoId);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ /**
+ * Current controller list : [tab0 (fullscreen), tab1 (PIP)]
+ */
+ info(`tab1 enters PIP so tab1 should become new main controller`);
+ const mainControllerChange = waitUntilMainMediaControllerChanged();
+ const winPIP = await triggerPictureInPicture(tab1.linkedBrowser, testVideoId);
+ await mainControllerChange;
+
+ info(`currrent metadata should be equal to tab1's metadata`);
+ await isCurrentMetadataEqualTo(tab1.metadata);
+
+ /**
+ * Current controller list : [tab2, tab0 (fullscreen), tab1 (PIP)]
+ */
+ info(`play video from tab2 which shouldn't affect main controller`);
+ await playMediaAndWaitUntilRegisteringController(tab2, testVideoId);
+
+ /**
+ * Current controller list : [tab2, tab0 (fullscreen)]
+ */
+ info(`remove tab1 and wait until main controller changes`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab1.close()]);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ /**
+ * Current controller list : [tab2]
+ */
+ info(`remove tab0 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab0.close()]);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab2.metadata);
+
+ /**
+ * Current controller list : []
+ */
+ info(`remove tab2 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab2.close()]);
+ isCurrentMetadataEmpty();
+});
+
+/**
+ * The following are helper functions
+ */
+async function setMediaMetadataForTabs(tabs) {
+ for (let idx = 0; idx < tabs.length; idx++) {
+ const tabName = "tab" + idx;
+ info(`create metadata for ${tabName}`);
+ tabs[idx].metadata = {
+ title: tabName,
+ artist: tabName,
+ album: tabName,
+ artwork: [{ src: tabName, sizes: "128x128", type: "image/jpeg" }],
+ };
+ const spawn = SpecialPowers.spawn(
+ tabs[idx].linkedBrowser,
+ [tabs[idx].metadata],
+ data => {
+ content.navigator.mediaSession.metadata = new content.MediaMetadata(
+ data
+ );
+ }
+ );
+ // As those controller hasn't been activated yet, we can't listen to
+ // `mediacontroll.onmetadatachange`, which would only be notified after a
+ // controller becomes active.
+ await Promise.all([spawn, waitUntilControllerMetadataChanged()]);
+ }
+}
+
+function makeTabBecomeMainController(tab) {
+ const playPromise = SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [testVideoId],
+ async Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ // If media has been started, we would stop media first and then start it
+ // again, which would make controller's playback state change to `playing`
+ // again and result in updating new main controller.
+ if (!video.paused) {
+ video.pause();
+ info(`wait until media stops`);
+ await new Promise(r => (video.onpause = r));
+ }
+ info(`start media`);
+ return video.play();
+ }
+ );
+ return Promise.all([playPromise, waitUntilMainMediaControllerChanged()]);
+}
+
+function makeTabBecomeMainControllerAndWaitForMetadataChange(tab) {
+ return Promise.all([
+ new Promise(r => (tab.controller.onmetadatachange = r)),
+ makeTabBecomeMainController(tab),
+ ]);
+}
+
+function playMediaAndWaitUntilRegisteringController(tab, elementId) {
+ const playPromise = SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [elementId],
+ Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ return video.play();
+ }
+ );
+ return Promise.all([waitUntilMediaControllerAmountChanged(), playPromise]);
+}
+
+async function switchTabToForegroundAndEnableFullScreen(tab, elementId) {
+ // Fullscreen can only be allowed to enter from a focus tab.
+ await BrowserTestUtils.switchTab(gBrowser, tab.tabElement);
+ await SpecialPowers.spawn(tab.linkedBrowser, [elementId], elementId => {
+ return new Promise(r => {
+ const element = content.document.getElementById(elementId);
+ element.requestFullscreen();
+ element.onfullscreenchange = () => {
+ element.onfullscreenchange = null;
+ element.onfullscreenerror = null;
+ r();
+ };
+ element.onfullscreenerror = () => {
+ // Retry until the element successfully enters fullscreen.
+ element.requestFullscreen();
+ };
+ });
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_metadata.js b/dom/media/mediacontrol/tests/browser/browser_media_control_metadata.js
new file mode 100644
index 0000000000..75a0d80ac9
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_metadata.js
@@ -0,0 +1,416 @@
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const PAGE_EMPTY_TITLE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_empty_title.html";
+
+const testVideoId = "video";
+const defaultFaviconName = "defaultFavicon.svg";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test includes many different test cases of checking the current media
+ * metadata from the tab which is being controlled by the media control. Each
+ * `add_task(..)` is a different testing scenario.
+ *
+ * Media metadata is the information that can tell user about what title, artist,
+ * album and even art work the tab is currently playing to. The metadta is
+ * usually set from MediaSession API, but if the site doesn't use that API, we
+ * would also generate a default metadata instead.
+ *
+ * The current metadata would only be available after the page starts playing
+ * media at least once, if the page hasn't started any media before, then the
+ * current metadata is always empty.
+ *
+ * For following situations, we would create a default metadata which title is
+ * website's title and artwork is from our default favicon icon.
+ * (1) the page doesn't use MediaSession API
+ * (2) media session doesn't has metadata
+ * (3) media session has an empty metadata
+ *
+ * Otherwise, the current metadata would be media session's metadata from the
+ * tab which is currently controlled by the media control.
+ */
+add_task(async function testDefaultMetadataForPageWithoutMediaSession() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`should use default metadata because of lacking of media session`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(
+ async function testDefaultMetadataForEmptyTitlePageWithoutMediaSession() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_EMPTY_TITLE_URL);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`should use default metadata because of lacking of media session`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+ }
+);
+
+add_task(async function testDefaultMetadataForPageUsingEmptyMetadata() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create empty media metadata`);
+ await setMediaMetadata(tab, {
+ title: "",
+ artist: "",
+ album: "",
+ artwork: [],
+ });
+
+ info(`should use default metadata because of empty media metadata`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testDefaultMetadataForPageUsingNullMetadata() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create empty media metadata`);
+ await setNullMediaMetadata(tab);
+
+ info(`should use default metadata because of lacking of media metadata`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testMetadataWithEmptyTitleAndArtwork() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create media metadata with empty title and artwork`);
+ await setMediaMetadata(tab, {
+ title: "",
+ artist: "foo",
+ album: "bar",
+ artwork: [],
+ });
+
+ info(`should use default metadata because of empty title and artwork`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testMetadataWithoutTitleAndArtwork() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create media metadata with empty title and artwork`);
+ await setMediaMetadata(tab, {
+ artist: "foo",
+ album: "bar",
+ });
+
+ info(`should use default metadata because of lacking of title and artwork`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testMetadataInPrivateBrowsing() {
+ info(`create a private window`);
+ const inputWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY, { inputWindow });
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`set metadata`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+
+ info(`should use default metadata because of in private browsing mode`);
+ await isGivenTabUsingDefaultMetadata(tab, { isPrivateBrowsing: true });
+
+ info(`remove tab`);
+ await tab.close();
+
+ info(`close private window`);
+ await BrowserTestUtils.closeWindow(inputWindow);
+});
+
+add_task(async function testSetMetadataFromMediaSessionAPI() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`set metadata`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+
+ info(`check if current active metadata is equal to what we've set before`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`set metadata again to see if current metadata would change`);
+ metadata = {
+ title: "foo2",
+ artist: "bar2",
+ album: "foo2",
+ artwork: [{ src: "bar2.jpg", sizes: "129x129", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+
+ info(`check if current active metadata is equal to what we've set before`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSetMetadataBeforeMediaStarts() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY, {
+ needCheck: false,
+ });
+
+ info(`set metadata`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata, { notExpectChange: true });
+
+ info(`current media metadata should be empty before media starts`);
+ isCurrentMetadataEmpty();
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSetMetadataAfterMediaPaused() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media in order to let this tab be controlled`);
+ await playMedia(tab, testVideoId);
+
+ info(`pause media`);
+ await pauseMedia(tab, testVideoId);
+
+ info(`set metadata after media is paused`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+
+ info(`check if current active metadata is equal to what we've set before`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSetMetadataAmongMultipleTabs() {
+ info(`open media page in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media in tab1`);
+ await playMedia(tab1, testVideoId);
+
+ info(`set metadata for tab1`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab1, metadata);
+
+ info(`check if current active metadata is equal to what we've set before`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`open another page in tab2`);
+ const tab2 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media in tab2`);
+ await playMedia(tab2, testVideoId);
+
+ info(`set metadata for tab2`);
+ metadata = {
+ title: "foo2",
+ artist: "bar2",
+ album: "foo2",
+ artwork: [{ src: "bar2.jpg", sizes: "129x129", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab2, metadata);
+
+ info(`current active metadata should become metadata from tab2`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(
+ `update metadata for tab1, which should not affect current metadata ` +
+ `because media session in tab2 is the one we're controlling right now`
+ );
+ await setMediaMetadata(tab1, {
+ title: "foo3",
+ artist: "bar3",
+ album: "foo3",
+ artwork: [{ src: "bar3.jpg", sizes: "130x130", type: "image/jpeg" }],
+ });
+
+ info(`current active metadata should still be metadata from tab2`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`remove tabs`);
+ await Promise.all([tab1.close(), tab2.close()]);
+});
+
+add_task(async function testMetadataAfterTabNavigation() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`set metadata`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+
+ info(`check if current active metadata is equal to what we've set before`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`navigate tab to blank page`);
+ await Promise.all([
+ new Promise(r => (tab.controller.ondeactivated = r)),
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:blank"),
+ ]);
+
+ info(`current media metadata should be reset`);
+ isCurrentMetadataEmpty();
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testUpdateDefaultMetadataWhenPageTitleChanges() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`should use default metadata because of lacking of media session`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`default metadata should be updated after page title changes`);
+ await changePageTitle(tab, { shouldAffectMetadata: true });
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`after setting metadata, title change won't affect current metadata`);
+ const metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+ await changePageTitle(tab, { shouldAffectMetadata: false });
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function setMediaMetadata(tab, metadata, { notExpectChange } = {}) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ const metadatachangePromise = notExpectChange
+ ? Promise.resolve()
+ : new Promise(r => (controller.onmetadatachange = r));
+ return Promise.all([
+ metadatachangePromise,
+ SpecialPowers.spawn(tab.linkedBrowser, [metadata], data => {
+ content.navigator.mediaSession.metadata = new content.MediaMetadata(data);
+ }),
+ ]);
+}
+
+function setNullMediaMetadata(tab) {
+ const promise = SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.navigator.mediaSession.metadata = null;
+ });
+ return Promise.all([promise, waitUntilControllerMetadataChanged()]);
+}
+
+async function changePageTitle(tab, { shouldAffectMetadata } = {}) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ const shouldWaitMetadataChangePromise = shouldAffectMetadata
+ ? new Promise(r => (controller.onmetadatachange = r))
+ : Promise.resolve();
+ await Promise.all([
+ shouldWaitMetadataChangePromise,
+ SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content.document.title = "new title";
+ }),
+ ]);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_non_eligible_media.js b/dom/media/mediacontrol/tests/browser/browser_media_control_non_eligible_media.js
new file mode 100644
index 0000000000..ab18eab634
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_non_eligible_media.js
@@ -0,0 +1,204 @@
+const PAGE_NON_ELIGIBLE_MEDIA =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_eligible_media.html";
+
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+// Bug 1673509 - This test requests a lot of fullscreen for media elements,
+// which sometime Gecko would take longer time to fulfill.
+requestLongerTimeout(2);
+
+// This array contains the elements' id in `file_non_eligible_media.html`.
+const gNonEligibleElementIds = [
+ "muted",
+ "volume-0",
+ "silent-audio-track",
+ "no-audio-track",
+ "short-duration",
+ "inaudible-captured-media",
+];
+
+/**
+ * This test is used to test couples of things about what kinds of media is
+ * eligible for being controlled by media control keys.
+ * (1) If media is inaudible all the time, then we would not control it.
+ * (2) If media starts inaudibly, we would not try to control it. But once it
+ * becomes audible later, we would keep controlling it until it's destroyed.
+ * (3) If media's duration is too short (<3s), then we would not control it.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+add_task(
+ async function testNonAudibleMediaCantActivateControllerButAudibleMediaCan() {
+ for (const elementId of gNonEligibleElementIds) {
+ info(`open new tab with non eligible media elements`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_ELIGIBLE_MEDIA, {
+ needCheck: couldElementBecomeEligible(elementId),
+ });
+
+ info(`although media is playing but it won't activate controller`);
+ await Promise.all([
+ startNonEligibleMedia(tab, elementId),
+ checkIfMediaIsStillPlaying(tab, elementId),
+ ]);
+ ok(!tab.controller.isActive, "controller is still inactive");
+
+ if (couldElementBecomeEligible(elementId)) {
+ info(`make element ${elementId} audible would activate controller`);
+ await Promise.all([
+ makeElementEligible(tab, elementId),
+ checkOrWaitUntilControllerBecomeActive(tab),
+ ]);
+ }
+
+ info(`remove tab`);
+ await tab.close();
+ }
+ }
+);
+
+/**
+ * Normally those media are not able to being controlled, however, once they
+ * enter fullsceen or Picture-in-Picture mode, then they can be controlled.
+ */
+add_task(async function testNonEligibleMediaEnterFullscreen() {
+ info(`open new tab with non eligible media elements`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_ELIGIBLE_MEDIA);
+
+ for (const elementId of gNonEligibleElementIds) {
+ await startNonEligibleMedia(tab, elementId);
+
+ info(`entering fullscreen should activate the media controller`);
+ await enterFullScreen(tab, elementId);
+ await checkOrWaitUntilControllerBecomeActive(tab);
+ ok(true, `fullscreen ${elementId} media is able to being controlled`);
+
+ info(`leave fullscreen`);
+ await leaveFullScreen(tab);
+ }
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testNonEligibleMediaEnterPIPMode() {
+ info(`open new tab with non eligible media elements`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_ELIGIBLE_MEDIA);
+
+ for (const elementId of gNonEligibleElementIds) {
+ await startNonEligibleMedia(tab, elementId);
+
+ info(`media entering PIP mode should activate the media controller`);
+ const winPIP = await triggerPictureInPicture(tab.linkedBrowser, elementId);
+ await checkOrWaitUntilControllerBecomeActive(tab);
+ ok(true, `PIP ${elementId} media is able to being controlled`);
+
+ info(`stop PIP mode`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ }
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function startNonEligibleMedia(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ if (Id == "volume-0") {
+ video.volume = 0.0;
+ }
+ if (Id == "inaudible-captured-media") {
+ const context = new content.AudioContext();
+ context.createMediaElementSource(video);
+ }
+ info(`start non eligible media ${Id}`);
+ return video.play();
+ });
+}
+
+function checkIfMediaIsStillPlaying(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ return new Promise(r => {
+ // In order to test "media isn't affected by media control", we would not
+ // only check `mPaused`, we would also oberve "timeupdate" event multiple
+ // times to ensure that video is still playing continually.
+ let timeUpdateCount = 0;
+ ok(!video.paused);
+ video.ontimeupdate = () => {
+ if (++timeUpdateCount == 3) {
+ video.ontimeupdate = null;
+ r();
+ }
+ };
+ });
+ });
+}
+
+function couldElementBecomeEligible(elementId) {
+ return elementId == "muted" || elementId == "volume-0";
+}
+
+function makeElementEligible(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ // to turn inaudible media become audible in order to be controlled.
+ video.volume = 1.0;
+ video.muted = false;
+ });
+}
+
+function waitUntilMediaPaused(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ if (video.paused) {
+ ok(true, "media has been paused");
+ return Promise.resolve();
+ }
+ return new Promise(r => (video.onpaused = r));
+ });
+}
+
+function enterFullScreen(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ return new Promise(r => {
+ const element = content.document.getElementById(Id);
+ element.requestFullscreen();
+ element.onfullscreenchange = () => {
+ element.onfullscreenchange = null;
+ element.onfullscreenerror = null;
+ r();
+ };
+ element.onfullscreenerror = () => {
+ // Retry until the element successfully enters fullscreen.
+ element.requestFullscreen();
+ };
+ });
+ });
+}
+
+function leaveFullScreen(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ return content.document.exitFullscreen();
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_playback_state.js b/dom/media/mediacontrol/tests/browser/browser_media_control_playback_state.js
new file mode 100644
index 0000000000..4d30566e0a
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_playback_state.js
@@ -0,0 +1,116 @@
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test is used to check the actual playback state [1] of the main media
+ * controller. The declared playback state is the playback state from the active
+ * media session, and the guessed playback state is determined by the media's
+ * playback state. Both the declared playback and the guessed playback state
+ * would be used to decide the final result of the actual playback state.
+ *
+ * [1] https://w3c.github.io/mediasession/#actual-playback-state
+ */
+add_task(async function testDefaultPlaybackStateBeforeAnyMediaStart() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY, {
+ needCheck: false,
+ });
+
+ info(`before media starts, playback state should be 'none'`);
+ await isActualPlaybackStateEqualTo(tab, "none");
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testGuessedPlaybackState() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(
+ `Now declared='none', guessed='playing', so actual playback state should be 'playing'`
+ );
+ await setGuessedPlaybackState(tab, "playing");
+ await isActualPlaybackStateEqualTo(tab, "playing");
+
+ info(
+ `Now declared='none', guessed='paused', so actual playback state should be 'paused'`
+ );
+ await setGuessedPlaybackState(tab, "paused");
+ await isActualPlaybackStateEqualTo(tab, "paused");
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testBothGuessedAndDeclaredPlaybackState() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(
+ `Now declared='paused', guessed='playing', so actual playback state should be 'playing'`
+ );
+ await setDeclaredPlaybackState(tab, "paused");
+ await setGuessedPlaybackState(tab, "playing");
+ await isActualPlaybackStateEqualTo(tab, "playing");
+
+ info(
+ `Now declared='paused', guessed='paused', so actual playback state should be 'paused'`
+ );
+ await setGuessedPlaybackState(tab, "paused");
+ await isActualPlaybackStateEqualTo(tab, "paused");
+
+ info(
+ `Now declared='playing', guessed='paused', so actual playback state should be 'playing'`
+ );
+ await setDeclaredPlaybackState(tab, "playing");
+ await isActualPlaybackStateEqualTo(tab, "playing");
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function setGuessedPlaybackState(tab, state) {
+ if (state == "playing") {
+ return playMedia(tab, testVideoId);
+ } else if (state == "paused") {
+ return pauseMedia(tab, testVideoId);
+ }
+ // We won't set the state `stopped`, which would only happen if no any media
+ // has ever been started in the page.
+ ok(false, `should only set 'playing' or 'paused' state`);
+ return Promise.resolve();
+}
+
+async function isActualPlaybackStateEqualTo(tab, expectedState) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ if (controller.playbackState != expectedState) {
+ await new Promise(r => (controller.onplaybackstatechange = r));
+ }
+ is(
+ controller.playbackState,
+ expectedState,
+ `current state '${controller.playbackState}' is equal to '${expectedState}'`
+ );
+}
+
+function setDeclaredPlaybackState(tab, state) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [state], playbackState => {
+ info(`set declared playback state to '${playbackState}'`);
+ content.navigator.mediaSession.playbackState = playbackState;
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js b/dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js
new file mode 100644
index 0000000000..f32ce26063
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js
@@ -0,0 +1,150 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const IFRAME_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test is used to check if we can receive correct position state change,
+ * when we set the position state to the media session.
+ */
+add_task(async function testSetPositionState() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`set duration only`);
+ await setPositionState(tab, {
+ duration: 60,
+ });
+
+ info(`set duration and playback rate`);
+ await setPositionState(tab, {
+ duration: 50,
+ playbackRate: 2.0,
+ });
+
+ info(`set duration, playback rate and position`);
+ await setPositionState(tab, {
+ duration: 40,
+ playbackRate: 3.0,
+ position: 10,
+ });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSetPositionStateFromInactiveMediaSession() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(
+ `add an event listener to measure how many times the position state changes`
+ );
+ let positionChangedNum = 0;
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ controller.onpositionstatechange = () => positionChangedNum++;
+
+ info(`set position state on the main page which has an active media session`);
+ await setPositionState(tab, {
+ duration: 60,
+ });
+
+ info(`set position state on the iframe which has an inactive media session`);
+ await setPositionStateOnInactiveMediaSession(tab);
+
+ info(`set position state on the main page again`);
+ await setPositionState(tab, {
+ duration: 60,
+ });
+ is(
+ positionChangedNum,
+ 2,
+ `We should only receive two times of position change, because ` +
+ `the second one which performs on inactive media session is effectless`
+ );
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+async function setPositionState(tab, positionState) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ const positionStateChanged = new Promise(r => {
+ controller.addEventListener(
+ "positionstatechange",
+ event => {
+ const { duration, playbackRate, position } = positionState;
+ // duration is mandatory.
+ is(
+ event.duration,
+ duration,
+ `expected duration ${event.duration} is equal to ${duration}`
+ );
+
+ // Playback rate is optional, if it's not present, default should be 1.0
+ if (playbackRate) {
+ is(
+ event.playbackRate,
+ playbackRate,
+ `expected playbackRate ${event.playbackRate} is equal to ${playbackRate}`
+ );
+ } else {
+ is(event.playbackRate, 1.0, `expected default playbackRate is 1.0`);
+ }
+
+ // Position state is optional, if it's not present, default should be 0.0
+ if (position) {
+ is(
+ event.position,
+ position,
+ `expected position ${event.position} is equal to ${position}`
+ );
+ } else {
+ is(event.position, 0.0, `expected default position is 0.0`);
+ }
+ r();
+ },
+ { once: true }
+ );
+ });
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [positionState],
+ positionState => {
+ content.navigator.mediaSession.setPositionState(positionState);
+ }
+ );
+ await positionStateChanged;
+}
+
+async function setPositionStateOnInactiveMediaSession(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [IFRAME_URL], async url => {
+ info(`create iframe and wait until it finishes loading`);
+ const iframe = content.document.getElementById("iframe");
+ iframe.src = url;
+ await new Promise(r => (iframe.onload = r));
+
+ info(`trigger media in iframe entering into fullscreen`);
+ iframe.contentWindow.postMessage("setPositionState", "*");
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_seekto.js b/dom/media/mediacontrol/tests/browser/browser_media_control_seekto.js
new file mode 100644
index 0000000000..70b75841b6
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_seekto.js
@@ -0,0 +1,89 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test is used to check if the `seekto` action can be sent when calling
+ * media controller's `seekTo()`. In addition, the seeking related properties
+ * which would be sent to the action handler should also be correct as what we
+ * set in `seekTo()`.
+ */
+add_task(async function testSetPositionState() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ const seektime = 0;
+ info(`seek to ${seektime} seconds.`);
+ await PerformSeekTo(tab, {
+ seekTime: seektime,
+ });
+
+ info(`seek to ${seektime} seconds and set fastseek to boolean`);
+ await PerformSeekTo(tab, {
+ seekTime: seektime,
+ fastSeek: true,
+ });
+
+ info(`seek to ${seektime} seconds and set fastseek to false`);
+ await PerformSeekTo(tab, {
+ seekTime: seektime,
+ fastSeek: false,
+ });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following is helper function.
+ */
+async function PerformSeekTo(tab, seekDetails) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [seekDetails, testVideoId],
+ (seekDetails, Id) => {
+ const { seekTime, fastSeek } = seekDetails;
+ content.navigator.mediaSession.setActionHandler("seekto", details => {
+ ok(details.seekTime != undefined, "Seektime must be presented");
+ is(seekTime, details.seekTime, "Get correct seektime");
+ if (fastSeek) {
+ is(fastSeek, details.fastSeek, "Get correct fastSeek");
+ } else {
+ ok(
+ details.fastSeek === undefined,
+ "Details should not contain fastSeek"
+ );
+ }
+ // We use `onseek` as a hint to know if the `seekto` has been received
+ // or not. The reason we don't return a resolved promise instead is
+ // because if we do so, it can't guarantees that the `seekto` action
+ // handler has been set before calling `mediaController.seekTo`.
+ content.document.getElementById(Id).currentTime = seekTime;
+ });
+ }
+ );
+ const seekPromise = SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [testVideoId],
+ Id => {
+ const video = content.document.getElementById(Id);
+ return new Promise(r => (video.onseeking = r()));
+ }
+ );
+ const { seekTime, fastSeek } = seekDetails;
+ tab.linkedBrowser.browsingContext.mediaController.seekTo(seekTime, fastSeek);
+ await seekPromise;
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_stop_timer.js b/dom/media/mediacontrol/tests/browser/browser_media_control_stop_timer.js
new file mode 100644
index 0000000000..34fc10badd
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_stop_timer.js
@@ -0,0 +1,79 @@
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["media.mediacontrol.stopcontrol.timer", true],
+ ["media.mediacontrol.stopcontrol.timer.ms", 0],
+ ],
+ });
+});
+
+/**
+ * This test is used to check the stop timer for media element, which would stop
+ * media control for the specific element when the element has been paused over
+ * certain length of time. (That is controlled by the value of the pref
+ * `media.mediacontrol.stopcontrol.timer.ms`) In this test, we set the pref to 0
+ * which means the stop timer would be triggered after the media is paused.
+ * However, if the media is being used in PIP mode, we won't start the stop
+ * timer for it.
+ */
+add_task(async function testStopMediaControlAfterPausingMedia() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`pause media and the stop timer would stop media control`);
+ await pauseMediaAndMediaControlShouldBeStopped(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testNotToStopMediaControlForPIPVideo() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`trigger PIP mode`);
+ const winPIP = await triggerPictureInPicture(tab.linkedBrowser, testVideoId);
+
+ info(`pause media and the stop timer would not stop media control`);
+ await pauseMedia(tab, testVideoId);
+
+ info(`pressing 'play' key should start PIP video again`);
+ await generateMediaControlKeyEvent("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ await tab.close();
+});
+
+/**
+ * The following is helper function.
+ */
+function pauseMediaAndMediaControlShouldBeStopped(tab, elementId) {
+ // After pausing media, the stop timer would be triggered and stop the media
+ // control.
+ return Promise.all([
+ new Promise(r => (tab.controller.ondeactivated = r)),
+ SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ content.document.getElementById(Id).pause();
+ }),
+ ]);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_supported_keys.js b/dom/media/mediacontrol/tests/browser/browser_media_control_supported_keys.js
new file mode 100644
index 0000000000..2312e90c88
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_supported_keys.js
@@ -0,0 +1,130 @@
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+const sDefaultSupportedKeys = ["focus", "play", "pause", "playpause", "stop"];
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * Supported media keys are used for indicating what UI button should be shown
+ * on the virtual control interface. All supported media keys are listed in
+ * `MediaKey` in `MediaController.webidl`. Some media keys are defined as
+ * default media keys which are always supported. Otherwise, other media keys
+ * have to have corresponding action handler on the active media session in
+ * order to be added to the supported keys.
+ */
+add_task(async function testDefaultSupportedKeys() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`should use default supported keys`);
+ await supportedKeysShouldEqualTo(tab, sDefaultSupportedKeys);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testNoActionHandlerBeingSet() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create media session but not set any action handler`);
+ await setMediaSessionSupportedAction(tab, []);
+
+ info(
+ `should use default supported keys even if ` +
+ `media session doesn't have any action handler`
+ );
+ await supportedKeysShouldEqualTo(tab, sDefaultSupportedKeys);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSettingActionsWhichAreAlreadyDefaultKeys() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create media session but not set any action handler`);
+ await setMediaSessionSupportedAction(tab, ["play", "pause", "stop"]);
+
+ info(
+ `those actions has already been included in default supported keys, so ` +
+ `the result should still be default supported keys`
+ );
+ await supportedKeysShouldEqualTo(tab, sDefaultSupportedKeys);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSettingActionsWhichAreNotDefaultKeys() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create media session but not set any action handler`);
+ let nonDefaultActions = [
+ "seekbackward",
+ "seekforward",
+ "previoustrack",
+ "nexttrack",
+ ];
+ await setMediaSessionSupportedAction(tab, nonDefaultActions);
+
+ info(
+ `supported keys should include those actions which are not default supported keys`
+ );
+ let expectedKeys = sDefaultSupportedKeys.concat(nonDefaultActions);
+ await supportedKeysShouldEqualTo(tab, expectedKeys);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+async function supportedKeysShouldEqualTo(tab, expectedKeys) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ const supportedKeys = controller.supportedKeys;
+ while (JSON.stringify(expectedKeys) != JSON.stringify(supportedKeys)) {
+ await new Promise(r => (controller.onsupportedkeyschange = r));
+ }
+ for (let idx = 0; idx < expectedKeys.length; idx++) {
+ is(
+ supportedKeys[idx],
+ expectedKeys[idx],
+ `'${supportedKeys[idx]}' should equal to '${expectedKeys[idx]}'`
+ );
+ }
+}
+
+function setMediaSessionSupportedAction(tab, actions) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [actions], actionArr => {
+ for (let action of actionArr) {
+ content.navigator.mediaSession.setActionHandler(action, () => {
+ info(`set '${action}' action handler`);
+ });
+ }
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_nosrc_and_error_media.js b/dom/media/mediacontrol/tests/browser/browser_nosrc_and_error_media.js
new file mode 100644
index 0000000000..837b0ecda6
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_nosrc_and_error_media.js
@@ -0,0 +1,102 @@
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+const PAGE_NOSRC_MEDIA =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_no_src_media.html";
+const PAGE_ERROR_MEDIA =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_error_media.html";
+const PAGES = [PAGE_NOSRC_MEDIA, PAGE_ERROR_MEDIA];
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * To ensure the no src media and media with error won't activate the media
+ * controller even if they enters PIP mode or fullscreen.
+ */
+add_task(async function testNoSrcOrErrorMediaEntersPIPMode() {
+ for (let page of PAGES) {
+ info(`open media page ${page}`);
+ const tab = await createLoadedTabWrapper(page, { needCheck: false });
+
+ info(`controller should always inactive`);
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ controller.onactivated = () => {
+ ok(false, "should not get activated!");
+ };
+
+ info(`enter PIP mode which would not affect controller`);
+ const winPIP = await triggerPictureInPicture(
+ tab.linkedBrowser,
+ testVideoId
+ );
+ info(`leave PIP mode`);
+ await ensureMessageAndClosePiP(
+ tab.linkedBrowser,
+ testVideoId,
+ winPIP,
+ false
+ );
+ ok(!controller.isActive, "controller is still inactive");
+
+ info(`remove tab`);
+ await tab.close();
+ }
+});
+
+add_task(async function testNoSrcOrErrorMediaEntersFullscreen() {
+ for (let page of PAGES) {
+ info(`open media page ${page}`);
+ const tab = await createLoadedTabWrapper(page, { needCheck: false });
+
+ info(`controller should always inactive`);
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ controller.onactivated = () => {
+ ok(false, "should not get activated!");
+ };
+
+ info(`enter and leave fullscreen which would not affect controller`);
+ await enterAndLeaveFullScreen(tab, testVideoId);
+ ok(!controller.isActive, "controller is still inactive");
+
+ info(`remove tab`);
+ await tab.close();
+ }
+});
+
+/**
+ * The following is helper function.
+ */
+async function enterAndLeaveFullScreen(tab, elementId) {
+ await new Promise(resolve =>
+ SimpleTest.waitForFocus(resolve, tab.linkedBrowser.ownerGlobal)
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [elementId], elementId => {
+ return new Promise(r => {
+ const element = content.document.getElementById(elementId);
+ ok(!content.document.fullscreenElement, "no fullscreen element");
+ element.requestFullscreen();
+ element.onfullscreenchange = () => {
+ if (content.document.fullscreenElement) {
+ element.onfullscreenerror = null;
+ content.document.exitFullscreen();
+ } else {
+ element.onfullscreenchange = null;
+ element.onfullscreenerror = null;
+ r();
+ }
+ };
+ element.onfullscreenerror = () => {
+ // Retry until the element successfully enters fullscreen.
+ element.requestFullscreen();
+ };
+ });
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_only_control_non_real_time_media.js b/dom/media/mediacontrol/tests/browser/browser_only_control_non_real_time_media.js
new file mode 100644
index 0000000000..76cf8b0ffd
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_only_control_non_real_time_media.js
@@ -0,0 +1,76 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_empty_title.html";
+
+/**
+ * This test is used to ensure that real-time media won't be affected by the
+ * media control. Only non-real-time media would.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+add_task(async function testOnlyControlNonRealTimeMedia() {
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ await StartRealTimeMedia(tab);
+ ok(
+ !controller.isActive,
+ "starting a real-time media won't acivate controller"
+ );
+
+ info(`playing a non-real-time media would activate controller`);
+ await Promise.all([
+ new Promise(r => (controller.onactivated = r)),
+ startNonRealTimeMedia(tab),
+ ]);
+
+ info(`'pause' action should only pause non-real-time media`);
+ MediaControlService.generateMediaControlKey("pause");
+ await new Promise(r => (controller.onplaybackstatechange = r));
+ await checkIfMediaAreAffectedByMediaControl(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+async function startNonRealTimeMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ let video = content.document.getElementById("video");
+ if (!video) {
+ ok(false, `can not get the video element!`);
+ return;
+ }
+ await video.play();
+ });
+}
+
+async function StartRealTimeMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ let videoRealTime = content.document.createElement("video");
+ content.document.body.appendChild(videoRealTime);
+ videoRealTime.srcObject = await content.navigator.mediaDevices.getUserMedia(
+ { audio: true, fake: true }
+ );
+ // We want to ensure that the checking of should the media be controlled by
+ // media control would be performed after the element finishes loading the
+ // media stream. Using `autoplay` would trigger the play invocation only
+ // after the element get enough data.
+ videoRealTime.autoplay = true;
+ await new Promise(r => (videoRealTime.onplaying = r));
+ });
+}
+
+async function checkIfMediaAreAffectedByMediaControl(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ const vids = content.document.getElementsByTagName("video");
+ for (let vid of vids) {
+ if (!vid.srcObject) {
+ ok(vid.paused, "non-real-time media should be paused");
+ } else {
+ ok(!vid.paused, "real-time media should not be affected");
+ }
+ }
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_remove_controllable_media_for_active_controller.js b/dom/media/mediacontrol/tests/browser/browser_remove_controllable_media_for_active_controller.js
new file mode 100644
index 0000000000..a0aa5af4e9
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_remove_controllable_media_for_active_controller.js
@@ -0,0 +1,108 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * If an active controller has an active media session, then it can still be
+ * controlled via media key even if there is no controllable media presents.
+ * As active media session could still create other controllable media in its
+ * action handler, it should still receive media key. However, if a controller
+ * doesn't have an active media session, then it won't be controlled via media
+ * key when no controllable media presents.
+ */
+add_task(
+ async function testControllerWithActiveMediaSessionShouldStillBeActiveWhenNoControllableMediaPresents() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`play media would activate controller and media session`);
+ await setupMediaSession(tab);
+ await playMedia(tab, testVideoId);
+ await checkOrWaitControllerBecomesActive(tab);
+
+ info(`remove playing media so we don't have any controllable media now`);
+ await Promise.all([
+ new Promise(r => (tab.controller.onplaybackstatechange = r)),
+ removePlayingMedia(tab),
+ ]);
+
+ info(`despite that, controller should still be active`);
+ await checkOrWaitControllerBecomesActive(tab);
+
+ info(`active media session can still receive media key`);
+ await ensureActiveMediaSessionReceivedMediaKey(tab);
+
+ info(`remove tab`);
+ await tab.close();
+ }
+);
+
+add_task(
+ async function testControllerWithoutActiveMediaSessionShouldBecomeInactiveWhenNoControllableMediaPresents() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`play media would activate controller`);
+ await playMedia(tab, testVideoId);
+ await checkOrWaitControllerBecomesActive(tab);
+
+ info(
+ `remove playing media so we don't have any controllable media, which would deactivate controller`
+ );
+ await Promise.all([
+ new Promise(r => (tab.controller.ondeactivated = r)),
+ removePlayingMedia(tab),
+ ]);
+
+ info(`remove tab`);
+ await tab.close();
+ }
+);
+
+/**
+ * The following are helper functions.
+ */
+function setupMediaSession(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ // except `play/pause/stop`, set an action handler for arbitrary key in
+ // order to later verify if the session receives that media key by listening
+ // to session's `onpositionstatechange`.
+ content.navigator.mediaSession.setActionHandler("seekforward", _ => {
+ content.navigator.mediaSession.setPositionState({
+ duration: 60,
+ });
+ });
+ });
+}
+
+async function ensureActiveMediaSessionReceivedMediaKey(tab) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ const positionChangePromise = new Promise(
+ r => (controller.onpositionstatechange = r)
+ );
+ MediaControlService.generateMediaControlKey("seekforward");
+ await positionChangePromise;
+ ok(true, "active media session received media key");
+}
+
+function removePlayingMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [testVideoId], Id => {
+ content.document.getElementById(Id).remove();
+ });
+}
+
+async function checkOrWaitControllerBecomesActive(tab) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ if (!controller.isActive) {
+ info(`wait until controller gets activated`);
+ await new Promise(r => (controller.onactivated = r));
+ }
+ ok(controller.isActive, `controller is active`);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_resume_latest_paused_media.js b/dom/media/mediacontrol/tests/browser/browser_resume_latest_paused_media.js
new file mode 100644
index 0000000000..58cd3f5a0f
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_resume_latest_paused_media.js
@@ -0,0 +1,189 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_multiple_audible_media.html";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * This test is used to check when resuming media, we would only resume latest
+ * paused media, not all media in the page.
+ */
+add_task(async function testResumingLatestPausedMedias() {
+ info(`open media page and play all media`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ await playAllMedia(tab);
+
+ /**
+ * Pressing `pause` key would pause video1, video2, video3
+ * So resuming from media control key would affect those three media
+ */
+ info(`pressing 'pause' should pause all media`);
+ await generateMediaControlKeyEvent("pause");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`all media are latest paused, pressing 'play' should resume all`);
+ await generateMediaControlKeyEvent("play");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: false,
+ shouldVideo3BePaused: false,
+ });
+
+ info(`pause only one playing video by calling its webidl method`);
+ await pauseMedia(tab, "video3");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: false,
+ shouldVideo3BePaused: true,
+ });
+
+ /**
+ * Pressing `pause` key would pause video1, video2
+ * So resuming from media control key would affect those two media
+ */
+ info(`pressing 'pause' should pause two playing media`);
+ await generateMediaControlKeyEvent("pause");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`two media are latest paused, pressing 'play' should only affect them`);
+ await generateMediaControlKeyEvent("play");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: false,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`pause only one playing video by calling its webidl method`);
+ await pauseMedia(tab, "video2");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ /**
+ * Pressing `pause` key would pause video1
+ * So resuming from media control key would only affect one media
+ */
+ info(`pressing 'pause' should pause one playing media`);
+ await generateMediaControlKeyEvent("pause");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`one media is latest paused, pressing 'play' should only affect it`);
+ await generateMediaControlKeyEvent("play");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ /**
+ * Only one media is playing, so pausing it should not stop controlling media.
+ * We should still be able to resume it later.
+ */
+ info(`pause only playing video by calling its webidl method`);
+ await pauseMedia(tab, "video1");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`pressing 'pause' for already paused media, nothing would happen`);
+ // All media are already paused, so no need to wait for playback state change,
+ // call the method directly.
+ MediaControlService.generateMediaControlKey("pause");
+
+ info(`pressing 'play' would still affect on latest paused media`);
+ await generateMediaControlKeyEvent("play");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+async function playAllMedia(tab) {
+ const playbackStateChangedPromise = waitUntilDisplayedPlaybackChanged();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return new Promise(r => {
+ const videos = content.document.getElementsByTagName("video");
+ let mediaCount = 0;
+ docShell.chromeEventHandler.addEventListener(
+ "MozStartMediaControl",
+ () => {
+ if (++mediaCount == videos.length) {
+ info(`all media have started media control`);
+ r();
+ }
+ }
+ );
+ for (let video of videos) {
+ info(`play ${video.id} video`);
+ video.play();
+ }
+ });
+ });
+ await playbackStateChangedPromise;
+}
+
+async function pauseMedia(tab, videoId) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [videoId], videoId => {
+ const video = content.document.getElementById(videoId);
+ if (!video) {
+ ok(false, `can not find ${videoId}!`);
+ }
+ video.pause();
+ });
+}
+
+function checkMediaPausedState(
+ tab,
+ { shouldVideo1BePaused, shouldVideo2BePaused, shouldVideo3BePaused }
+) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [shouldVideo1BePaused, shouldVideo2BePaused, shouldVideo3BePaused],
+ (shouldVideo1BePaused, shouldVideo2BePaused, shouldVideo3BePaused) => {
+ const video1 = content.document.getElementById("video1");
+ const video2 = content.document.getElementById("video2");
+ const video3 = content.document.getElementById("video3");
+ is(
+ video1.paused,
+ shouldVideo1BePaused,
+ "Correct paused state for video1"
+ );
+ is(
+ video2.paused,
+ shouldVideo2BePaused,
+ "Correct paused state for video2"
+ );
+ is(
+ video3.paused,
+ shouldVideo3BePaused,
+ "Correct paused state for video3"
+ );
+ }
+ );
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_seek_captured_audio.js b/dom/media/mediacontrol/tests/browser/browser_seek_captured_audio.js
new file mode 100644
index 0000000000..3dbbee065e
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_seek_captured_audio.js
@@ -0,0 +1,59 @@
+const PAGE_NON_AUTOPLAY_MEDIA =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * Seeking a captured audio media before it starts, and it should still be able
+ * to be controlled via media key after it starts playing.
+ */
+add_task(async function testSeekAudibleCapturedMedia() {
+ info(`open new non autoplay media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY_MEDIA);
+
+ info(`perform seek on the captured media before it starts`);
+ await captureAudio(tab, testVideoId);
+ await seekAudio(tab, testVideoId);
+
+ info(`start captured media`);
+ await playMedia(tab, testVideoId);
+
+ info(`pressing 'pause' key, captured media should be paused`);
+ await generateMediaControlKeyEvent("pause");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function captureAudio(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ const context = new content.AudioContext();
+ // Capture audio from the media element to a MediaElementAudioSourceNode.
+ context.createMediaElementSource(video);
+ });
+}
+
+function seekAudio(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], async Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ video.currentTime = 0.0;
+ await new Promise(r => (video.onseeked = r));
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_stop_control_after_media_reaches_to_end.js b/dom/media/mediacontrol/tests/browser/browser_stop_control_after_media_reaches_to_end.js
new file mode 100644
index 0000000000..cc8ccf270a
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_stop_control_after_media_reaches_to_end.js
@@ -0,0 +1,108 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_looping_media.html";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.stopcontrol.aftermediaends", true]],
+ });
+});
+
+/**
+ * This test is used to ensure that we would stop controlling media after it
+ * reaches to the end when a controller doesn't have an active media session.
+ * If a controller has an active media session, it would keep active despite
+ * media reaches to the end.
+ */
+add_task(async function testControllerShouldStopAfterMediaReachesToTheEnd() {
+ info(`open media page and play media until the end`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ await Promise.all([
+ checkIfMediaControllerBecomeInactiveAfterMediaEnds(tab),
+ playMediaUntilItReachesToTheEnd(tab),
+ ]);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testControllerWontStopAfterMediaReachesToTheEnd() {
+ info(`open media page and create media session`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ await createMediaSession(tab);
+
+ info(`play media until the end`);
+ await playMediaUntilItReachesToTheEnd(tab);
+
+ info(`controller is still active because of having active media session`);
+ await checkControllerIsActive(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function checkIfMediaControllerBecomeInactiveAfterMediaEnds(tab) {
+ return new Promise(r => {
+ let activeChangedNums = 0;
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ controller.onactivated = () => {
+ is(
+ ++activeChangedNums,
+ 1,
+ `Receive ${activeChangedNums} times 'onactivechange'`
+ );
+ // We activate controller when it becomes playing, which doesn't guarantee
+ // it's already audible, so we won't check audible state here.
+ ok(controller.isActive, "controller should be active");
+ ok(controller.isPlaying, "controller should be playing");
+ };
+ controller.ondeactivated = () => {
+ is(
+ ++activeChangedNums,
+ 2,
+ `Receive ${activeChangedNums} times 'onactivechange'`
+ );
+ ok(!controller.isActive, "controller should be inactive");
+ ok(!controller.isAudible, "controller should be inaudible");
+ ok(!controller.isPlaying, "controller should be paused");
+ r();
+ };
+ });
+}
+
+function playMediaUntilItReachesToTheEnd(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ const video = content.document.getElementById("video");
+ if (!video) {
+ ok(false, "can't get video");
+ }
+
+ if (video.readyState < video.HAVE_METADATA) {
+ info(`load media to get its duration`);
+ video.load();
+ await new Promise(r => (video.loadedmetadata = r));
+ }
+
+ info(`adjust the start position to faster reach to the end`);
+ ok(video.duration > 1.0, "video's duration is larger than 1.0s");
+ video.currentTime = video.duration - 1.0;
+
+ info(`play ${video.id} video`);
+ video.play();
+ await new Promise(r => (video.onended = r));
+ });
+}
+
+function createMediaSession(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ // simply create a media session, which would become the active media session later.
+ content.navigator.mediaSession;
+ });
+}
+
+function checkControllerIsActive(tab) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ ok(controller.isActive, `controller is active`);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_suspend_inactive_tab.js b/dom/media/mediacontrol/tests/browser/browser_suspend_inactive_tab.js
new file mode 100644
index 0000000000..334717a2f2
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_suspend_inactive_tab.js
@@ -0,0 +1,131 @@
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const VIDEO_ID = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.suspend_inactive.enabled", true],
+ ["dom.audiocontext.testing", true],
+ ],
+ });
+});
+
+/**
+ * This test to used to test the feature that would suspend the inactive tab,
+ * which currently is only used on Android.
+ *
+ * Normally when tab becomes inactive, we would suspend it and stop its script
+ * from running. However, if a tab has a main controller, which indicates it
+ * might have playng media, or waiting media keys to control media, then it
+ * would not be suspended event if it's inactive.
+ *
+ * In addition, Note that, on Android, audio focus management is enabled by
+ * default, so there is only one tab being able to play at a time, which means
+ * the tab playing media always has main controller.
+ */
+add_task(async function testInactiveTabWouldBeSuspended() {
+ info(`open a tab`);
+ const tab = await createTab(PAGE_NON_AUTOPLAY);
+ await assertIfWindowGetSuspended(tab, { shouldBeSuspended: false });
+
+ info(`tab should be suspended when it becomes inactive`);
+ setTabActive(tab, false);
+ await assertIfWindowGetSuspended(tab, { shouldBeSuspended: true });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testInactiveTabEverStartPlayingWontBeSuspended() {
+ info(`open tab1 and play media`);
+ const tab1 = await createTab(PAGE_NON_AUTOPLAY, { needCheck: true });
+ await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: false });
+ await playMedia(tab1, VIDEO_ID);
+
+ info(`tab with playing media won't be suspended when it becomes inactive`);
+ setTabActive(tab1, false);
+ await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: false });
+
+ info(
+ `even if media is paused, keep tab running so that it could listen to media keys to control media in the future`
+ );
+ await pauseMedia(tab1, VIDEO_ID);
+ await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: false });
+
+ info(`open tab2 and play media`);
+ const tab2 = await createTab(PAGE_NON_AUTOPLAY, { needCheck: true });
+ await assertIfWindowGetSuspended(tab2, { shouldBeSuspended: false });
+ await playMedia(tab2, VIDEO_ID);
+
+ info(
+ `as inactive tab1 doesn't own main controller, it should be suspended again`
+ );
+ await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: true });
+
+ info(`remove tabs`);
+ await Promise.all([tab1.close(), tab2.close()]);
+});
+
+add_task(
+ async function testInactiveTabWithRunningAudioContextWontBeSuspended() {
+ info(`open tab and start an audio context (AC)`);
+ const tab = await createTab("about:blank");
+ await startAudioContext(tab);
+ await assertIfWindowGetSuspended(tab, { shouldBeSuspended: false });
+
+ info(`tab with running AC won't be suspended when it becomes inactive`);
+ setTabActive(tab, false);
+ await assertIfWindowGetSuspended(tab, { shouldBeSuspended: false });
+
+ info(`if AC has been suspended, then inactive tab should be suspended`);
+ await suspendAudioContext(tab);
+ await assertIfWindowGetSuspended(tab, { shouldBeSuspended: true });
+
+ info(`remove tab`);
+ await tab.close();
+ }
+);
+
+/**
+ * The following are helper functions.
+ */
+async function createTab(url, needCheck = false) {
+ const tab = await createLoadedTabWrapper(url, { needCheck });
+ return tab;
+}
+
+function assertIfWindowGetSuspended(tab, { shouldBeSuspended }) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [shouldBeSuspended],
+ expectedSuspend => {
+ const isSuspended = content.windowUtils.suspendedByBrowsingContextGroup;
+ is(
+ expectedSuspend,
+ isSuspended,
+ `window suspended state (${isSuspended}) is equal to the expected`
+ );
+ }
+ );
+}
+
+function setTabActive(tab, isActive) {
+ tab.linkedBrowser.docShellIsActive = isActive;
+}
+
+function startAudioContext(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ content.ac = new content.AudioContext();
+ await new Promise(r => (content.ac.onstatechange = r));
+ ok(content.ac.state == "running", `Audio context started running`);
+ });
+}
+
+function suspendAudioContext(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ await content.ac.suspend();
+ ok(content.ac.state == "suspended", `Audio context is suspended`);
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/file_audio_and_inaudible_media.html b/dom/media/mediacontrol/tests/browser/file_audio_and_inaudible_media.html
new file mode 100644
index 0000000000..e16d5dee26
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_audio_and_inaudible_media.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>page with audible and inaudible media</title>
+</head>
+<body>
+<video id="video1" src="gizmo.mp4" loop></video>
+<video id="video2" src="gizmo.mp4" loop muted></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_autoplay.html b/dom/media/mediacontrol/tests/browser/file_autoplay.html
new file mode 100644
index 0000000000..97a58ec2a2
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_autoplay.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Autoplay page</title>
+</head>
+<body>
+<video id="autoplay" src="gizmo.mp4" autoplay loop></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_empty_title.html b/dom/media/mediacontrol/tests/browser/file_empty_title.html
new file mode 100644
index 0000000000..516c16036f
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_empty_title.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title></title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_error_media.html b/dom/media/mediacontrol/tests/browser/file_error_media.html
new file mode 100644
index 0000000000..7f54340dd1
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_error_media.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Error media</title>
+</head>
+<body>
+<video id="video" src="bogus.ogv"></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_iframe_media.html b/dom/media/mediacontrol/tests/browser/file_iframe_media.html
new file mode 100644
index 0000000000..2d2c4fd122
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_iframe_media.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+<script type="text/javascript">
+
+const video = document.getElementById("video");
+const w = window.opener || window.parent;
+
+window.onmessage = async event => {
+ if (event.data == "fullscreen") {
+ video.requestFullscreen();
+ video.onfullscreenchange = () => {
+ video.onfullscreenchange = null;
+ video.onfullscreenerror = null;
+ w.postMessage("entered-fullscreen", "*");
+ }
+ video.onfullscreenerror = () => {
+ // Retry until the element successfully enters fullscreen.
+ video.requestFullscreen();
+ }
+ } else if (event.data == "check-playing") {
+ if (!video.paused) {
+ w.postMessage("checked-playing", "*");
+ } else {
+ video.onplaying = () => {
+ video.onplaying = null;
+ w.postMessage("checked-playing", "*");
+ }
+ }
+ } else if (event.data == "check-pause") {
+ if (video.paused) {
+ w.postMessage("checked-pause", "*");
+ } else {
+ video.onpause = () => {
+ video.onpause = null;
+ w.postMessage("checked-pause", "*");
+ }
+ }
+ } else if (event.data == "play") {
+ await video.play();
+ w.postMessage("played", "*");
+ } else if (event.data == "pause") {
+ video.pause();
+ w.postMessage("paused", "*");
+ } else if (event.data == "setMetadata") {
+ const metadata = {
+ title: document.title,
+ artist: document.title,
+ album: document.title,
+ artwork: [{ src: document.title, sizes: "128x128", type: "image/jpeg" }],
+ };
+ navigator.mediaSession.metadata = new window.MediaMetadata(metadata);
+ w.postMessage(metadata, "*");
+ } else if (event.data == "setPositionState") {
+ navigator.mediaSession.setPositionState({
+ duration: 60, // The value doesn't matter
+ });
+ } else if (event.data.cmd == "setActionHandler") {
+ if (window.triggeredActionHandler === undefined) {
+ window.triggeredActionHandler = {};
+ }
+ const action = event.data.action;
+ window.triggeredActionHandler[action] = new Promise(r => {
+ navigator.mediaSession.setActionHandler(action, async () => {
+ if (action == "stop" || action == "pause") {
+ video.pause();
+ } else if (action == "play") {
+ await video.play();
+ }
+ r();
+ });
+ });
+ w.postMessage("setActionHandler-done", "*");
+ } else if (event.data.cmd == "checkActionHandler") {
+ const action = event.data.action;
+ if (!window.triggeredActionHandler[action]) {
+ w.postMessage("checkActionHandler-fail", "*");
+ } else {
+ await window.triggeredActionHandler[action];
+ w.postMessage("checkActionHandler-done", "*");
+ }
+ } else if (event.data == "create-media-session") {
+ // simply calling a media session would create an instance.
+ navigator.mediaSession;
+ w.postMessage("created-media-session", "*");
+ }
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html b/dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html
new file mode 100644
index 0000000000..f8e7aa9afe
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Media control page with multiple iframes which contain media session</title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+<iframe id="frame1"></iframe>
+<iframe id="frame2"></iframe>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_multiple_audible_media.html b/dom/media/mediacontrol/tests/browser/file_multiple_audible_media.html
new file mode 100644
index 0000000000..e78fabe7fa
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_multiple_audible_media.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>mutiple audible media</title>
+</head>
+<body>
+<video id="video1" src="gizmo.mp4" loop></video>
+<video id="video2" src="gizmo.mp4" loop></video>
+<video id="video3" src="gizmo.mp4" loop></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_muted_autoplay.html b/dom/media/mediacontrol/tests/browser/file_muted_autoplay.html
new file mode 100644
index 0000000000..f64d537a46
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_muted_autoplay.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Muted autoplay page</title>
+</head>
+<body>
+<video id="autoplay" src="gizmo.mp4" autoplay loop muted></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_no_src_media.html b/dom/media/mediacontrol/tests/browser/file_no_src_media.html
new file mode 100644
index 0000000000..e1318e863c
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_no_src_media.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>No src media</title>
+</head>
+<body>
+<video id="video"></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_non_autoplay.html b/dom/media/mediacontrol/tests/browser/file_non_autoplay.html
new file mode 100644
index 0000000000..06daa7e2d8
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_non_autoplay.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Non-Autoplay page</title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+<image id="image" src="data:image/svg+xml;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiPgogIDxwYXRoIGZpbGw9ImNvbnRleHQtZmlsbCIgZmlsbC1vcGFjaXR5PSJjb250ZXh0LWZpbGwtb3BhY2l0eSIgZD0iTTggMGE4IDggMCAxIDAgOCA4IDguMDA5IDguMDA5IDAgMCAwLTgtOHptNS4xNjMgNC45NThoLTEuNTUyYTcuNyA3LjcgMCAwIDAtMS4wNTEtMi4zNzYgNi4wMyA2LjAzIDAgMCAxIDIuNjAzIDIuMzc2ek0xNCA4YTUuOTYzIDUuOTYzIDAgMCAxLS4zMzUgMS45NThoLTEuODIxQTEyLjMyNyAxMi4zMjcgMCAwIDAgMTIgOGExMi4zMjcgMTIuMzI3IDAgMCAwLS4xNTYtMS45NThoMS44MjFBNS45NjMgNS45NjMgMCAwIDEgMTQgOHptLTYgNmMtMS4wNzUgMC0yLjAzNy0xLjItMi41NjctMi45NThoNS4xMzVDMTAuMDM3IDEyLjggOS4wNzUgMTQgOCAxNHpNNS4xNzQgOS45NThhMTEuMDg0IDExLjA4NCAwIDAgMSAwLTMuOTE2aDUuNjUxQTExLjExNCAxMS4xMTQgMCAwIDEgMTEgOGExMS4xMTQgMTEuMTE0IDAgMCAxLS4xNzQgMS45NTh6TTIgOGE1Ljk2MyA1Ljk2MyAwIDAgMSAuMzM1LTEuOTU4aDEuODIxYTEyLjM2MSAxMi4zNjEgMCAwIDAgMCAzLjkxNkgyLjMzNUE1Ljk2MyA1Ljk2MyAwIDAgMSAyIDh6bTYtNmMxLjA3NSAwIDIuMDM3IDEuMiAyLjU2NyAyLjk1OEg1LjQzM0M1Ljk2MyAzLjIgNi45MjUgMiA4IDJ6bS0yLjU2LjU4MmE3LjcgNy43IDAgMCAwLTEuMDUxIDIuMzc2SDIuODM3QTYuMDMgNi4wMyAwIDAgMSA1LjQ0IDIuNTgyem0tMi42IDguNDZoMS41NDlhNy43IDcuNyAwIDAgMCAxLjA1MSAyLjM3NiA2LjAzIDYuMDMgMCAwIDEtMi42MDMtMi4zNzZ6bTcuNzIzIDIuMzc2YTcuNyA3LjcgMCAwIDAgMS4wNTEtMi4zNzZoMS41NTJhNi4wMyA2LjAzIDAgMCAxLTIuNjA2IDIuMzc2eiIvPgo8L3N2Zz4K">
+<iframe id="iframe" allow="fullscreen *" allowfullscreen></iframe>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_non_eligible_media.html b/dom/media/mediacontrol/tests/browser/file_non_eligible_media.html
new file mode 100644
index 0000000000..bf27943fce
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_non_eligible_media.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Media are not eligible to be controlled</title>
+</head>
+<body>
+<video id="muted" src="gizmo.mp4" controls muted loop></video>
+<video id="volume-0" src="gizmo.mp4" controls loop></video>
+<video id="no-audio-track" src="gizmo-noaudio.webm" controls loop></video>
+<video id="silent-audio-track" src="silentAudioTrack.webm" controls loop></video>
+<video id="short-duration" src="gizmo-short.mp4" controls loop></video>
+<video id="inaudible-captured-media" src="gizmo.mp4" muted controls loop></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_non_looping_media.html b/dom/media/mediacontrol/tests/browser/file_non_looping_media.html
new file mode 100644
index 0000000000..41e049645c
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_non_looping_media.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Non looping media page</title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4"></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/head.js b/dom/media/mediacontrol/tests/browser/head.js
new file mode 100644
index 0000000000..cac96c0bff
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/head.js
@@ -0,0 +1,402 @@
+/**
+ * This function would create a new foreround tab and load the url for it. In
+ * addition, instead of returning a tab element, we return a tab wrapper that
+ * helps us to automatically detect if the media controller of that tab
+ * dispatches the first (activated) and the last event (deactivated) correctly.
+ * @ param url
+ * the page url which tab would load
+ * @ param input window (optional)
+ * if it exists, the tab would be created from the input window. If not,
+ * then the tab would be created in current window.
+ * @ param needCheck (optional)
+ * it decides if we would perform the check for the first and last event
+ * on the media controller. It's default true.
+ */
+async function createLoadedTabWrapper(
+ url,
+ { inputWindow = window, needCheck = true } = {}
+) {
+ class tabWrapper {
+ constructor(tab, needCheck) {
+ this._tab = tab;
+ this._controller = tab.linkedBrowser.browsingContext.mediaController;
+ this._firstEvent = "";
+ this._lastEvent = "";
+ this._events = [
+ "activated",
+ "deactivated",
+ "metadatachange",
+ "playbackstatechange",
+ "positionstatechange",
+ "supportedkeyschange",
+ ];
+ this._needCheck = needCheck;
+ if (this._needCheck) {
+ this._registerAllEvents();
+ }
+ }
+ _registerAllEvents() {
+ for (let event of this._events) {
+ this._controller.addEventListener(event, this._handleEvent.bind(this));
+ }
+ }
+ _unregisterAllEvents() {
+ for (let event of this._events) {
+ this._controller.removeEventListener(
+ event,
+ this._handleEvent.bind(this)
+ );
+ }
+ }
+ _handleEvent(event) {
+ info(`handle event=${event.type}`);
+ if (this._firstEvent === "") {
+ this._firstEvent = event.type;
+ }
+ this._lastEvent = event.type;
+ }
+ get linkedBrowser() {
+ return this._tab.linkedBrowser;
+ }
+ get controller() {
+ return this._controller;
+ }
+ get tabElement() {
+ return this._tab;
+ }
+ async close() {
+ info(`wait until finishing close tab wrapper`);
+ const deactivationPromise = this._controller.isActive
+ ? new Promise(r => (this._controller.ondeactivated = r))
+ : Promise.resolve();
+ BrowserTestUtils.removeTab(this._tab);
+ await deactivationPromise;
+ if (this._needCheck) {
+ is(this._firstEvent, "activated", "First event should be 'activated'");
+ is(
+ this._lastEvent,
+ "deactivated",
+ "Last event should be 'deactivated'"
+ );
+ this._unregisterAllEvents();
+ }
+ }
+ }
+ const browser = inputWindow ? inputWindow.gBrowser : window.gBrowser;
+ let tab = await BrowserTestUtils.openNewForegroundTab(browser, url);
+ return new tabWrapper(tab, needCheck);
+}
+
+/**
+ * Returns a promise that resolves when generated media control keys has
+ * triggered the main media controller's corresponding method and changes its
+ * playback state.
+ *
+ * @param {string} event
+ * The event name of the media control key
+ * @return {Promise}
+ * Resolve when the main controller receives the media control key event
+ * and change its playback state.
+ */
+function generateMediaControlKeyEvent(event) {
+ const playbackStateChanged = waitUntilDisplayedPlaybackChanged();
+ MediaControlService.generateMediaControlKey(event);
+ return playbackStateChanged;
+}
+
+/**
+ * Play the specific media and wait until it plays successfully and the main
+ * controller has been updated.
+ *
+ * @param {tab} tab
+ * The tab that contains the media which we would play
+ * @param {string} elementId
+ * The element Id of the media which we would play
+ * @return {Promise}
+ * Resolve when the media has been starting playing and the main
+ * controller has been updated.
+ */
+async function playMedia(tab, elementId) {
+ const playbackStatePromise = waitUntilDisplayedPlaybackChanged();
+ await SpecialPowers.spawn(tab.linkedBrowser, [elementId], async Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ ok(
+ await video.play().then(
+ _ => true,
+ _ => false
+ ),
+ "video started playing"
+ );
+ });
+ return playbackStatePromise;
+}
+
+/**
+ * Pause the specific media and wait until it pauses successfully and the main
+ * controller has been updated.
+ *
+ * @param {tab} tab
+ * The tab that contains the media which we would pause
+ * @param {string} elementId
+ * The element Id of the media which we would pause
+ * @return {Promise}
+ * Resolve when the media has been paused and the main controller has
+ * been updated.
+ */
+function pauseMedia(tab, elementId) {
+ const pausePromise = SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [elementId],
+ Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ ok(!video.paused, `video is playing before calling pause`);
+ video.pause();
+ }
+ );
+ return Promise.all([pausePromise, waitUntilDisplayedPlaybackChanged()]);
+}
+
+/**
+ * Returns a promise that resolves when the specific media starts playing.
+ *
+ * @param {tab} tab
+ * The tab that contains the media which we would check
+ * @param {string} elementId
+ * The element Id of the media which we would check
+ * @return {Promise}
+ * Resolve when the media has been starting playing.
+ */
+function checkOrWaitUntilMediaStartedPlaying(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ return new Promise(resolve => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ if (!video.paused) {
+ ok(true, `media started playing`);
+ resolve();
+ } else {
+ info(`wait until media starts playing`);
+ video.onplaying = () => {
+ video.onplaying = null;
+ ok(true, `media started playing`);
+ resolve();
+ };
+ }
+ });
+ });
+}
+
+/**
+ * Returns a promise that resolves when the specific media stops playing.
+ *
+ * @param {tab} tab
+ * The tab that contains the media which we would check
+ * @param {string} elementId
+ * The element Id of the media which we would check
+ * @return {Promise}
+ * Resolve when the media has been stopped playing.
+ */
+function checkOrWaitUntilMediaStoppedPlaying(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ return new Promise(resolve => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ if (video.paused) {
+ ok(true, `media stopped playing`);
+ resolve();
+ } else {
+ info(`wait until media stops playing`);
+ video.onpause = () => {
+ video.onpause = null;
+ ok(true, `media stopped playing`);
+ resolve();
+ };
+ }
+ });
+ });
+}
+
+/**
+ * Check if the active metadata is empty.
+ */
+function isCurrentMetadataEmpty() {
+ const current = MediaControlService.getCurrentActiveMediaMetadata();
+ is(current.title, "", `current title should be empty`);
+ is(current.artist, "", `current title should be empty`);
+ is(current.album, "", `current album should be empty`);
+ is(current.artwork.length, 0, `current artwork should be empty`);
+}
+
+/**
+ * Check if the active metadata is equal to the given metadata.artwork
+ *
+ * @param {object} metadata
+ * The metadata that would be compared with the active metadata
+ */
+function isCurrentMetadataEqualTo(metadata) {
+ const current = MediaControlService.getCurrentActiveMediaMetadata();
+ is(
+ current.title,
+ metadata.title,
+ `tile '${current.title}' is equal to ${metadata.title}`
+ );
+ is(
+ current.artist,
+ metadata.artist,
+ `artist '${current.artist}' is equal to ${metadata.artist}`
+ );
+ is(
+ current.album,
+ metadata.album,
+ `album '${current.album}' is equal to ${metadata.album}`
+ );
+ is(
+ current.artwork.length,
+ metadata.artwork.length,
+ `artwork length '${current.artwork.length}' is equal to ${metadata.artwork.length}`
+ );
+ for (let idx = 0; idx < metadata.artwork.length; idx++) {
+ // the current src we got would be a completed path of the image, so we do
+ // not check if they are equal, we check if the current src includes the
+ // metadata's file name. Eg. "http://foo/bar.jpg" v.s. "bar.jpg"
+ ok(
+ current.artwork[idx].src.includes(metadata.artwork[idx].src),
+ `artwork src '${current.artwork[idx].src}' includes ${metadata.artwork[idx].src}`
+ );
+ is(
+ current.artwork[idx].sizes,
+ metadata.artwork[idx].sizes,
+ `artwork sizes '${current.artwork[idx].sizes}' is equal to ${metadata.artwork[idx].sizes}`
+ );
+ is(
+ current.artwork[idx].type,
+ metadata.artwork[idx].type,
+ `artwork type '${current.artwork[idx].type}' is equal to ${metadata.artwork[idx].type}`
+ );
+ }
+}
+
+/**
+ * Check if the given tab is using the default metadata. If the tab is being
+ * used in the private browsing mode, `isPrivateBrowsing` should be definded in
+ * the `options`.
+ */
+async function isGivenTabUsingDefaultMetadata(tab, options = {}) {
+ const localization = new Localization([
+ "branding/brand.ftl",
+ "dom/media.ftl",
+ ]);
+ const fallbackTitle = await localization.formatValue(
+ "mediastatus-fallback-title"
+ );
+ ok(fallbackTitle.length, "l10n fallback title is not empty");
+
+ const metadata =
+ tab.linkedBrowser.browsingContext.mediaController.getMetadata();
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [metadata.title, fallbackTitle, options.isPrivateBrowsing],
+ (title, fallbackTitle, isPrivateBrowsing) => {
+ if (isPrivateBrowsing || !content.document.title.length) {
+ is(title, fallbackTitle, "Using a generic default fallback title");
+ } else {
+ is(
+ title,
+ content.document.title,
+ "Using website title as a default title"
+ );
+ }
+ }
+ );
+ is(metadata.artwork.length, 1, "Default metada contains one artwork");
+ ok(
+ metadata.artwork[0].src.includes("defaultFavicon.svg"),
+ "Using default favicon as a default art work"
+ );
+}
+
+/**
+ * Wait until the main media controller changes its playback state, we would
+ * observe that by listening for `media-displayed-playback-changed`
+ * notification.
+ *
+ * @return {Promise}
+ * Resolve when observing `media-displayed-playback-changed`
+ */
+function waitUntilDisplayedPlaybackChanged() {
+ return BrowserUtils.promiseObserved("media-displayed-playback-changed");
+}
+
+/**
+ * Wait until the metadata that would be displayed on the virtual control
+ * interface changes. we would observe that by listening for
+ * `media-displayed-metadata-changed` notification.
+ *
+ * @return {Promise}
+ * Resolve when observing `media-displayed-metadata-changed`
+ */
+function waitUntilDisplayedMetadataChanged() {
+ return BrowserUtils.promiseObserved("media-displayed-metadata-changed");
+}
+
+/**
+ * Wait until the main media controller has been changed, we would observe that
+ * by listening for the `main-media-controller-changed` notification.
+ *
+ * @return {Promise}
+ * Resolve when observing `main-media-controller-changed`
+ */
+function waitUntilMainMediaControllerChanged() {
+ return BrowserUtils.promiseObserved("main-media-controller-changed");
+}
+
+/**
+ * Wait until any media controller updates its metadata even if it's not the
+ * main controller. The difference between this function and
+ * `waitUntilDisplayedMetadataChanged()` is that the changed metadata might come
+ * from non-main controller so it won't be show on the virtual control
+ * interface. we would observe that by listening for
+ * `media-session-controller-metadata-changed` notification.
+ *
+ * @return {Promise}
+ * Resolve when observing `media-session-controller-metadata-changed`
+ */
+function waitUntilControllerMetadataChanged() {
+ return BrowserUtils.promiseObserved(
+ "media-session-controller-metadata-changed"
+ );
+}
+
+/**
+ * Wait until media controller amount changes, we would observe that by
+ * listening for `media-controller-amount-changed` notification.
+ *
+ * @return {Promise}
+ * Resolve when observing `media-controller-amount-changed`
+ */
+function waitUntilMediaControllerAmountChanged() {
+ return BrowserUtils.promiseObserved("media-controller-amount-changed");
+}
+
+/**
+ * check if the media controll from given tab is active. If not, return a
+ * promise and resolve it when controller become active.
+ */
+async function checkOrWaitUntilControllerBecomeActive(tab) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ if (controller.isActive) {
+ return;
+ }
+ await new Promise(r => (controller.onactivated = r));
+}
diff --git a/dom/media/mediacontrol/tests/gtest/MediaKeyListenerTest.h b/dom/media/mediacontrol/tests/gtest/MediaKeyListenerTest.h
new file mode 100644
index 0000000000..5145eb7dbb
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/MediaKeyListenerTest.h
@@ -0,0 +1,39 @@
+/* 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/. */
+
+#ifndef DOM_MEDIA_MEDIAKEYLISTENERTEST_H_
+#define DOM_MEDIA_MEDIAKEYLISTENERTEST_H_
+
+#include "MediaControlKeySource.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla {
+namespace dom {
+
+class MediaKeyListenerTest : public MediaControlKeyListener {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaKeyListenerTest, override)
+
+ void Clear() { mReceivedKey = mozilla::Nothing(); }
+
+ void OnActionPerformed(const MediaControlAction& aAction) override {
+ mReceivedKey = mozilla::Some(aAction.mKey);
+ }
+ bool IsResultEqualTo(MediaControlKey aResult) const {
+ if (mReceivedKey) {
+ return *mReceivedKey == aResult;
+ }
+ return false;
+ }
+ bool IsReceivedResult() const { return mReceivedKey.isSome(); }
+
+ private:
+ ~MediaKeyListenerTest() = default;
+ mozilla::Maybe<MediaControlKey> mReceivedKey;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_MEDIA_MEDIAKEYLISTENERTEST_H_
diff --git a/dom/media/mediacontrol/tests/gtest/TestAudioFocusManager.cpp b/dom/media/mediacontrol/tests/gtest/TestAudioFocusManager.cpp
new file mode 100644
index 0000000000..df1bb288a4
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestAudioFocusManager.cpp
@@ -0,0 +1,163 @@
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "AudioFocusManager.h"
+#include "MediaControlService.h"
+#include "mozilla/Preferences.h"
+
+using namespace mozilla::dom;
+
+#define FIRST_CONTROLLER_ID 0
+#define SECOND_CONTROLLER_ID 1
+
+// This RAII class is used to set the audio focus management pref within a test
+// and automatically revert the change when a test ends, in order not to
+// interfere other tests unexpectedly.
+class AudioFocusManagmentPrefSetterRAII {
+ public:
+ explicit AudioFocusManagmentPrefSetterRAII(bool aPrefValue) {
+ mOriginalValue = mozilla::Preferences::GetBool(mPrefName, false);
+ mozilla::Preferences::SetBool(mPrefName, aPrefValue);
+ }
+ ~AudioFocusManagmentPrefSetterRAII() {
+ mozilla::Preferences::SetBool(mPrefName, mOriginalValue);
+ }
+
+ private:
+ const char* mPrefName = "media.audioFocus.management";
+ bool mOriginalValue;
+};
+
+TEST(AudioFocusManager, TestRequestAudioFocus)
+{
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller = new MediaController(FIRST_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RevokeAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+}
+
+TEST(AudioFocusManager, TestAudioFocusNumsWhenEnableAudioFocusManagement)
+{
+ // When enabling audio focus management, we only allow one controller owing
+ // audio focus at a time when the audio competing occurs. As the mechanism of
+ // handling the audio competing involves multiple components, we can't test it
+ // simply by using the APIs from AudioFocusManager.
+ AudioFocusManagmentPrefSetterRAII prefSetter(true);
+
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller1 =
+ new MediaController(FIRST_CONTROLLER_ID);
+
+ RefPtr<MediaController> controller2 =
+ new MediaController(SECOND_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller1);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ // When controller2 starts, it would win the audio focus from controller1. So
+ // only one audio focus would exist.
+ manager.RequestAudioFocus(controller2);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RevokeAudioFocus(controller2);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+}
+
+TEST(AudioFocusManager, TestAudioFocusNumsWhenDisableAudioFocusManagement)
+{
+ // When disabling audio focus management, we won't handle the audio competing,
+ // so we allow multiple audio focus existing at the same time.
+ AudioFocusManagmentPrefSetterRAII prefSetter(false);
+
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller1 =
+ new MediaController(FIRST_CONTROLLER_ID);
+
+ RefPtr<MediaController> controller2 =
+ new MediaController(SECOND_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller1);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RequestAudioFocus(controller2);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 2);
+
+ manager.RevokeAudioFocus(controller1);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RevokeAudioFocus(controller2);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+}
+
+TEST(AudioFocusManager, TestRequestAudioFocusRepeatedly)
+{
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller = new MediaController(FIRST_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RequestAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+}
+
+TEST(AudioFocusManager, TestRevokeAudioFocusRepeatedly)
+{
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller = new MediaController(FIRST_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RevokeAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ manager.RevokeAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+}
+
+TEST(AudioFocusManager, TestRevokeAudioFocusWithoutRequestAudioFocus)
+{
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller = new MediaController(FIRST_CONTROLLER_ID);
+
+ manager.RevokeAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+}
+
+TEST(AudioFocusManager,
+ TestRevokeAudioFocusForControllerWithoutOwningAudioFocus)
+{
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller1 =
+ new MediaController(FIRST_CONTROLLER_ID);
+
+ RefPtr<MediaController> controller2 =
+ new MediaController(SECOND_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller1);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RevokeAudioFocus(controller2);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+}
diff --git a/dom/media/mediacontrol/tests/gtest/TestMediaControlService.cpp b/dom/media/mediacontrol/tests/gtest/TestMediaControlService.cpp
new file mode 100644
index 0000000000..ab5b0cb8c7
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestMediaControlService.cpp
@@ -0,0 +1,64 @@
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "MediaControlService.h"
+#include "MediaController.h"
+
+using namespace mozilla::dom;
+
+#define FIRST_CONTROLLER_ID 0
+#define SECOND_CONTROLLER_ID 1
+
+TEST(MediaControlService, TestAddOrRemoveControllers)
+{
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+
+ RefPtr<MediaController> controller1 =
+ new MediaController(FIRST_CONTROLLER_ID);
+ RefPtr<MediaController> controller2 =
+ new MediaController(SECOND_CONTROLLER_ID);
+
+ service->RegisterActiveMediaController(controller1);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 1);
+
+ service->RegisterActiveMediaController(controller2);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 2);
+
+ service->UnregisterActiveMediaController(controller1);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 1);
+
+ service->UnregisterActiveMediaController(controller2);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+}
+
+TEST(MediaControlService, TestMainController)
+{
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+
+ RefPtr<MediaController> controller1 =
+ new MediaController(FIRST_CONTROLLER_ID);
+ service->RegisterActiveMediaController(controller1);
+
+ RefPtr<MediaController> mainController = service->GetMainController();
+ ASSERT_TRUE(mainController->Id() == FIRST_CONTROLLER_ID);
+
+ RefPtr<MediaController> controller2 =
+ new MediaController(SECOND_CONTROLLER_ID);
+ service->RegisterActiveMediaController(controller2);
+
+ mainController = service->GetMainController();
+ ASSERT_TRUE(mainController->Id() == SECOND_CONTROLLER_ID);
+
+ service->UnregisterActiveMediaController(controller2);
+ mainController = service->GetMainController();
+ ASSERT_TRUE(mainController->Id() == FIRST_CONTROLLER_ID);
+
+ service->UnregisterActiveMediaController(controller1);
+ mainController = service->GetMainController();
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+ ASSERT_TRUE(!mainController);
+}
diff --git a/dom/media/mediacontrol/tests/gtest/TestMediaController.cpp b/dom/media/mediacontrol/tests/gtest/TestMediaController.cpp
new file mode 100644
index 0000000000..5ba8d2a7d5
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestMediaController.cpp
@@ -0,0 +1,204 @@
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "MediaControlService.h"
+#include "MediaController.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+
+using namespace mozilla::dom;
+
+#define CONTROLLER_ID 0
+#define FAKE_CONTEXT_ID 0
+
+#define FIRST_CONTROLLER_ID 0
+
+TEST(MediaController, DefaultValueCheck)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+ ASSERT_TRUE(!controller->IsAnyMediaBeingControlled());
+ ASSERT_TRUE(controller->Id() == CONTROLLER_ID);
+ ASSERT_TRUE(controller->PlaybackState() == MediaSessionPlaybackState::None);
+ ASSERT_TRUE(!controller->IsAudible());
+}
+
+TEST(MediaController, IsAnyMediaBeingControlled)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+ ASSERT_TRUE(!controller->IsAnyMediaBeingControlled());
+
+ controller->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStarted);
+ ASSERT_TRUE(controller->IsAnyMediaBeingControlled());
+
+ controller->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStarted);
+ ASSERT_TRUE(controller->IsAnyMediaBeingControlled());
+
+ controller->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStopped);
+ ASSERT_TRUE(controller->IsAnyMediaBeingControlled());
+
+ controller->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStopped);
+ ASSERT_TRUE(!controller->IsAnyMediaBeingControlled());
+}
+
+class FakeControlledMedia final {
+ public:
+ explicit FakeControlledMedia(MediaController* aController)
+ : mController(aController) {
+ mController->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStarted);
+ }
+
+ void SetPlaying(MediaPlaybackState aState) {
+ if (mPlaybackState == aState) {
+ return;
+ }
+ mController->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID, aState);
+ mPlaybackState = aState;
+ }
+
+ void SetAudible(MediaAudibleState aState) {
+ if (mAudibleState == aState) {
+ return;
+ }
+ mController->NotifyMediaAudibleChanged(FAKE_CONTEXT_ID, aState);
+ mAudibleState = aState;
+ }
+
+ ~FakeControlledMedia() {
+ if (mPlaybackState == MediaPlaybackState::ePlayed) {
+ mController->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::ePaused);
+ }
+ mController->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStopped);
+ }
+
+ private:
+ MediaPlaybackState mPlaybackState = MediaPlaybackState::eStopped;
+ MediaAudibleState mAudibleState = MediaAudibleState::eInaudible;
+ RefPtr<MediaController> mController;
+};
+
+TEST(MediaController, ActiveAndDeactiveController)
+{
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+
+ RefPtr<MediaController> controller = new MediaController(FIRST_CONTROLLER_ID);
+
+ // In order to check active control number after FakeControlledMedia
+ // destroyed.
+ {
+ FakeControlledMedia fakeMedia(controller);
+ fakeMedia.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 1);
+
+ fakeMedia.SetAudible(MediaAudibleState::eAudible);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 1);
+
+ fakeMedia.SetAudible(MediaAudibleState::eInaudible);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 1);
+ }
+
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+}
+
+TEST(MediaController, AudibleChanged)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+
+ FakeControlledMedia fakeMedia(controller);
+ fakeMedia.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(!controller->IsAudible());
+
+ fakeMedia.SetAudible(MediaAudibleState::eAudible);
+ ASSERT_TRUE(controller->IsAudible());
+
+ fakeMedia.SetAudible(MediaAudibleState::eInaudible);
+ ASSERT_TRUE(!controller->IsAudible());
+}
+
+TEST(MediaController, PlayingStateChangeViaControlledMedia)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+
+ // In order to check playing state after FakeControlledMedia destroyed.
+ {
+ FakeControlledMedia foo(controller);
+ ASSERT_TRUE(controller->PlaybackState() == MediaSessionPlaybackState::None);
+
+ foo.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+
+ foo.SetPlaying(MediaPlaybackState::ePaused);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Paused);
+
+ foo.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+ }
+
+ // FakeControlledMedia has been destroyed, no playing media exists.
+ ASSERT_TRUE(controller->PlaybackState() == MediaSessionPlaybackState::Paused);
+}
+
+TEST(MediaController, ControllerShouldRemainPlayingIfAnyPlayingMediaExists)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+
+ {
+ FakeControlledMedia foo(controller);
+ ASSERT_TRUE(controller->PlaybackState() == MediaSessionPlaybackState::None);
+
+ foo.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+
+ // foo is playing, so controller is in `playing` state.
+ FakeControlledMedia bar(controller);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+
+ bar.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+
+ // Although we paused bar, but foo is still playing, so the controller would
+ // still be in `playing`.
+ bar.SetPlaying(MediaPlaybackState::ePaused);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+
+ foo.SetPlaying(MediaPlaybackState::ePaused);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Paused);
+ }
+
+ // both foo and bar have been destroyed, no playing media exists.
+ ASSERT_TRUE(controller->PlaybackState() == MediaSessionPlaybackState::Paused);
+}
+
+TEST(MediaController, PictureInPictureModeOrFullscreen)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+ ASSERT_TRUE(!controller->IsBeingUsedInPIPModeOrFullscreen());
+
+ controller->SetIsInPictureInPictureMode(FAKE_CONTEXT_ID, true);
+ ASSERT_TRUE(controller->IsBeingUsedInPIPModeOrFullscreen());
+
+ controller->SetIsInPictureInPictureMode(FAKE_CONTEXT_ID, false);
+ ASSERT_TRUE(!controller->IsBeingUsedInPIPModeOrFullscreen());
+
+ controller->NotifyMediaFullScreenState(FAKE_CONTEXT_ID, true);
+ ASSERT_TRUE(controller->IsBeingUsedInPIPModeOrFullscreen());
+
+ controller->NotifyMediaFullScreenState(FAKE_CONTEXT_ID, false);
+ ASSERT_TRUE(!controller->IsBeingUsedInPIPModeOrFullscreen());
+}
diff --git a/dom/media/mediacontrol/tests/gtest/TestMediaKeysEvent.cpp b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEvent.cpp
new file mode 100644
index 0000000000..a73ceb8592
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEvent.cpp
@@ -0,0 +1,49 @@
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "MediaController.h"
+#include "MediaControlKeySource.h"
+
+using namespace mozilla::dom;
+
+class MediaControlKeySourceTestImpl : public MediaControlKeySource {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaControlKeySourceTestImpl, override)
+ bool Open() override { return true; }
+ bool IsOpened() const override { return true; }
+ void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override {}
+
+ private:
+ ~MediaControlKeySourceTestImpl() = default;
+};
+
+TEST(MediaControlKey, TestAddOrRemoveListener)
+{
+ RefPtr<MediaControlKeySource> source = new MediaControlKeySourceTestImpl();
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaControlKeyListener> listener = new MediaControlKeyHandler();
+
+ source->AddListener(listener);
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+
+ source->RemoveListener(listener);
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+}
+
+TEST(MediaControlKey, SetSourcePlaybackState)
+{
+ RefPtr<MediaControlKeySource> source = new MediaControlKeySourceTestImpl();
+ ASSERT_TRUE(source->GetPlaybackState() == MediaSessionPlaybackState::None);
+
+ source->SetPlaybackState(MediaSessionPlaybackState::Playing);
+ ASSERT_TRUE(source->GetPlaybackState() == MediaSessionPlaybackState::Playing);
+
+ source->SetPlaybackState(MediaSessionPlaybackState::Paused);
+ ASSERT_TRUE(source->GetPlaybackState() == MediaSessionPlaybackState::Paused);
+
+ source->SetPlaybackState(MediaSessionPlaybackState::None);
+ ASSERT_TRUE(source->GetPlaybackState() == MediaSessionPlaybackState::None);
+}
diff --git a/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMac.mm b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMac.mm
new file mode 100644
index 0000000000..3c8ce8cd9e
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMac.mm
@@ -0,0 +1,136 @@
+/* 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/. */
+
+#import <AppKit/AppKit.h>
+#import <AppKit/NSEvent.h>
+#import <ApplicationServices/ApplicationServices.h>
+#import <CoreFoundation/CoreFoundation.h>
+#import <IOKit/hidsystem/ev_keymap.h>
+
+#include "gtest/gtest.h"
+#include "MediaHardwareKeysEventSourceMac.h"
+#include "MediaKeyListenerTest.h"
+#include "mozilla/Maybe.h"
+
+using namespace mozilla::dom;
+using namespace mozilla::widget;
+
+static const int kSystemDefinedEventMediaKeysSubtype = 8;
+
+static void SendFakeEvent(RefPtr<MediaHardwareKeysEventSourceMac>& aSource, int aKeyData) {
+ NSEvent* event = [NSEvent otherEventWithType:NSEventTypeSystemDefined
+ location:NSZeroPoint
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:nil
+ subtype:kSystemDefinedEventMediaKeysSubtype
+ data1:aKeyData
+ data2:0];
+ aSource->EventTapCallback(nullptr, static_cast<CGEventType>(0), [event CGEvent], aSource.get());
+}
+
+static void NotifyFakeNonMediaKey(RefPtr<MediaHardwareKeysEventSourceMac>& aSource,
+ bool aIsKeyPressed) {
+ int keyData = 0 | ((aIsKeyPressed ? 0xA : 0xB) << 8);
+ SendFakeEvent(aSource, keyData);
+}
+
+static void NotifyFakeMediaControlKey(RefPtr<MediaHardwareKeysEventSourceMac>& aSource,
+ MediaControlKey aEvent, bool aIsKeyPressed) {
+ int keyData = 0;
+ if (aEvent == MediaControlKey::Playpause) {
+ keyData = NX_KEYTYPE_PLAY << 16;
+ } else if (aEvent == MediaControlKey::Nexttrack) {
+ keyData = NX_KEYTYPE_NEXT << 16;
+ } else if (aEvent == MediaControlKey::Previoustrack) {
+ keyData = NX_KEYTYPE_PREVIOUS << 16;
+ }
+ keyData |= ((aIsKeyPressed ? 0xA : 0xB) << 8);
+ SendFakeEvent(aSource, keyData);
+}
+
+static void NotifyKeyPressedMediaKey(RefPtr<MediaHardwareKeysEventSourceMac>& aSource,
+ MediaControlKey aEvent) {
+ NotifyFakeMediaControlKey(aSource, aEvent, true /* key pressed */);
+}
+
+static void NotifyKeyReleasedMediaKeysEvent(RefPtr<MediaHardwareKeysEventSourceMac>& aSource,
+ MediaControlKey aEvent) {
+ NotifyFakeMediaControlKey(aSource, aEvent, false /* key released */);
+}
+
+static void NotifyKeyPressedNonMediaKeysEvents(RefPtr<MediaHardwareKeysEventSourceMac>& aSource) {
+ NotifyFakeNonMediaKey(aSource, true /* key pressed */);
+}
+
+static void NotifyKeyReleasedNonMediaKeysEvents(RefPtr<MediaHardwareKeysEventSourceMac>& aSource) {
+ NotifyFakeNonMediaKey(aSource, false /* key released */);
+}
+
+TEST(MediaHardwareKeysEventSourceMac, TestKeyPressedMediaKeysEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMac> source = new MediaHardwareKeysEventSourceMac();
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+ source->AddListener(listener.get());
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyPressedMediaKey(source, MediaControlKey::Playpause);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Playpause));
+
+ NotifyKeyPressedMediaKey(source, MediaControlKey::Nexttrack);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Nexttrack));
+
+ NotifyKeyPressedMediaKey(source, MediaControlKey::Previoustrack);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Previoustrack));
+
+ source->RemoveListener(listener);
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+}
+
+TEST(MediaHardwareKeysEventSourceMac, TestKeyReleasedMediaKeysEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMac> source = new MediaHardwareKeysEventSourceMac();
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+ source->AddListener(listener.get());
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyReleasedMediaKeysEvent(source, MediaControlKey::Playpause);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyReleasedMediaKeysEvent(source, MediaControlKey::Nexttrack);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyReleasedMediaKeysEvent(source, MediaControlKey::Previoustrack);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ source->RemoveListener(listener);
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+}
+
+TEST(MediaHardwareKeysEventSourceMac, TestNonMediaKeysEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMac> source = new MediaHardwareKeysEventSourceMac();
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+ source->AddListener(listener.get());
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyPressedNonMediaKeysEvents(source);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyReleasedNonMediaKeysEvents(source);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ source->RemoveListener(listener);
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+}
diff --git a/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMediaCenter.mm b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMediaCenter.mm
new file mode 100644
index 0000000000..1d32960d2f
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMediaCenter.mm
@@ -0,0 +1,166 @@
+/* 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/. */
+#import <MediaPlayer/MediaPlayer.h>
+
+#include "gtest/gtest.h"
+#include "MediaHardwareKeysEventSourceMacMediaCenter.h"
+#include "MediaKeyListenerTest.h"
+#include "nsCocoaFeatures.h"
+#include "nsCocoaUtils.h"
+#include "prinrval.h"
+#include "prthread.h"
+
+using namespace mozilla::dom;
+using namespace mozilla::widget;
+
+NS_ASSUME_NONNULL_BEGIN
+
+TEST(MediaHardwareKeysEventSourceMacMediaCenter, TestMediaCenterPlayPauseEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMacMediaCenter> source =
+ new MediaHardwareKeysEventSourceMacMediaCenter();
+
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+
+ source->AddListener(listener.get());
+
+ ASSERT_TRUE(source->Open());
+
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePlaying);
+
+ MediaCenterEventHandler playPauseHandler = source->CreatePlayPauseHandler();
+ playPauseHandler(nil);
+
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePaused);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Playpause));
+
+ listener->Clear(); // Reset stored media key
+
+ playPauseHandler(nil);
+
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePlaying);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Playpause));
+}
+
+TEST(MediaHardwareKeysEventSourceMacMediaCenter, TestMediaCenterPlayEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMacMediaCenter> source =
+ new MediaHardwareKeysEventSourceMacMediaCenter();
+
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+
+ source->AddListener(listener.get());
+
+ ASSERT_TRUE(source->Open());
+
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePlaying);
+
+ MediaCenterEventHandler playHandler = source->CreatePlayHandler();
+
+ center.playbackState = MPNowPlayingPlaybackStatePaused;
+
+ playHandler(nil);
+
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePlaying);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Play));
+}
+
+TEST(MediaHardwareKeysEventSourceMacMediaCenter, TestMediaCenterPauseEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMacMediaCenter> source =
+ new MediaHardwareKeysEventSourceMacMediaCenter();
+
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+
+ source->AddListener(listener.get());
+
+ ASSERT_TRUE(source->Open());
+
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePlaying);
+
+ MediaCenterEventHandler pauseHandler = source->CreatePauseHandler();
+
+ pauseHandler(nil);
+
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePaused);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Pause));
+}
+
+TEST(MediaHardwareKeysEventSourceMacMediaCenter, TestMediaCenterPrevNextEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMacMediaCenter> source =
+ new MediaHardwareKeysEventSourceMacMediaCenter();
+
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+
+ source->AddListener(listener.get());
+
+ ASSERT_TRUE(source->Open());
+
+ MediaCenterEventHandler nextHandler = source->CreateNextTrackHandler();
+
+ nextHandler(nil);
+
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Nexttrack));
+
+ MediaCenterEventHandler previousHandler = source->CreatePreviousTrackHandler();
+
+ previousHandler(nil);
+
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Previoustrack));
+}
+
+TEST(MediaHardwareKeysEventSourceMacMediaCenter, TestSetMetadata)
+{
+ RefPtr<MediaHardwareKeysEventSourceMacMediaCenter> source =
+ new MediaHardwareKeysEventSourceMacMediaCenter();
+
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+
+ source->AddListener(listener.get());
+
+ ASSERT_TRUE(source->Open());
+
+ MediaMetadataBase metadata;
+ metadata.mTitle = u"MediaPlayback";
+ metadata.mArtist = u"Firefox";
+ metadata.mAlbum = u"Mozilla";
+ source->SetMediaMetadata(metadata);
+
+ // The update procedure of nowPlayingInfo is async, so wait for a second
+ // before checking the result.
+ PR_Sleep(PR_SecondsToInterval(1));
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ ASSERT_TRUE([center.nowPlayingInfo[MPMediaItemPropertyTitle] isEqualToString:@"MediaPlayback"]);
+ ASSERT_TRUE([center.nowPlayingInfo[MPMediaItemPropertyArtist] isEqualToString:@"Firefox"]);
+ ASSERT_TRUE([center.nowPlayingInfo[MPMediaItemPropertyAlbumTitle] isEqualToString:@"Mozilla"]);
+
+ source->Close();
+ PR_Sleep(PR_SecondsToInterval(1));
+ ASSERT_TRUE(center.nowPlayingInfo == nil);
+}
+
+NS_ASSUME_NONNULL_END
diff --git a/dom/media/mediacontrol/tests/gtest/moz.build b/dom/media/mediacontrol/tests/gtest/moz.build
new file mode 100644
index 0000000000..7043bfcd5e
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += [
+ "TestAudioFocusManager.cpp",
+ "TestMediaController.cpp",
+ "TestMediaControlService.cpp",
+ "TestMediaKeysEvent.cpp",
+]
+
+if CONFIG["MOZ_APPLEMEDIA"]:
+ UNIFIED_SOURCES += ["TestMediaKeysEventMac.mm", "TestMediaKeysEventMediaCenter.mm"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/media/mediacontrol",
+]
+
+FINAL_LIBRARY = "xul-gtest"