tor-browser

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

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>