<!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", "");
  if (!navigator.userAgent.includes("Linux")) {
    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>
