test_setSinkId-stream-source.html (5253B)
1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <title>Test setSinkId() on an Audio element with MediaStream source</title> 5 <script src="/tests/SimpleTest/SimpleTest.js"></script> 6 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> 7 </head> 8 <script> 9 "use strict"; 10 11 SimpleTest.requestFlakyTimeout("delays to trigger races"); 12 13 function maybeTodoIs(a, b, msg) { 14 if (Object.is(a, b)) { 15 is(a, b, msg); 16 } else { 17 todo(false, msg, `got ${a}, wanted ${b}`); 18 } 19 } 20 21 add_task(async () => { 22 await SpecialPowers.pushPrefEnv({set: [ 23 // skip selectAudioOutput/getUserMedia permission prompt 24 ["media.navigator.permission.disabled", true], 25 // enumerateDevices() without focus 26 ["media.devices.unfocused.enabled", true], 27 ]}); 28 29 const audio = new Audio(); 30 const stream1 = new AudioContext().createMediaStreamDestination().stream; 31 audio.srcObject = stream1; 32 audio.controls = true; 33 document.body.appendChild(audio); 34 await audio.play(); 35 36 // Expose an audio output device. 37 SpecialPowers.wrap(document).notifyUserGestureActivation(); 38 const {deviceId, label: label1} = await navigator.mediaDevices.selectAudioOutput(); 39 isnot(deviceId, "", "deviceId from selectAudioOutput()"); 40 41 // pre-fill devices cache to reduce delay until MediaStreamRenderer acts on 42 // setSinkId(). 43 await navigator.mediaDevices.enumerateDevices(); 44 45 SpecialPowers.pushPrefEnv({set: [ 46 ["media.cubeb.slow_stream_init_ms", 200], 47 ]}); 48 49 // When playback is stopped before setSinkId()'s parallel step "Switch the 50 // underlying audio output device for element to the audio device identified 51 // by sinkId" completes, then whether that step "failed" might be debatable. 52 // https://w3c.github.io/mediacapture-output/#dom-htmlmediaelement-setsinkid 53 // Gecko chooses to resolve the setSinkId() promise so that behavior does 54 // not depend on a race (assuming that switching would succeed if allowed to 55 // complete). 56 async function expectSetSinkIdResolutionWithSubsequentAction( 57 deviceId, action, actionLabel) { 58 let p = audio.setSinkId(deviceId); 59 // Wait long enough for MediaStreamRenderer to initiate a switch to the new 60 // device, but not so long as the new device's graph has started. 61 await new Promise(r => setTimeout(r, 100)); 62 action(); 63 const resolved = await p.then(() => true, () => false); 64 ok(resolved, `setSinkId before ${actionLabel}`); 65 } 66 67 await expectSetSinkIdResolutionWithSubsequentAction( 68 deviceId, () => audio.pause(), "pause"); 69 70 await audio.setSinkId(""); 71 await audio.play(); 72 await expectSetSinkIdResolutionWithSubsequentAction( 73 deviceId, () => audio.srcObject = null, "null srcObject"); 74 75 await audio.setSinkId(""); 76 audio.srcObject = stream1; 77 await audio.play(); 78 await expectSetSinkIdResolutionWithSubsequentAction( 79 deviceId, () => stream1.getTracks()[0].stop(), "stop"); 80 81 const stream2 = new AudioContext().createMediaStreamDestination().stream; 82 audio.srcObject = stream2; 83 await audio.play(); 84 85 let loopbackInputLabel = 86 SpecialPowers.getCharPref("media.audio_loopback_dev", ""); 87 if (!navigator.userAgent.includes("Linux")) { 88 todo_isnot(loopbackInputLabel, "", "audio_loopback_dev"); 89 return; 90 } 91 isnot(loopbackInputLabel, "", 92 "audio_loopback_dev. Use --use-test-media-devices."); 93 94 // Expose more output devices 95 SpecialPowers.pushPrefEnv({set: [ 96 ["media.audio_loopback_dev", ""], 97 ]}); 98 const inputStream = await navigator.mediaDevices.getUserMedia({audio: true}); 99 inputStream.getTracks()[0].stop(); 100 const devices = await navigator.mediaDevices.enumerateDevices(); 101 const {deviceId: otherDeviceId} = devices.find( 102 ({kind, label}) => kind == "audiooutput" && label != label1); 103 ok(otherDeviceId, "id2"); 104 isnot(otherDeviceId, deviceId, "other id is different"); 105 106 // With multiple setSinkId() calls having `sinkId` parameters differing from 107 // the element's `sinkId` attribute, the order of each "switch the 108 // underlying audio output device" and each subsequent Promise settling is 109 // not clearly specified due to parallel steps for different calls not 110 // specifically running on the same task queue. 111 // https://w3c.github.io/mediacapture-output/#dom-htmlmediaelement-setsinkid 112 // Gecko aims to switch and settle in the same order as corresonding 113 // setSinkId() calls, but this does not necessarily happen - bug 1874629. 114 async function setSinkIdTwice(id1, id2, label) { 115 const p1 = audio.setSinkId(id1); 116 const p2 = audio.setSinkId(id2); 117 let p1Settled = false; 118 let p1SettledFirst; 119 const results = await Promise.allSettled([ 120 p1.finally(() => p1Settled = true), 121 p2.finally(() => p1SettledFirst = p1Settled), 122 ]); 123 maybeTodoIs(results[0].status, "fulfilled", `${label}: results[0]`); 124 maybeTodoIs(results[1].status, "fulfilled", `${label}: results[1]`); 125 maybeTodoIs(p1SettledFirst, true, 126 `${label}: first promise should settle first`); 127 } 128 129 is(audio.sinkId, deviceId, "sinkId after stop"); 130 await setSinkIdTwice(otherDeviceId, "", "other then empty"); 131 132 maybeTodoIs(audio.sinkId, "", "sinkId after empty"); 133 await setSinkIdTwice(deviceId, otherDeviceId, "both not empty"); 134 135 stream2.getTracks()[0].stop() 136 }); 137 </script> 138 </html>