Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script type="application/javascript" src="mediaStreamPlayback.js"></script>
</head>
<body>
<script type="application/javascript">
"use strict";
createHTML({
title: "ondevicechange tests",
bug: "1152383"
});
async function resolveOnEvent(target, name) {
return new Promise(r => target.addEventListener(name, r, {once: true}));
}
let eventCount = 0;
async function triggerVideoDevicechange() {
++eventCount;
// "media.getusermedia.fake-camera-name" specifies the name of the single
// fake video camera.
// Changing the pref imitates replacing one device with another.
return pushPrefs(["media.getusermedia.fake-camera-name",
`devicechange ${eventCount}`])
}
function addIframe() {
const iframe = document.createElement("iframe");
// Workaround for bug 1743933
iframe.loadPromise = resolveOnEvent(iframe, "load");
document.documentElement.appendChild(iframe);
return iframe;
}
runTest(async () => {
// A toplevel Window and an iframe Windows are compared for devicechange
// events.
const iframe1 = addIframe();
const iframe2 = addIframe();
await Promise.all([
iframe1.loadPromise,
iframe2.loadPromise,
pushPrefs(
// Use the fake video backend to trigger devicechange events.
["media.navigator.streams.fake", true],
// Loopback would override fake.
["media.video_loopback_dev", ""],
// Make fake devices count as real, permission-wise, or devicechange
// events won't be exposed
["media.navigator.permission.fake", true],
// For gUM.
["media.navigator.permission.disabled", true]
),
]);
const topDevices = navigator.mediaDevices;
const frame1Devices = iframe1.contentWindow.navigator.mediaDevices;
const frame2Devices = iframe2.contentWindow.navigator.mediaDevices;
// Initialization of MediaDevices::mLastPhysicalDevices is triggered when
// ondevicechange is set but tests "media.getusermedia.fake-camera-name"
// asynchronously. Wait for getUserMedia() completion to ensure that the
// pref has been read before doDevicechanges() changes it.
frame1Devices.ondevicechange = () => {};
const topEventPromise = resolveOnEvent(topDevices, "devicechange");
const frame2EventPromise = resolveOnEvent(frame2Devices, "devicechange");
(await frame1Devices.getUserMedia({video: true})).getTracks()[0].stop();
await Promise.all([
resolveOnEvent(frame1Devices, "devicechange"),
triggerVideoDevicechange(),
]);
ok(true,
"devicechange event is fired when gUM has been in use");
// The number of devices has not changed. Race a settled Promise to check
// that no devicechange event has been received in frame2.
const racer = {};
is(await Promise.race([frame2EventPromise, racer]), racer,
"devicechange event is NOT fired in iframe2 for replaced device when " +
"gUM has NOT been in use");
// getUserMedia() is invoked on frame2Devices after a first device list
// change but before returning to the previous state, in order to test that
// the device set is compared with the set after previous device list
// changes regardless of whether a "devicechange" event was previously
// dispatched.
(await frame2Devices.getUserMedia({video: true})).getTracks()[0].stop();
// Revert device list change.
await Promise.all([
resolveOnEvent(frame1Devices, "devicechange"),
resolveOnEvent(frame2Devices, "devicechange"),
SpecialPowers.popPrefEnv(),
]);
ok(true,
"devicechange event is fired on return to previous list " +
"after gUM has been is use");
const frame1EventPromise1 = resolveOnEvent(frame1Devices, "devicechange");
while (true) {
const racePromise = Promise.race([
frame1EventPromise1,
// 100ms is half the coalescing time in MediaManager::DeviceListChanged().
wait(100, {type: "wait done"}),
]);
await triggerVideoDevicechange();
if ((await racePromise).type == "devicechange") {
ok(true,
"devicechange event is fired even when hardware changes continue");
break;
}
}
is(await Promise.race([topEventPromise, racer]), racer,
"devicechange event is NOT fired for device replacements when " +
"gUM has NOT been in use");
if (navigator.userAgent.includes("Android")) {
todo(false, "test assumes Firefox-for-Desktop specific API and behavior");
return;
}
// Open a new tab, which is expected to receive focus and hide the first tab.
const tab = window.open();
SimpleTest.registerCleanupFunction(() => tab.close());
await Promise.all([
resolveOnEvent(document, 'visibilitychange'),
resolveOnEvent(tab, 'focus'),
]);
ok(tab.document.hasFocus(), "tab.document.hasFocus()");
await Promise.all([
resolveOnEvent(tab, 'blur'),
SpecialPowers.spawnChrome([], function focusUrlBar() {
this.browsingContext.topChromeWindow.gURLBar.focus();
}),
]);
ok(!tab.document.hasFocus(), "!tab.document.hasFocus()");
is(document.visibilityState, 'hidden', 'visibilityState')
const frame1EventPromise2 = resolveOnEvent(frame1Devices, "devicechange");
const tabDevices = tab.navigator.mediaDevices;
tabDevices.ondevicechange = () => {};
const tabStream = await tabDevices.getUserMedia({video: true});
// Trigger and await two devicechanges on tabDevices to wait long enough to
// provide that a devicechange on another MediaDevices would be received.
for (let i = 0; i < 2; ++i) {
await Promise.all([
resolveOnEvent(tabDevices, "devicechange"),
triggerVideoDevicechange(),
]);
};
is(await Promise.race([frame1EventPromise2, racer]), racer,
"devicechange event is NOT fired while tab is in background");
tab.close();
await resolveOnEvent(document, 'visibilitychange');
is(document.visibilityState, 'visible', 'visibilityState')
await frame1EventPromise2;
ok(true, "devicechange event IS fired when tab returns to foreground");
const audioLoopbackDev =
SpecialPowers.getCharPref("media.audio_loopback_dev", "");
const desktopLinux = navigator.userAgent.includes("Linux") &&
!navigator.userAgent.includes("Android");
if (!desktopLinux) {
todo_isnot(audioLoopbackDev, "", "audio_loopback_dev");
return;
}
isnot(audioLoopbackDev, "", "audio_loopback_dev");
await Promise.all([
resolveOnEvent(topDevices, "devicechange"),
pushPrefs(["media.audio_loopback_dev", "none"]),
]);
ok(true,
"devicechange event IS fired when last audio device is removed and " +
"gUM has NOT been in use");
await Promise.all([
resolveOnEvent(topDevices, "devicechange"),
pushPrefs(["media.audio_loopback_dev", audioLoopbackDev]),
]);
ok(true,
"devicechange event IS fired when first audio device is added and " +
"gUM has NOT been in use");
});
</script>
</body>
</html>