test_setSinkId-echoCancellation.html (3643B)
1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <title></title> 5 <script src="mediaStreamPlayback.js"></script> 6 </head> 7 <script> 8 "use strict"; 9 10 createHTML({ 11 title: "Test echoCancellation with setSinkId()", 12 bug: "1849108", 13 visible: true, 14 }); 15 /** 16 This test captures audio from a loopback device to test echo cancellation. 17 */ 18 runTest(async () => { 19 await SpecialPowers.pushPrefEnv({set: [ 20 // skip selectAudioOutput/getUserMedia permission prompt 21 ["media.navigator.permission.disabled", true], 22 // enumerateDevices() without focus 23 ["media.devices.unfocused.enabled", true], 24 ]}); 25 26 const ac = new AudioContext(); 27 const dest = new MediaStreamAudioDestinationNode(ac); 28 const gain = new GainNode(ac, {gain: 0.5}); 29 gain.connect(dest); 30 // Use a couple of triangle waves for audio with some bandwidth. 31 // Pick incommensurable frequencies so that the audio is aperiodic. 32 // Perhaps that might help the AEC determine the delay. 33 const osc1 = 34 new OscillatorNode(ac, {type: "triangle", frequency: 200}); 35 const osc2 = 36 new OscillatorNode(ac, {type: "triangle", frequency: Math.PI * 100}); 37 osc1.connect(gain); 38 osc2.connect(gain); 39 osc1.start(); 40 osc2.start(); 41 const audio = new Audio(); 42 audio.srcObject = dest.stream; 43 audio.controls = true; 44 document.body.appendChild(audio); 45 46 // The loopback device is currenly only available on Linux. 47 let loopbackInputLabel = 48 SpecialPowers.getCharPref("media.audio_loopback_dev", ""); 49 if (!navigator.userAgent.includes("Linux")) { 50 todo_isnot(loopbackInputLabel, "", "audio_loopback_dev"); 51 return; 52 } 53 isnot(loopbackInputLabel, "", 54 "audio_loopback_dev. Use --use-test-media-devices."); 55 56 const loopbackStream = await navigator.mediaDevices.getUserMedia({ audio: { 57 echoCancellation: false, 58 autoGainControl: false, 59 noiseSuppression: false, 60 }}); 61 is(loopbackStream.getTracks()[0].label, loopbackInputLabel, 62 "loopback track label"); 63 64 // Check that the loopback stream contains silence now. 65 const loopbackNode = ac.createMediaStreamSource(loopbackStream); 66 const processor1 = ac.createScriptProcessor(4096, 1, 0); 67 loopbackNode.connect(processor1); 68 const {inputBuffer} = await new Promise(r => processor1.onaudioprocess = r); 69 loopbackNode.disconnect(); 70 is(inputBuffer.getChannelData(0).find(value => value != 0.0), undefined, 71 "should have silence in loopback input"); 72 73 // Find the loopback output device 74 const devices = await navigator.mediaDevices.enumerateDevices(); 75 let loopbackOutputLabel = 76 SpecialPowers.getCharPref("media.cubeb.output_device", ""); 77 const outputDeviceInfo = devices.find( 78 ({kind, label}) => kind == "audiooutput" && label == loopbackOutputLabel 79 ); 80 ok(outputDeviceInfo, `found "${loopbackOutputLabel}"`); 81 82 await audio.setSinkId(outputDeviceInfo.deviceId); 83 await audio.play(); 84 85 const analyser = new AudioStreamAnalyser(ac, loopbackStream); 86 const bin1 = analyser.binIndexForFrequency(osc1.frequency.value); 87 const bin2 = analyser.binIndexForFrequency(osc2.frequency.value); 88 try { 89 analyser.enableDebugCanvas(); 90 // Check for audio with AEC. 91 await analyser.waitForAnalysisSuccess(array => { 92 return array[bin1] > 200 && array[bin2] > 200; 93 }); 94 95 // Check echo cancellation. 96 await loopbackStream.getTracks()[0].applyConstraints({ 97 echoCancellation: true, 98 autoGainControl: false, 99 noiseSuppression: false, 100 }); 101 await analyser.waitForAnalysisSuccess(array => { 102 return !array.find(bin => bin > 50); 103 }); 104 } finally { 105 await ac.close(); 106 loopbackStream.getTracks()[0].stop(); 107 } 108 }); 109 </script> 110 </html>