summaryrefslogtreecommitdiffstats
path: root/dom/media/webrtc/tests/mochitests/test_ondevicechange.html
blob: 4358d9d748e78fd4b422a88b4bed299892843595 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
<!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>