Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/**
* media.audio_session and media.audio_focus Glean telemetry. Each task resets
* FOG, drives a real path (adoption, arbitration, interrupt/resume) with
* dom.audio_session.enabled on, and asserts the recorded probe values.
* Content-process probes are read after flushing child FOG data to the parent.
*/
"use strict";
const MEDIA_ELEMENT_URL = GetTestWebBasedURL("file_autoplay.html");
const TOP_URL =
const IFRAME_URL =
const TOP_VIDEO_ID = "audible_video";
const IFRAME_VIDEO_ID = "iframe_video";
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["dom.audio_session.enabled", true],
["media.audioFocus.webaudio.enabled", true],
["media.mediacontrol.testingevents.enabled", true],
],
});
});
// navigator.audioSession use and an explicit type assignment are recorded.
add_task(async function test_adoption() {
Services.fog.testResetFOG();
info("open an audible page and set an explicit audioSession.type");
const tab = await createLoadedTabWrapper(MEDIA_ELEMENT_URL, {
needCheck: false,
});
await checkOrWaitUntilMediaStartedPlaying(tab, "autoplay");
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
content.navigator.audioSession.type = "playback";
});
await Services.fog.testFlushAllChildren();
Assert.greaterOrEqual(
Glean.mediaAudioSession.apiUsed.testGetValue() ?? 0,
1,
"api_used recorded"
);
is(
Glean.mediaAudioSession.typeSet.playback.testGetValue(),
1,
"type_set[playback] recorded"
);
await tab.close();
});
// An audible playback element becomes the selected session; a system
// interruption is recorded and its end is recorded as a recovery, and the
// element's resume is recorded.
add_task(async function test_interrupt_and_resume() {
Services.fog.testResetFOG();
const tab = await createLoadedTabWrapper(MEDIA_ELEMENT_URL, {
needCheck: false,
});
const controller = tab.linkedBrowser.browsingContext.mediaController;
info("play audible media so it becomes the selected audio session");
await checkOrWaitUntilMediaStartedPlaying(tab, "autoplay");
await checkOrWaitUntilControllerBecomeActive(tab);
Assert.greaterOrEqual(
Glean.mediaAudioSession.selectedChanged.testGetValue() ?? 0,
1,
"selected_changed recorded once a session is selected"
);
info("auto-typed selected session resolves to playback for audible media");
Assert.greaterOrEqual(
Glean.mediaAudioSession.effectiveAutoType.playback.testGetValue() ?? 0,
1,
"effective_auto_type[playback] recorded for the auto-typed selected session"
);
info("a system interruption pauses the media");
controller.pause("system-transient");
await checkOrWaitUntilMediaStoppedPlaying(tab, "autoplay");
is(
Glean.mediaAudioFocus.interruptCount.system_transient.testGetValue(),
1,
"interrupt_count[system_transient] recorded"
);
info("the interruption ends; the media resumes and recovers");
controller.resume();
await checkOrWaitUntilMediaStartedPlaying(tab, "autoplay");
await Services.fog.testFlushAllChildren();
const outcomes = Glean.mediaAudioFocus.interruptOutcome.testGetValue() ?? [];
// One terminal outcome per interruption, so this reconciles with the single
// interrupt_count above.
Assert.deepEqual(
outcomes.map(e => e.extra?.outcome),
["recovered"],
"interrupt_outcome recorded exactly one recovery"
);
is(
outcomes[0].extra?.cause,
"system_transient",
"interrupt_outcome cause is the transient loss"
);
Assert.greaterOrEqual(
Glean.mediaAudioFocus.resumeDecision.media.testGetValue() ?? 0,
1,
"resume_decision[media] recorded"
);
await tab.close();
});
// interrupt_outcome is per suspended session: an interruption that suspends two
// sessions reports a recovery for each once both are returned to active.
add_task(async function test_interrupt_outcome_is_per_suspended_session() {
Services.fog.testResetFOG();
// Ambient is non-exclusive, so the main frame's session stays active
// alongside the iframe's exclusive one and the interruption suspends both.
info("main frame plays an audible video under an ambient session");
const tab = await createLoadedTabWrapper(TOP_URL);
const controller = tab.linkedBrowser.browsingContext.mediaController;
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
content.navigator.audioSession.type = "ambient";
});
await playMedia(tab, TOP_VIDEO_ID);
info("inject a cross-origin iframe and start its audible video");
await SpecialPowers.spawn(tab.linkedBrowser, [IFRAME_URL], async url => {
const iframe = content.document.createElement("iframe");
iframe.src = url;
content.document.body.appendChild(iframe);
await new Promise(r => (iframe.onload = r));
});
const iframeBC = tab.linkedBrowser.browsingContext.children[0];
await SpecialPowers.spawn(iframeBC, [IFRAME_VIDEO_ID], async id => {
await content.document.getElementById(id).play();
});
info("iframe takes an exclusive playback session");
await SpecialPowers.spawn(iframeBC, [], () => {
content.wrappedJSObject.setIframeAudioSessionType("playback");
});
// Only the iframe can produce a `playback` effective type here, and selection
// picks it only once its browsing context is audible, so this is the signal
// that both sessions are active and interruptible.
await waitForEffectiveAudioSessionType(controller, "playback");
info("a system interruption suspends both sessions");
controller.pause("system-transient");
await checkOrWaitUntilMediaStoppedPlaying(tab, TOP_VIDEO_ID);
is(
Glean.mediaAudioFocus.interruptCount.system_transient.testGetValue(),
1,
"one interruption counted for the two suspended sessions"
);
info("the interruption ends");
controller.resume();
await Services.fog.testFlushAllChildren();
Assert.deepEqual(
(Glean.mediaAudioFocus.interruptOutcome.testGetValue() ?? []).map(
e => e.extra?.outcome
),
["recovered", "recovered"],
"a recovery for each of the two suspended sessions"
);
await tab.close();
});
// A resume that does not follow an interruption reports no outcome at all.
add_task(async function test_resume_without_interruption_records_no_outcome() {
Services.fog.testResetFOG();
const tab = await createLoadedTabWrapper(MEDIA_ELEMENT_URL, {
needCheck: false,
});
const controller = tab.linkedBrowser.browsingContext.mediaController;
await checkOrWaitUntilMediaStartedPlaying(tab, "autoplay");
await checkOrWaitUntilControllerBecomeActive(tab);
info("resume without any preceding interruption");
controller.resume();
await Services.fog.testFlushAllChildren();
Assert.deepEqual(
Glean.mediaAudioFocus.interruptOutcome.testGetValue() ?? [],
[],
"no interrupt_outcome recorded when nothing was interrupted"
);
await tab.close();
});
// Two exclusive sessions in one tab (main frame + cross-origin iframe): when
// the iframe's exclusive session takes over, arbitration inactivates the main
// frame's session.
add_task(async function test_arbitration_inactivation() {
Services.fog.testResetFOG();
info("main frame plays an exclusive playback video");
const tab = await createLoadedTabWrapper(TOP_URL);
await playMedia(tab, TOP_VIDEO_ID);
info("inject a cross-origin iframe and start its audible video");
await SpecialPowers.spawn(tab.linkedBrowser, [IFRAME_URL], async url => {
const iframe = content.document.createElement("iframe");
iframe.src = url;
content.document.body.appendChild(iframe);
await new Promise(r => (iframe.onload = r));
});
const iframeBC = tab.linkedBrowser.browsingContext.children[0];
await SpecialPowers.spawn(iframeBC, [IFRAME_VIDEO_ID], async id => {
await content.document.getElementById(id).play();
});
info("iframe takes an exclusive session, displacing the main frame");
await SpecialPowers.spawn(iframeBC, [], () => {
content.wrappedJSObject.setIframeAudioSessionType("playback");
});
await TestUtils.waitForCondition(
() =>
(Glean.mediaAudioSession.inactivatedByArbitration.testGetValue() ?? 0) >=
1,
"waiting for arbitration to inactivate the displaced session"
);
Assert.greaterOrEqual(
Glean.mediaAudioSession.inactivatedByArbitration.testGetValue() ?? 0,
1,
"inactivated_by_arbitration recorded"
);
await tab.close();
});