test_ondevicechange.html (6779B)
1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <script type="application/javascript" src="mediaStreamPlayback.js"></script> 6 </head> 7 <body> 8 <script type="application/javascript"> 9 "use strict"; 10 11 createHTML({ 12 title: "ondevicechange tests", 13 bug: "1152383" 14 }); 15 16 async function resolveOnEvent(target, name) { 17 return new Promise(r => target.addEventListener(name, r, {once: true})); 18 } 19 let eventCount = 0; 20 async function triggerVideoDevicechange() { 21 ++eventCount; 22 // "media.getusermedia.fake-camera-name" specifies the name of the single 23 // fake video camera. 24 // Changing the pref imitates replacing one device with another. 25 return pushPrefs(["media.getusermedia.fake-camera-name", 26 `devicechange ${eventCount}`]) 27 } 28 function addIframe() { 29 const iframe = document.createElement("iframe"); 30 // Workaround for bug 1743933 31 iframe.loadPromise = resolveOnEvent(iframe, "load"); 32 document.documentElement.appendChild(iframe); 33 return iframe; 34 } 35 36 runTest(async () => { 37 // A toplevel Window and an iframe Windows are compared for devicechange 38 // events. 39 const iframe1 = addIframe(); 40 const iframe2 = addIframe(); 41 await Promise.all([ 42 iframe1.loadPromise, 43 iframe2.loadPromise, 44 pushPrefs( 45 // Use the fake video backend to trigger devicechange events. 46 ["media.navigator.streams.fake", true], 47 // Loopback would override fake. 48 ["media.video_loopback_dev", ""], 49 // Make fake devices count as real, permission-wise, or devicechange 50 // events won't be exposed 51 ["media.navigator.permission.fake", true], 52 // For gUM. 53 ["media.navigator.permission.disabled", true] 54 ), 55 ]); 56 const topDevices = navigator.mediaDevices; 57 const frame1Devices = iframe1.contentWindow.navigator.mediaDevices; 58 const frame2Devices = iframe2.contentWindow.navigator.mediaDevices; 59 // Initialization of MediaDevices::mLastPhysicalDevices is triggered when 60 // ondevicechange is set but tests "media.getusermedia.fake-camera-name" 61 // asynchronously. Wait for getUserMedia() completion to ensure that the 62 // pref has been read before doDevicechanges() changes it. 63 frame1Devices.ondevicechange = () => {}; 64 const topEventPromise = resolveOnEvent(topDevices, "devicechange"); 65 const frame2EventPromise = resolveOnEvent(frame2Devices, "devicechange"); 66 (await frame1Devices.getUserMedia({video: true})).getTracks()[0].stop(); 67 68 await Promise.all([ 69 resolveOnEvent(frame1Devices, "devicechange"), 70 triggerVideoDevicechange(), 71 ]); 72 ok(true, 73 "devicechange event is fired when gUM has been in use"); 74 // The number of devices has not changed. Race a settled Promise to check 75 // that no devicechange event has been received in frame2. 76 const racer = {}; 77 is(await Promise.race([frame2EventPromise, racer]), racer, 78 "devicechange event is NOT fired in iframe2 for replaced device when " + 79 "gUM has NOT been in use"); 80 // getUserMedia() is invoked on frame2Devices after a first device list 81 // change but before returning to the previous state, in order to test that 82 // the device set is compared with the set after previous device list 83 // changes regardless of whether a "devicechange" event was previously 84 // dispatched. 85 (await frame2Devices.getUserMedia({video: true})).getTracks()[0].stop(); 86 // Revert device list change. 87 await Promise.all([ 88 resolveOnEvent(frame1Devices, "devicechange"), 89 resolveOnEvent(frame2Devices, "devicechange"), 90 SpecialPowers.popPrefEnv(), 91 ]); 92 ok(true, 93 "devicechange event is fired on return to previous list " + 94 "after gUM has been is use"); 95 96 const frame1EventPromise1 = resolveOnEvent(frame1Devices, "devicechange"); 97 while (true) { 98 const racePromise = Promise.race([ 99 frame1EventPromise1, 100 // 100ms is half the coalescing time in MediaManager::DeviceListChanged(). 101 wait(100, {type: "wait done"}), 102 ]); 103 await triggerVideoDevicechange(); 104 if ((await racePromise).type == "devicechange") { 105 ok(true, 106 "devicechange event is fired even when hardware changes continue"); 107 break; 108 } 109 } 110 111 is(await Promise.race([topEventPromise, racer]), racer, 112 "devicechange event is NOT fired for device replacements when " + 113 "gUM has NOT been in use"); 114 115 if (navigator.userAgent.includes("Android")) { 116 todo(false, "test assumes Firefox-for-Desktop specific API and behavior"); 117 return; 118 } 119 // Open a new tab, which is expected to receive focus and hide the first tab. 120 const tab = window.open(); 121 SimpleTest.registerCleanupFunction(() => tab.close()); 122 await Promise.all([ 123 resolveOnEvent(document, 'visibilitychange'), 124 resolveOnEvent(tab, 'focus'), 125 ]); 126 ok(tab.document.hasFocus(), "tab.document.hasFocus()"); 127 await Promise.all([ 128 resolveOnEvent(tab, 'blur'), 129 SpecialPowers.spawnChrome([], function focusUrlBar() { 130 this.browsingContext.topChromeWindow.gURLBar.focus(); 131 }), 132 ]); 133 ok(!tab.document.hasFocus(), "!tab.document.hasFocus()"); 134 is(document.visibilityState, 'hidden', 'visibilityState') 135 const frame1EventPromise2 = resolveOnEvent(frame1Devices, "devicechange"); 136 const tabDevices = tab.navigator.mediaDevices; 137 tabDevices.ondevicechange = () => {}; 138 const tabStream = await tabDevices.getUserMedia({video: true}); 139 // Trigger and await two devicechanges on tabDevices to wait long enough to 140 // provide that a devicechange on another MediaDevices would be received. 141 for (let i = 0; i < 2; ++i) { 142 await Promise.all([ 143 resolveOnEvent(tabDevices, "devicechange"), 144 triggerVideoDevicechange(), 145 ]); 146 }; 147 is(await Promise.race([frame1EventPromise2, racer]), racer, 148 "devicechange event is NOT fired while tab is in background"); 149 tab.close(); 150 await resolveOnEvent(document, 'visibilitychange'); 151 is(document.visibilityState, 'visible', 'visibilityState') 152 await frame1EventPromise2; 153 ok(true, "devicechange event IS fired when tab returns to foreground"); 154 155 const audioLoopbackDev = 156 SpecialPowers.getCharPref("media.audio_loopback_dev", ""); 157 if (!navigator.userAgent.includes("Linux")) { 158 todo_isnot(audioLoopbackDev, "", "audio_loopback_dev"); 159 return; 160 } 161 isnot(audioLoopbackDev, "", "audio_loopback_dev"); 162 await Promise.all([ 163 resolveOnEvent(topDevices, "devicechange"), 164 pushPrefs(["media.audio_loopback_dev", "none"]), 165 ]); 166 ok(true, 167 "devicechange event IS fired when last audio device is removed and " + 168 "gUM has NOT been in use"); 169 await Promise.all([ 170 resolveOnEvent(topDevices, "devicechange"), 171 pushPrefs(["media.audio_loopback_dev", audioLoopbackDev]), 172 ]); 173 ok(true, 174 "devicechange event IS fired when first audio device is added and " + 175 "gUM has NOT been in use"); 176 }); 177 178 </script> 179 </body> 180 </html>