test_setSinkId_after_loop.html (4328B)
1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <title>Test setSinkId() after looping</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 type="application/javascript"> 9 "use strict"; 10 11 /** 12 This test captures loopback device audio to test that a media element produces 13 audio output soon after a setSinkId() call after looping. In bug 1838756, 14 incorrect timekeeping caused silence in the output as long as the duration of 15 loops completed. 16 */ 17 add_task(async () => { 18 const audio = new Audio(); 19 audio.src = "sin-441-1s-44100.flac"; 20 audio.loop = true; 21 audio.controls = true; 22 // Mute initially so that delayed audio from before setSinkId does not 23 // trigger a false pass. 24 audio.muted = true; 25 document.body.appendChild(audio); 26 const canplaythroughPromise = new Promise(r => audio.oncanplaythrough = r); 27 // Let the audio loop at least twice, which would result in 2 extra seconds 28 // of silence with bug 1838756. 29 let count = 0; 30 const loopedPromise = new Promise(r => audio.onseeked = () => { 31 if (++count == 2) { 32 r(); 33 } 34 }); 35 36 await SpecialPowers.pushPrefEnv({set: [ 37 // skip gUM permission prompt 38 ["media.navigator.permission.disabled", true], 39 // enumerateDevices() without focus 40 ["media.devices.unfocused.enabled", true], 41 ]}); 42 let loopbackInputLabel = 43 SpecialPowers.getCharPref("media.audio_loopback_dev", ""); 44 if (!navigator.userAgent.includes("Linux")) { 45 todo_isnot(loopbackInputLabel, "", "audio_loopback_dev"); 46 return; 47 } 48 isnot(loopbackInputLabel, "", 49 "audio_loopback_dev. Try --use-test-media-devices."); 50 const loopbackStream = 51 await navigator.mediaDevices.getUserMedia({audio: true}); 52 is(loopbackStream.getTracks()[0].label, loopbackInputLabel, 53 "loopback track label"); 54 55 await canplaythroughPromise; 56 let loopbackNode; 57 try { 58 audio.play(); 59 await new Promise(r => audio.onplaying = r); 60 const ac = new AudioContext; 61 loopbackNode = ac.createMediaStreamSource(loopbackStream); 62 const processor1 = ac.createScriptProcessor(4096, 1, 0); 63 loopbackNode.connect(processor1); 64 65 // Check that the loopback stream contains silence now. 66 const {inputBuffer} = await new Promise(r => processor1.onaudioprocess = r); 67 loopbackNode.disconnect(); 68 is(inputBuffer.getChannelData(0).find(value => value != 0.0), undefined, 69 "should have silence"); 70 71 // Find output device 72 const devices = await navigator.mediaDevices.enumerateDevices(); 73 let loopbackOutputLabel = 74 SpecialPowers.getCharPref("media.cubeb.output_device", ""); 75 const outputDeviceInfo = devices.find( 76 ({kind, label}) => kind == "audiooutput" && label == loopbackOutputLabel 77 ); 78 ok(outputDeviceInfo, `found ${loopbackOutputLabel}`); 79 80 await loopedPromise; 81 await audio.setSinkId(outputDeviceInfo.deviceId); 82 audio.muted = false; 83 // Use a new ScriptProcessor so that the first audioprocess event provides 84 // the rendering thread time after unmuting. 85 const processor2 = ac.createScriptProcessor(4096, 1, 0); 86 loopbackNode.connect(processor2); 87 88 let seenAudioCount = 0; 89 let lastSample, firstTime; 90 while (seenAudioCount < ac.sampleRate) { 91 const event = await new Promise(r => processor2.onaudioprocess = r); 92 let samples = event.inputBuffer.getChannelData(0); 93 for (const sample of samples) { 94 if (!seenAudioCount) { 95 if (!firstTime) { 96 firstTime = event.playbackTime; 97 } 98 if (sample != 0.0) { 99 seenAudioCount = 1; 100 } else { 101 const delay = event.playbackTime - firstTime; 102 if (delay > 1.0) { 103 ok(false, `${delay} seconds passed without receiving audio`); 104 return; 105 } 106 } 107 } else if (sample != 0.0) { 108 ++seenAudioCount; 109 } else if (lastSample == 0.0) { 110 ok(false, "should not have two consecutive zero sample in audio"); 111 return; 112 } 113 lastSample = sample; 114 } 115 } 116 ok(true, "have one continuous second of audio"); 117 } finally { 118 if (loopbackNode) { 119 loopbackNode.disconnect(); 120 } 121 audio.pause(); 122 loopbackStream.getTracks()[0].stop(); 123 } 124 }); 125 </script> 126 </html>