/** * Cross-source audio focus competition tests. * * With media.audioFocus.management = true, the chrome-side AudioFocusManager * lets only one tab own audio focus at a time. When a new audible source * claims focus, the previous owner receives Stop and silences itself: an * HTMLMediaElement pauses, an AudioContext suspends, and a * SpeechSynthesisUtterance pauses. * * The Web Speech tab uses the test fake-synth voice "it-IT-noend" * (eSuppressEnd) so the utterance stays speaking through audio-focus * competition and FakeSynth's OnPause callback fires the pause event on Stop. * That makes the test mechanism platform-independent — real-platform Pause * support (macOS/Windows have it; Linux/Android don't, tracked by Bug * 2038329 / Bug 1238538) is exercised separately by the underlying Pause * code path. */ const PAGE_HTML_MEDIA = "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_autoplay.html"; const PAGE_WEB_AUDIO = "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_web_audio.html"; const PAGE_WEB_SPEECH = "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_web_speech.html"; const HTML_MEDIA_ID = "autoplay"; // Each source describes how to open it, start it, wait for its silence // response (when it loses audio focus) and tear it down. const HTMLMediaSource = { name: "HTMLMediaElement", page: PAGE_HTML_MEDIA, start: tab => checkOrWaitUntilMediaStartedPlaying(tab, HTML_MEDIA_ID), waitForSilenced: tab => checkOrWaitUntilMediaStoppedPlaying(tab, HTML_MEDIA_ID), cleanup: () => Promise.resolve(), }; const WebAudioSource = { name: "Web Audio", page: PAGE_WEB_AUDIO, start: startWebAudio, waitForSilenced: waitForAudioContextSuspended, cleanup: () => Promise.resolve(), }; const WebSpeechSource = { name: "Web Speech", page: PAGE_WEB_SPEECH, start: startWebSpeechAndWaitAudible, waitForSilenced: waitForSpeechPaused, cleanup: cancelSpeech, }; add_setup(async function () { await SpecialPowers.pushPrefEnv({ set: [ ["media.audioFocus.management", true], ["media.audioFocus.webaudio.enabled", true], ], }); }); add_task(async function test_html_media_vs_web_audio() { await testFocusStealingBetween(HTMLMediaSource, WebAudioSource); }); add_task(async function test_html_media_vs_web_speech() { await testFocusStealingBetween(HTMLMediaSource, WebSpeechSource); }); add_task(async function test_web_audio_vs_web_speech() { await testFocusStealingBetween(WebAudioSource, WebSpeechSource); }); // ----- Helpers ----- // Run focus competition between two sources in both directions: the prior // owner starts first then the new claimer takes focus and silences the prior // owner; then swap the roles and repeat. async function testFocusStealingBetween(a, b) { for (const [priorOwner, newClaimer] of [ [a, b], [b, a], ]) { info(`${priorOwner.name} (tab1) vs ${newClaimer.name} (tab2)`); const tab1 = await createLoadedTabWrapper(priorOwner.page, { needCheck: false, }); await priorOwner.start(tab1); const tab2 = await createLoadedTabWrapper(newClaimer.page, { needCheck: false, }); await newClaimer.start(tab2); info(`${priorOwner.name} in tab1 should be silenced by audio focus loss`); await priorOwner.waitForSilenced(tab1); await priorOwner.cleanup(tab1); await newClaimer.cleanup(tab2); await closeTabs([tab1, tab2]); } } function waitForControllerAudible(tab) { const controller = tab.linkedBrowser.browsingContext.mediaController; if (controller.isAudible) { return Promise.resolve(); } return new Promise(resolve => { controller.addEventListener("audiblechange", function handler() { if (controller.isAudible) { controller.removeEventListener("audiblechange", handler); resolve(); } }); }); } async function startWebAudio(tab) { const becameAudible = waitForControllerAudible(tab); await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { content.document.getElementById("start").click(); await content.wrappedJSObject.waitForAudioContextState("running"); }); await becameAudible; } async function startWebSpeech(tab) { await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { content.document.getElementById("start").click(); await content.wrappedJSObject.waitForSpeechStart(); }); } async function startWebSpeechAndWaitAudible(tab) { const becameAudible = waitForControllerAudible(tab); await startWebSpeech(tab); await becameAudible; } async function waitForAudioContextSuspended(tab) { await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { await content.wrappedJSObject.waitForAudioContextState("suspended"); }); ok(true, "AudioContext reached suspended state"); } async function waitForSpeechPaused(tab) { const result = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { return content.wrappedJSObject.waitForSpeechPause(); }); ok(result, "Web Speech reached paused state"); } async function cancelSpeech(tab) { await SpecialPowers.spawn(tab.linkedBrowser, [], () => { content.wrappedJSObject.cancelSpeech(); }); } async function closeTabs(tabs) { for (let tab of tabs) { await tab.close(); } }