summaryrefslogtreecommitdiffstats
path: root/dom/media/webrtc/tests/mochitests/test_setSinkId-stream-source.html
blob: 5b044b0bc8c343d0f601e9f7216acb3f25c6f0df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<!DOCTYPE HTML>
<html>
<head>
<title>Test setSinkId() on an Audio element with MediaStream source</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<script>
"use strict";

SimpleTest.requestFlakyTimeout("delays to trigger races");

function maybeTodoIs(a, b, msg) {
  if (Object.is(a, b)) {
    is(a, b, msg);
  } else {
    todo(false, msg, `got ${a}, wanted ${b}`);
  }
}

add_task(async () => {
  await SpecialPowers.pushPrefEnv({set: [
    // skip selectAudioOutput/getUserMedia permission prompt
    ["media.navigator.permission.disabled", true],
    // enumerateDevices() without focus
    ["media.devices.unfocused.enabled", true],
  ]});

  const audio = new Audio();
  const stream1 = new AudioContext().createMediaStreamDestination().stream;
  audio.srcObject = stream1;
  audio.controls = true;
  document.body.appendChild(audio);
  await audio.play();

  // Expose an audio output device.
  SpecialPowers.wrap(document).notifyUserGestureActivation();
  const {deviceId, label: label1} = await navigator.mediaDevices.selectAudioOutput();
  isnot(deviceId, "", "deviceId from selectAudioOutput()");

  // pre-fill devices cache to reduce delay until MediaStreamRenderer acts on
  // setSinkId().
  await navigator.mediaDevices.enumerateDevices();

  SpecialPowers.pushPrefEnv({set: [
    ["media.cubeb.slow_stream_init_ms", 200],
  ]});

  // When playback is stopped before setSinkId()'s parallel step "Switch the
  // underlying audio output device for element to the audio device identified
  // by sinkId" completes, then whether that step "failed" might be debatable.
  // https://w3c.github.io/mediacapture-output/#dom-htmlmediaelement-setsinkid
  // Gecko chooses to resolve the setSinkId() promise so that behavior does
  // not depend on a race (assuming that switching would succeed if allowed to
  // complete).
  async function expectSetSinkIdResolutionWithSubsequentAction(
    deviceId, action, actionLabel) {
    let p = audio.setSinkId(deviceId);
    // Wait long enough for MediaStreamRenderer to initiate a switch to the new
    // device, but not so long as the new device's graph has started.
    await new Promise(r => setTimeout(r, 100));
    action();
    const resolved = await p.then(() => true, () => false);
    ok(resolved, `setSinkId before ${actionLabel}`);
  }

  await expectSetSinkIdResolutionWithSubsequentAction(
    deviceId, () => audio.pause(), "pause");

  await audio.setSinkId("");
  await audio.play();
  await expectSetSinkIdResolutionWithSubsequentAction(
    deviceId, () => audio.srcObject = null, "null srcObject");

  await audio.setSinkId("");
  audio.srcObject = stream1;
  await audio.play();
  await expectSetSinkIdResolutionWithSubsequentAction(
    deviceId, () => stream1.getTracks()[0].stop(), "stop");

  const stream2 = new AudioContext().createMediaStreamDestination().stream;
  audio.srcObject = stream2;
  await audio.play();

  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. Use --use-test-media-devices.");

  // Expose more output devices
  SpecialPowers.pushPrefEnv({set: [
    ["media.audio_loopback_dev", ""],
  ]});
  const inputStream = await navigator.mediaDevices.getUserMedia({audio: true});
  inputStream.getTracks()[0].stop();
  const devices = await navigator.mediaDevices.enumerateDevices();
  const {deviceId: otherDeviceId} = devices.find(
    ({kind, label}) => kind == "audiooutput" && label != label1);
  ok(otherDeviceId, "id2");
  isnot(otherDeviceId, deviceId, "other id is different");

  // With multiple setSinkId() calls having `sinkId` parameters differing from
  // the element's `sinkId` attribute, the order of each "switch the
  // underlying audio output device" and each subsequent Promise settling is
  // not clearly specified due to parallel steps for different calls not
  // specifically running on the same task queue.
  // https://w3c.github.io/mediacapture-output/#dom-htmlmediaelement-setsinkid
  // Gecko aims to switch and settle in the same order as corresonding
  // setSinkId() calls, but this does not necessarily happen - bug 1874629.
  async function setSinkIdTwice(id1, id2, label) {
    const p1 = audio.setSinkId(id1);
    const p2 = audio.setSinkId(id2);
    let p1Settled = false;
    let p1SettledFirst;
    const results = await Promise.allSettled([
      p1.finally(() => p1Settled = true),
      p2.finally(() => p1SettledFirst = p1Settled),
    ]);
    maybeTodoIs(results[0].status, "fulfilled", `${label}: results[0]`);
    maybeTodoIs(results[1].status, "fulfilled", `${label}: results[1]`);
    maybeTodoIs(p1SettledFirst, true,
                `${label}: first promise should settle first`);
  }

  is(audio.sinkId, deviceId, "sinkId after stop");
  await setSinkIdTwice(otherDeviceId, "", "other then empty");

  maybeTodoIs(audio.sinkId, "", "sinkId after empty");
  await setSinkIdTwice(deviceId, otherDeviceId, "both not empty");

  stream2.getTracks()[0].stop()
});
</script>
</html>