<!DOCTYPE HTML>
<html>
<head>
<title>Test setSinkId() after looping</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<script type="application/javascript">
"use strict";

/**
This test captures loopback device audio to test that a media element produces
audio output soon after a setSinkId() call after looping.  In bug 1838756,
incorrect timekeeping caused silence in the output as long as the duration of
loops completed.
 **/
add_task(async () => {
  const audio = new Audio();
  audio.src = "sin-441-1s-44100.flac";
  audio.loop = true;
  audio.controls = true;
  // Mute initially so that delayed audio from before setSinkId does not
  // trigger a false pass.
  audio.muted = true;
  document.body.appendChild(audio);
  const canplaythroughPromise = new Promise(r => audio.oncanplaythrough = r);
  // Let the audio loop at least twice, which would result in 2 extra seconds
  // of silence with bug 1838756.
  let count = 0;
  const loopedPromise = new Promise(r => audio.onseeked = () => {
    if (++count == 2) {
      r();
    }
  });

  await SpecialPowers.pushPrefEnv({set: [
    // skip gUM permission prompt
    ["media.navigator.permission.disabled", true],
    // enumerateDevices() without focus
    ["media.devices.unfocused.enabled", true],
  ]});
  let loopbackInputLabel =
      SpecialPowers.getCharPref("media.audio_loopback_dev", "");
  if (!navigator.userAgent.includes("Linux")) {
    todo_isnot(loopbackInputLabel, "", "audio_loopback_dev");
    return;
  }
  isnot(loopbackInputLabel, "",
        "audio_loopback_dev. Try --use-test-media-devices.");
  const loopbackStream =
        await navigator.mediaDevices.getUserMedia({audio: true});
  is(loopbackStream.getTracks()[0].label, loopbackInputLabel,
     "loopback track label");

  await canplaythroughPromise;
  let loopbackNode;
  try {
    audio.play();
    await new Promise(r => audio.onplaying = r);
    const ac = new AudioContext;
    loopbackNode = ac.createMediaStreamSource(loopbackStream);
    const processor1 = ac.createScriptProcessor(4096, 1, 0);
    loopbackNode.connect(processor1);

    // Check that the loopback stream contains silence now.
    const {inputBuffer} = await new Promise(r => processor1.onaudioprocess = r);
    loopbackNode.disconnect();
    is(inputBuffer.getChannelData(0).find(value => value != 0.0), undefined,
       "should have silence");

    // Find output device
    const devices = await navigator.mediaDevices.enumerateDevices();
    let loopbackOutputLabel =
        SpecialPowers.getCharPref("media.cubeb.output_device", "");
    const outputDeviceInfo = devices.find(
      ({kind, label}) => kind == "audiooutput" && label == loopbackOutputLabel
    );
    ok(outputDeviceInfo, `found ${loopbackOutputLabel}`);

    await loopedPromise;
    await audio.setSinkId(outputDeviceInfo.deviceId);
    audio.muted = false;
    // Use a new ScriptProcessor so that the first audioprocess event provides
    // the rendering thread time after unmuting.
    const processor2 = ac.createScriptProcessor(4096, 1, 0);
    loopbackNode.connect(processor2);

    let seenAudioCount = 0;
    let lastSample, firstTime;
    while (seenAudioCount < ac.sampleRate) {
      const event = await new Promise(r => processor2.onaudioprocess = r);
      let samples = event.inputBuffer.getChannelData(0);
      for (const sample of samples) {
        if (!seenAudioCount) {
          if (!firstTime) {
            firstTime = event.playbackTime;
          }
          if (sample != 0.0) {
            seenAudioCount = 1;
          } else {
            const delay = event.playbackTime - firstTime;
            if (delay > 1.0) {
              ok(false, `${delay} seconds passed without receiving audio`);
              return;
            }
          }
        } else if (sample != 0.0) {
          ++seenAudioCount;
        } else if (lastSample == 0.0) {
          ok(false, "should not have two consecutive zero sample in audio");
          return;
        }
        lastSample = sample;
      }
    }
    ok(true, "have one continuous second of audio");
  } finally {
    if (loopbackNode) {
      loopbackNode.disconnect();
    }
    audio.pause();
    loopbackStream.getTracks()[0].stop();
  }
});
</script>
</html>
