Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!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>