summaryrefslogtreecommitdiffstats
path: root/dom/media/webrtc/tests/mochitests/test_setSinkId-stream-source.html
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/webrtc/tests/mochitests/test_setSinkId-stream-source.html')
-rw-r--r--dom/media/webrtc/tests/mochitests/test_setSinkId-stream-source.html138
1 files changed, 138 insertions, 0 deletions
diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId-stream-source.html b/dom/media/webrtc/tests/mochitests/test_setSinkId-stream-source.html
new file mode 100644
index 0000000000..5b044b0bc8
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_setSinkId-stream-source.html
@@ -0,0 +1,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>