tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>