summaryrefslogtreecommitdiffstats
path: root/dom/media/webaudio/test/test_audioContextSuspendResumeClose.html
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /dom/media/webaudio/test/test_audioContextSuspendResumeClose.html
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/webaudio/test/test_audioContextSuspendResumeClose.html')
-rw-r--r--dom/media/webaudio/test/test_audioContextSuspendResumeClose.html419
1 files changed, 419 insertions, 0 deletions
diff --git a/dom/media/webaudio/test/test_audioContextSuspendResumeClose.html b/dom/media/webaudio/test/test_audioContextSuspendResumeClose.html
new file mode 100644
index 0000000000..36cf8f720c
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioContextSuspendResumeClose.html
@@ -0,0 +1,419 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test suspend, resume and close method of the AudioContext</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function tryToCreateNodeOnClosedContext(ctx) {
+ is(ctx.state, "closed", "The context is in closed state");
+
+ [ { name: "createBufferSource" },
+ { name: "createMediaStreamDestination",
+ onOfflineAudioContext: false},
+ { name: "createScriptProcessor" },
+ { name: "createStereoPanner" },
+ { name: "createAnalyser" },
+ { name: "createGain" },
+ { name: "createDelay" },
+ { name: "createBiquadFilter" },
+ { name: "createWaveShaper" },
+ { name: "createPanner" },
+ { name: "createConvolver" },
+ { name: "createChannelSplitter" },
+ { name: "createChannelMerger" },
+ { name: "createDynamicsCompressor" },
+ { name: "createOscillator" },
+ { name: "createMediaElementSource",
+ args: [new Audio()],
+ onOfflineAudioContext: false },
+ { name: "createMediaStreamSource",
+ args: [(new AudioContext()).createMediaStreamDestination().stream],
+ onOfflineAudioContext: false } ].forEach(function(e) {
+
+ if (e.onOfflineAudioContext == false &&
+ ctx instanceof OfflineAudioContext) {
+ return;
+ }
+
+ expectNoException(function() {
+ ctx[e.name].apply(ctx, e.args);
+ }, DOMException.INVALID_STATE_ERR);
+ });
+}
+
+function loadFile(url, callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = function() {
+ callback(xhr.response);
+ };
+ xhr.send();
+}
+
+// createBuffer, createPeriodicWave and decodeAudioData should work on a context
+// that has `state` == "closed"
+function tryLegalOpeerationsOnClosedContext(ctx) {
+ is(ctx.state, "closed", "The context is in closed state");
+
+ [ { name: "createBuffer",
+ args: [1, 44100, 44100] },
+ { name: "createPeriodicWave",
+ args: [new Float32Array(10), new Float32Array(10)] }
+ ].forEach(function(e) {
+ expectNoException(function() {
+ ctx[e.name].apply(ctx, e.args);
+ });
+ });
+ loadFile("ting-44.1k-1ch.ogg", function(buf) {
+ ctx.decodeAudioData(buf).then(function(decodedBuf) {
+ ok(true, "decodeAudioData on a closed context should work, it did.")
+ finish();
+ }).catch(function(e){
+ ok(false, "decodeAudioData on a closed context should work, it did not");
+ finish();
+ });
+ });
+}
+
+// Test that MediaStreams that are the output of a suspended AudioContext are
+// producing silence
+// ac1 produce a sine fed to a MediaStreamAudioDestinationNode
+// ac2 is connected to ac1 with a MediaStreamAudioSourceNode, and check that
+// there is silence when ac1 is suspended
+function testMultiContextOutput() {
+ var ac1 = new AudioContext(),
+ ac2 = new AudioContext();
+
+ ac1.onstatechange = function() {
+ ac1.onstatechange = null;
+
+ var osc1 = ac1.createOscillator(),
+ mediaStreamDestination1 = ac1.createMediaStreamDestination();
+
+ var mediaStreamAudioSourceNode2 =
+ ac2.createMediaStreamSource(mediaStreamDestination1.stream),
+ sp2 = ac2.createScriptProcessor(),
+ silentBuffersInARow = 0;
+
+
+ sp2.onaudioprocess = function(e) {
+ ac1.suspend().then(function() {
+ is(ac1.state, "suspended", "ac1 is suspended");
+ sp2.onaudioprocess = checkSilence;
+ });
+ sp2.onaudioprocess = null;
+ }
+
+ function checkSilence(e) {
+ var input = e.inputBuffer.getChannelData(0);
+ var silent = true;
+ for (var i = 0; i < input.length; i++) {
+ if (input[i] != 0.0) {
+ silent = false;
+ }
+ }
+
+ if (silent) {
+ silentBuffersInARow++;
+ if (silentBuffersInARow == 10) {
+ ok(true,
+ "MediaStreams produce silence when their input is blocked.");
+ sp2.onaudioprocess = null;
+ ac1.close();
+ ac2.close();
+ finish();
+ }
+ } else {
+ is(silentBuffersInARow, 0,
+ "No non silent buffer inbetween silent buffers.");
+ }
+ }
+
+ osc1.connect(mediaStreamDestination1);
+
+ mediaStreamAudioSourceNode2.connect(sp2);
+ osc1.start();
+ }
+}
+
+
+// Test that there is no buffering between contexts when connecting a running
+// AudioContext to a suspended AudioContext. Our ScriptProcessorNode does some
+// buffering internally, so we ensure this by using a very very low frequency
+// on a sine, and oberve that the phase has changed by a big enough margin.
+function testMultiContextInput() {
+ var ac1 = new AudioContext(),
+ ac2 = new AudioContext();
+
+ ac1.onstatechange = function() {
+ ac1.onstatechange = null;
+
+ var osc1 = ac1.createOscillator(),
+ mediaStreamDestination1 = ac1.createMediaStreamDestination(),
+ sp1 = ac1.createScriptProcessor();
+
+ var mediaStreamAudioSourceNode2 =
+ ac2.createMediaStreamSource(mediaStreamDestination1.stream),
+ sp2 = ac2.createScriptProcessor(),
+ eventReceived = 0;
+
+
+ osc1.frequency.value = 0.0001;
+
+ function checkDiscontinuity(e) {
+ var inputBuffer = e.inputBuffer.getChannelData(0);
+ if (eventReceived++ == 3) {
+ var delta = Math.abs(inputBuffer[1] - sp2.value),
+ theoreticalIncrement = 2048 * 3 * Math.PI * 2 * osc1.frequency.value / ac1.sampleRate;
+ ok(delta >= theoreticalIncrement,
+ "Buffering did not occur when the context was suspended (delta:" + delta + " increment: " + theoreticalIncrement+")");
+ ac1.close();
+ ac2.close();
+ sp1.onaudioprocess = null;
+ sp2.onaudioprocess = null;
+ finish();
+ }
+ }
+
+ sp2.onaudioprocess = function(e) {
+ var inputBuffer = e.inputBuffer.getChannelData(0);
+ sp2.value = inputBuffer[inputBuffer.length - 1];
+ ac2.suspend().then(function() {
+ ac2.resume().then(function() {
+ sp2.onaudioprocess = checkDiscontinuity;
+ });
+ });
+ }
+
+ osc1.connect(mediaStreamDestination1);
+ osc1.connect(sp1);
+
+ mediaStreamAudioSourceNode2.connect(sp2);
+ osc1.start();
+ }
+}
+
+// Test that ScriptProcessorNode's onaudioprocess don't get called while the
+// context is suspended/closed. It is possible that we get the handler called
+// exactly once after suspend, because the event has already been sent to the
+// event loop.
+function testScriptProcessNodeSuspended() {
+ var ac = new AudioContext();
+ var sp = ac.createScriptProcessor();
+ var remainingIterations = 30;
+ var afterResume = false;
+ ac.onstatechange = function() {
+ ac.onstatechange = null;
+ sp.onaudioprocess = function() {
+ ok(ac.state == "running", "If onaudioprocess is called, the context" +
+ " must be running (was " + ac.state + ", remainingIterations:" + remainingIterations +")");
+ remainingIterations--;
+ if (!afterResume) {
+ if (remainingIterations == 0) {
+ ac.suspend().then(function() {
+ ac.resume().then(function() {
+ remainingIterations = 30;
+ afterResume = true;
+ });
+ });
+ }
+ } else {
+ sp.onaudioprocess = null;
+ finish();
+ }
+ }
+ }
+ sp.connect(ac.destination);
+}
+
+// Take an AudioContext, make sure it switches to running when the audio starts
+// flowing, and then, call suspend, resume and close on it, tracking its state.
+function testAudioContext() {
+ var ac = new AudioContext();
+ is(ac.state, "suspended", "AudioContext should start in suspended state.");
+ var stateTracker = {
+ previous: ac.state,
+ // no promise for the initial suspended -> running
+ initial: { handler: false },
+ suspend: { promise: false, handler: false },
+ resume: { promise: false, handler: false },
+ close: { promise: false, handler: false }
+ };
+
+ function initialSuspendToRunning() {
+ ok(stateTracker.previous == "suspended" &&
+ ac.state == "running",
+ "AudioContext should switch to \"running\" when the audio hardware is" +
+ " ready.");
+
+ stateTracker.previous = ac.state;
+ ac.onstatechange = afterSuspend;
+ stateTracker.initial.handler = true;
+
+ ac.suspend().then(function() {
+ ok(!stateTracker.suspend.promise && !stateTracker.suspend.handler,
+ "Promise should be resolved before the callback, and only once.")
+ stateTracker.suspend.promise = true;
+ });
+ }
+
+ function afterSuspend() {
+ ok(stateTracker.previous == "running" &&
+ ac.state == "suspended",
+ "AudioContext should switch to \"suspend\" when the audio stream is" +
+ "suspended.");
+ ok(stateTracker.suspend.promise && !stateTracker.suspend.handler,
+ "Handler should be called after the callback, and only once");
+
+ stateTracker.suspend.handler = true;
+ stateTracker.previous = ac.state;
+ ac.onstatechange = afterResume;
+
+ ac.resume().then(function() {
+ ok(!stateTracker.resume.promise && !stateTracker.resume.handler,
+ "Promise should be called before the callback, and only once");
+ stateTracker.resume.promise = true;
+ });
+ }
+
+ function afterResume() {
+ ok(stateTracker.previous == "suspended" &&
+ ac.state == "running",
+ "AudioContext should switch to \"running\" when the audio stream resumes.");
+
+ ok(stateTracker.resume.promise && !stateTracker.resume.handler,
+ "Handler should be called after the callback, and only once");
+
+ stateTracker.resume.handler = true;
+ stateTracker.previous = ac.state;
+ ac.onstatechange = afterClose;
+
+ ac.close().then(function() {
+ ok(!stateTracker.close.promise && !stateTracker.close.handler,
+ "Promise should be called before the callback, and only once");
+ stateTracker.close.promise = true;
+ tryToCreateNodeOnClosedContext(ac);
+ tryLegalOpeerationsOnClosedContext(ac);
+ });
+ }
+
+ function afterClose() {
+ ok(stateTracker.previous == "running" &&
+ ac.state == "closed",
+ "AudioContext should switch to \"closed\" when the audio stream is" +
+ " closed.");
+ ok(stateTracker.close.promise && !stateTracker.close.handler,
+ "Handler should be called after the callback, and only once");
+ }
+
+ ac.onstatechange = initialSuspendToRunning;
+}
+
+function testOfflineAudioContext() {
+ var o = new OfflineAudioContext(1, 44100, 44100);
+ is(o.state, "suspended", "OfflineAudioContext should start in suspended state.");
+
+ expectRejectedPromise(o, "resume", "NotSupportedError");
+
+ var previousState = o.state,
+ finishedRendering = false;
+ function beforeStartRendering() {
+ ok(previousState == "suspended" && o.state == "running", "onstatechanged" +
+ "handler is called on state changed, and the new state is running");
+ previousState = o.state;
+ o.onstatechange = onRenderingFinished;
+ }
+
+ function onRenderingFinished() {
+ ok(previousState == "running" && o.state == "closed",
+ "onstatechanged handler is called when rendering finishes, " +
+ "and the new state is closed");
+ ok(finishedRendering, "The Promise that is resolved when the rendering is" +
+ "done should be resolved earlier than the state change.");
+ previousState = o.state;
+ o.onstatechange = afterRenderingFinished;
+
+ tryToCreateNodeOnClosedContext(o);
+ tryLegalOpeerationsOnClosedContext(o);
+ }
+
+ function afterRenderingFinished() {
+ ok(false, "There should be no transition out of the closed state.");
+ }
+
+ o.onstatechange = beforeStartRendering;
+
+ o.startRendering().then(function(buffer) {
+ finishedRendering = true;
+ });
+}
+
+function testSuspendResumeEventLoop() {
+ var ac = new AudioContext();
+ var source = ac.createBufferSource();
+ source.buffer = ac.createBuffer(1, 44100, 44100);
+ source.onended = function() {
+ ok(true, "The AudioContext did resume.");
+ finish();
+ }
+ ac.onstatechange = function() {
+ ac.onstatechange = null;
+
+ ok(ac.state == "running", "initial state is running");
+ ac.suspend();
+ source.start();
+ ac.resume();
+ }
+}
+
+function testResumeInStateChangeForResumeCallback() {
+ // Regression test for bug 1468085.
+ var ac = new AudioContext;
+ ac.onstatechange = function() {
+ ac.resume().then(() => {
+ ok(true, "resume promise resolved as expected.");
+ finish();
+ });
+ }
+}
+
+var remaining = 0;
+function finish() {
+ remaining--;
+ if (remaining == 0) {
+ SimpleTest.finish();
+ }
+}
+
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var tests = [
+ testOfflineAudioContext,
+ testScriptProcessNodeSuspended,
+ testMultiContextOutput,
+ testMultiContextInput,
+ testSuspendResumeEventLoop,
+ testResumeInStateChangeForResumeCallback
+ ];
+
+ // See Bug 1305136, many intermittent failures on Linux
+ if (!navigator.platform.startsWith("Linux")) {
+ tests.push(testAudioContext);
+ }
+
+ remaining = tests.length;
+ tests.forEach(function(f) { f() });
+});
+
+</script>
+</pre>
+</body>
+</html>