Bug 1178738 - Have utterances dispatch "canceled" error when they never start. r?smaug
MozReview-Commit-ID: KfJurwcS7mw
--- a/dom/media/webspeech/synth/SpeechSynthesis.cpp
+++ b/dom/media/webspeech/synth/SpeechSynthesis.cpp
@@ -7,16 +7,17 @@
#include "nsISupportsPrimitives.h"
#include "nsSpeechTask.h"
#include "mozilla/Logging.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/SpeechSynthesisBinding.h"
+#include "mozilla/dom/SpeechSynthesisErrorEvent.h"
#include "SpeechSynthesis.h"
#include "nsSynthVoiceRegistry.h"
#include "nsIDocument.h"
#undef LOG
mozilla::LogModule*
GetSpeechSynthLog()
{
@@ -189,22 +190,43 @@ SpeechSynthesis::AdvanceQueue()
if (mCurrentTask) {
mCurrentTask->SetSpeechSynthesis(this);
}
return;
}
void
+SpeechSynthesis::DispatchToCanceledQueue()
+{
+ while (mCanceledQueue.Length()) {
+ RefPtr<SpeechSynthesisUtterance> utterance = mCanceledQueue.ElementAt(0);
+ mCanceledQueue.RemoveElementAt(0);
+
+ utterance->DispatchSpeechSynthesisErrorEvent(
+ 0, 0, SpeechSynthesisErrorCode::Canceled);
+ }
+}
+
+void
SpeechSynthesis::Cancel()
{
- mSpeechQueue.Clear();
+ if (mCanceledQueue.IsEmpty()) {
+ mCanceledQueue.SwapElements(mSpeechQueue);
+ } else {
+ while (mSpeechQueue.Length()) {
+ mCanceledQueue.AppendElement(mSpeechQueue.ElementAt(0));
+ mSpeechQueue.RemoveElementAt(0);
+ }
+ }
if (mCurrentTask) {
mCurrentTask->Cancel();
+ } else {
+ DispatchToCanceledQueue();
}
}
void
SpeechSynthesis::Pause()
{
if (Paused()) {
return;
@@ -234,16 +256,17 @@ SpeechSynthesis::Resume()
}
void
SpeechSynthesis::OnEnd(const nsSpeechTask* aTask)
{
MOZ_ASSERT(mCurrentTask == aTask);
mCurrentTask = nullptr;
+ DispatchToCanceledQueue();
AdvanceQueue();
}
void
SpeechSynthesis::GetVoices(nsTArray< RefPtr<SpeechSynthesisVoice> >& aResult)
{
aResult.Clear();
uint32_t voiceCount = 0;
--- a/dom/media/webspeech/synth/SpeechSynthesis.h
+++ b/dom/media/webspeech/synth/SpeechSynthesis.h
@@ -65,20 +65,24 @@ public:
IMPL_EVENT_HANDLER(voiceschanged)
private:
virtual ~SpeechSynthesis();
void AdvanceQueue();
+ void DispatchToCanceledQueue();
+
bool HasVoices() const;
nsTArray<RefPtr<SpeechSynthesisUtterance> > mSpeechQueue;
+ nsTArray<RefPtr<SpeechSynthesisUtterance> > mCanceledQueue;
+
RefPtr<nsSpeechTask> mCurrentTask;
nsRefPtrHashtable<nsStringHashKey, SpeechSynthesisVoice> mVoiceCache;
bool mHoldQueue;
uint64_t mInnerID;
};
--- a/dom/media/webspeech/synth/nsSpeechTask.cpp
+++ b/dom/media/webspeech/synth/nsSpeechTask.cpp
@@ -530,20 +530,29 @@ nsSpeechTask::DispatchErrorImpl(float aE
return NS_ERROR_NOT_AVAILABLE;
}
if (mSpeechSynthesis) {
mSpeechSynthesis->OnEnd(this);
}
RefPtr<SpeechSynthesisUtterance> utterance = mUtterance;
- utterance->mState = (utterance->mState == SpeechSynthesisUtterance::STATE_SPEAKING) ?
- SpeechSynthesisUtterance::STATE_ENDED : SpeechSynthesisUtterance::STATE_NONE;
- utterance->DispatchSpeechSynthesisErrorEvent(aCharIndex, aElapsedTime,
- SpeechSynthesisErrorCode(aError));
+ SpeechSynthesisErrorCode err;
+ if (utterance->mState == SpeechSynthesisUtterance::STATE_PENDING &&
+ aError == uint32_t(SpeechSynthesisErrorCode::Interrupted)) {
+ // If utterance never started the error should be "canceled" instead of
+ // "interrupted".
+ err = SpeechSynthesisErrorCode::Canceled;
+ utterance->mState = SpeechSynthesisUtterance::STATE_NONE;
+ } else {
+ err = SpeechSynthesisErrorCode(aError);
+ utterance->mState = SpeechSynthesisUtterance::STATE_ENDED;
+ }
+
+ utterance->DispatchSpeechSynthesisErrorEvent(aCharIndex, aElapsedTime, err);
return NS_OK;
}
NS_IMETHODIMP
nsSpeechTask::DispatchBoundary(const nsAString& aName,
float aElapsedTime, uint32_t aCharIndex)
{
--- a/dom/media/webspeech/synth/test/common.js
+++ b/dom/media/webspeech/synth/test/common.js
@@ -84,9 +84,15 @@ function loadSpeechTest(fileName, prefs,
});
}
function testSynthState(win, expectedState) {
for (var attr in expectedState) {
is(win.speechSynthesis[attr], expectedState[attr],
win.document.title + ": '" + attr + '" does not match');
}
+}
+
+function synthEventPromise(utterance, eventType) {
+ return new Promise(resolve => {
+ utterance.addEventListener(eventType, resolve);
+ });
}
\ No newline at end of file
--- a/dom/media/webspeech/synth/test/file_global_queue_cancel.html
+++ b/dom/media/webspeech/synth/test/file_global_queue_cancel.html
@@ -45,38 +45,50 @@ https://bugzilla.mozilla.org/show_bug.cg
utterance1.addEventListener('start', function(e) {
is(eventOrder.shift(), 'start1', 'start1');
testSynthState(win1, { speaking: true, pending: true });
testSynthState(win2, { speaking: true, pending: true });
win2.speechSynthesis.cancel();
SpecialPowers.wrap(win1.speechSynthesis).forceEnd();
});
- utterance1.addEventListener('end', function(e) {
+
+ var error4 = synthEventPromise(utterance4, 'error').then(isCanceled);
+ var error5 = synthEventPromise(utterance5, 'error').then(isCanceled);
+
+ var end1 = synthEventPromise(utterance1, 'end').then(() => {
is(eventOrder.shift(), 'end1', 'end1');
testSynthState(win1, { pending: true });
testSynthState(win2, { pending: false });
});
- utterance2.addEventListener('start', function(e) {
+
+ var start2 = synthEventPromise(utterance2, 'start').then(() => {
is(eventOrder.shift(), 'start2', 'start2');
testSynthState(win1, { speaking: true, pending: true });
testSynthState(win2, { speaking: true, pending: false });
- win1.speechSynthesis.cancel();
});
+
+ Promise.all([error4, error5, end1, start2]).then(
+ () => win1.speechSynthesis.cancel());
+
utterance2.addEventListener('error', function(e) {
is(eventOrder.shift(), 'error2', 'error2');
testSynthState(win1, { speaking: false, pending: false });
testSynthState(win2, { speaking: false, pending: false });
SimpleTest.finish();
});
function wrongUtterance(e) {
ok(false, 'This shall not be uttered: "' + e.target.text + '"');
}
+ function isCanceled(e) {
+ is(e.error, "canceled", "Utterance was canceled: " + e.target.text);
+ }
+
utterance3.addEventListener('start', wrongUtterance);
utterance4.addEventListener('start', wrongUtterance);
utterance5.addEventListener('start', wrongUtterance);
win1.speechSynthesis.speak(utterance1);
win1.speechSynthesis.speak(utterance2);
win1.speechSynthesis.speak(utterance3);
win2.speechSynthesis.speak(utterance4);
--- a/dom/media/webspeech/synth/test/file_speech_cancel.html
+++ b/dom/media/webspeech/synth/test/file_speech_cancel.html
@@ -39,21 +39,26 @@ function testFunc(done_cb) {
"consectetur bibendum. Integer a commodo tortor. Duis semper dolor eu" +
"facilisis facilisis. Etiam venenatis turpis est, quis tincidunt velit" +
"suscipit a. Cras semper orci in sapien rhoncus bibendum. Suspendisse" +
"eu ex lobortis, finibus enim in, condimentum quam. Maecenas eget dui" +
"ipsum. Aliquam tortor leo, interdum eget congue ut, tempor id elit.");
utterance.addEventListener('start', function(e) {
ok(true, 'start utterance 1');
speechSynthesis.cancel();
- info('cancel!');
- speechSynthesis.speak(utterance2);
- info('speak??');
});
+ var utterance_never = new SpeechSynthesisUtterance("I never get uttered");
+ var promise_cancelled = synthEventPromise(utterance_never, "error").then(
+ e => is(e.error, "canceled", "utterance was canceled"));
+ var promise_interrupted = synthEventPromise(utterance, "error").then(
+ e => is(e.error, "interrupted", "utterance was interrupted"));
+ Promise.all([promise_cancelled, promise_interrupted]).then(
+ () => speechSynthesis.speak(utterance2));
+
var utterance2 = new SpeechSynthesisUtterance("Proin ornare neque vitae " +
"risus mattis rutrum. Suspendisse a velit ut est convallis aliquet." +
"Nullam ante elit, malesuada vel luctus rutrum, ultricies nec libero." +
"Praesent eu iaculis orci. Sed nisl diam, sodales ac purus et," +
"volutpat interdum tortor. Nullam aliquam porta elit et maximus. Cras" +
"risus lectus, elementum vel sodales vel, ultricies eget lectus." +
"Curabitur velit lacus, mollis vel finibus et, molestie sit amet" +
"sapien. Proin vitae dolor ac augue posuere efficitur ac scelerisque" +
@@ -78,16 +83,17 @@ function testFunc(done_cb) {
speechSynthesis.pause();
speechSynthesis.speak(new SpeechSynthesisUtterance("hello."));
ok(speechSynthesis.pending, "paused speechSynthesis has an utterance queued.");
speechSynthesis.cancel();
ok(!speechSynthesis.pending, "paused speechSynthesis has no utterance queued.");
speechSynthesis.resume();
speechSynthesis.speak(utterance);
+ speechSynthesis.speak(utterance_never);
ok(!speechSynthesis.speaking, "speechSynthesis is not speaking yet.");
ok(speechSynthesis.pending, "speechSynthesis has an utterance queued.");
}
// Run test with no global queue, and then run it with a global queue.
testFunc(function() {
SpecialPowers.pushPrefEnv(
{ set: [['media.webspeech.synth.force_global_queue', true]] }, function() {
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_speech_cancel_reentry.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html lang="it-IT-noend">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1178738
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1178738: Make sure cancel is reentrant</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.info = parent.info;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ </script>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1178738">Mozilla Bug 1178738</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1178738 **/
+
+function testFunc(done_cb) {
+ var utterance = new SpeechSynthesisUtterance("utterance 1");
+ var utterance2 = new SpeechSynthesisUtterance("utterance 2");
+ var utterance3 = new SpeechSynthesisUtterance("utterance 3");
+ var utterance4 = new SpeechSynthesisUtterance("utterance 4");
+ var utterance5 = new SpeechSynthesisUtterance("utterance 5");
+ utterance5.lang = "en-GB";
+
+ utterance.addEventListener('start', function(e) {
+ ok(true, 'start utterance 1');
+ speechSynthesis.cancel();
+ });
+
+
+ utterance.addEventListener('error', function(e) {
+ is(e.error, "interrupted", "utterance was interrupted");
+ });
+
+ utterance2.addEventListener("error", function(e) {
+ is(e.error, "canceled", "utterance was canceled");
+ speechSynthesis.cancel();
+ speechSynthesis.speak(utterance3);
+ });
+
+ utterance2.addEventListener("start", function(e) {
+ ok(false, "Utterance should never be spoken")
+ });
+
+ utterance3.addEventListener('start', function(e) {
+ ok(true, 'start utterance 3');
+ speechSynthesis.speak(utterance4);
+ speechSynthesis.cancel();
+ });
+
+ utterance3.addEventListener('error', function(e) {
+ is(e.error, "interrupted", "utterance was interrupted");
+ speechSynthesis.cancel();
+ speechSynthesis.speak(utterance5);
+ });
+
+ var utterance4_error = synthEventPromise(utterance4, "error").then(
+ e => is(e.error, "canceled", "utterance 4 was canceled"));
+ var utterance5_end = synthEventPromise(utterance5, "end").then(
+ e => ok(true, "utterance 5 ended"));
+
+ Promise.all([utterance4_error, utterance5_end]).then(done_cb);
+
+
+ speechSynthesis.speak(utterance);
+ speechSynthesis.speak(utterance2);
+}
+
+// Run test with no global queue, and then run it with a global queue.
+testFunc(SimpleTest.finish);
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/webspeech/synth/test/mochitest.ini
+++ b/dom/media/webspeech/synth/test/mochitest.ini
@@ -3,24 +3,26 @@ tags=msg
subsuite = media
support-files =
common.js
file_bfcache_frame.html
file_setup.html
file_speech_queue.html
file_speech_simple.html
file_speech_cancel.html
+ file_speech_cancel_reentry.html
file_speech_error.html
file_indirect_service_events.html
file_global_queue.html
file_global_queue_cancel.html
file_global_queue_pause.html
[test_setup.html]
[test_speech_queue.html]
[test_speech_simple.html]
[test_speech_cancel.html]
+[test_speech_cancel_reentry.html]
[test_speech_error.html]
[test_indirect_service_events.html]
[test_global_queue.html]
[test_global_queue_cancel.html]
[test_global_queue_pause.html]
[test_bfcache.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_speech_cancel_reentry.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1178738
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1178738: Make sure cancel is reentrant</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1178738">Mozilla Bug 1178738</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1178738 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.enabled', true],
+ ['media.webspeech.synth.force_global_queue', false]] },
+ function() { loadSpeechTest("file_speech_cancel_reentry.html"); });
+
+</script>
+</pre>
+</body>
+</html>