Bug 1178738 - Have cancel() dispatch an "interrupted" error on spoken utterance. r?smaug draft
authorEitan Isaacson <eitan@monotonous.org>
Wed, 18 May 2016 10:59:08 -0700
changeset 374074 6b92c5806fb40ed964fe22605cd523bccad684c2
parent 374073 c9d7f153a8234534bae6394701689f40fb08772f
child 374075 46f8308c5d7d08f83f31a9a49944ea4685a7664f
push id19917
push userbmo:eitan@monotonous.org
push dateWed, 01 Jun 2016 19:05:35 +0000
reviewerssmaug
bugs1178738
milestone49.0a1
Bug 1178738 - Have cancel() dispatch an "interrupted" error on spoken utterance. r?smaug MozReview-Commit-ID: DtcEWQFQYEt
dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.mm
dom/media/webspeech/synth/nsSpeechTask.cpp
dom/media/webspeech/synth/nsSpeechTask.h
dom/media/webspeech/synth/speechd/SpeechDispatcherService.cpp
dom/media/webspeech/synth/test/file_global_queue_cancel.html
dom/media/webspeech/synth/test/file_indirect_service_events.html
dom/media/webspeech/synth/test/file_speech_cancel.html
dom/media/webspeech/synth/test/nsFakeSynthServices.cpp
dom/media/webspeech/synth/windows/SapiService.cpp
--- a/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.mm
+++ b/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.mm
@@ -30,16 +30,17 @@ class SpeechTaskCallback final : public 
 {
 public:
   SpeechTaskCallback(nsISpeechTask* aTask,
                      NSSpeechSynthesizer* aSynth,
                      const nsTArray<size_t>& aOffsets)
     : mTask(aTask)
     , mSpeechSynthesizer(aSynth)
     , mOffsets(aOffsets)
+    , mCanceled(false)
   {
     mStartingTime = TimeStamp::Now();
   }
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(SpeechTaskCallback, nsISpeechTaskCallback)
 
   NS_DECL_NSISPEECHTASKCALLBACK
@@ -56,16 +57,17 @@ private:
 
   float GetTimeDurationFromStart();
 
   nsCOMPtr<nsISpeechTask> mTask;
   NSSpeechSynthesizer* mSpeechSynthesizer;
   TimeStamp mStartingTime;
   uint32_t mCurrentIndex;
   nsTArray<size_t> mOffsets;
+  bool mCanceled;
 };
 
 NS_IMPL_CYCLE_COLLECTION(SpeechTaskCallback, mTask);
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechTaskCallback)
   NS_INTERFACE_MAP_ENTRY(nsISpeechTaskCallback)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechTaskCallback)
 NS_INTERFACE_MAP_END
@@ -73,16 +75,17 @@ NS_INTERFACE_MAP_END
 NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechTaskCallback)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechTaskCallback)
 
 NS_IMETHODIMP
 SpeechTaskCallback::OnCancel()
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
+  mCanceled = true;
   [mSpeechSynthesizer stopSpeaking];
   return NS_OK;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
 NS_IMETHODIMP
 SpeechTaskCallback::OnPause()
@@ -158,17 +161,22 @@ SpeechTaskCallback::OnError(uint32_t aIn
   // XXX: Provide more specific error messages
   mTask->DispatchError(GetTimeDurationFromStart(), aIndex,
     uint32_t(dom::SpeechSynthesisErrorCode::Synthesis_failed));
 }
 
 void
 SpeechTaskCallback::OnDidFinishSpeaking()
 {
-  mTask->DispatchEnd(GetTimeDurationFromStart(), mCurrentIndex);
+  if (mCanceled) {
+    mTask->DispatchError(GetTimeDurationFromStart(), mCurrentIndex,
+      uint32_t(dom::SpeechSynthesisErrorCode::Interrupted));
+  } else {
+    mTask->DispatchEnd(GetTimeDurationFromStart(), mCurrentIndex);
+  }
   // no longer needed
   [mSpeechSynthesizer setDelegate:nil];
   mTask = nullptr;
 }
 
 @interface SpeechDelegate : NSObject<NSSpeechSynthesizerDelegate>
 {
 @private
--- a/dom/media/webspeech/synth/nsSpeechTask.cpp
+++ b/dom/media/webspeech/synth/nsSpeechTask.cpp
@@ -406,39 +406,35 @@ nsSpeechTask::DispatchEndInner(float aEl
 nsresult
 nsSpeechTask::DispatchEndImpl(float aElapsedTime, uint32_t aCharIndex)
 {
   LOG(LogLevel::Debug, ("nsSpeechTask::DispatchEnd\n"));
 
   DestroyAudioChannelAgent();
 
   MOZ_ASSERT(mUtterance);
-  if(NS_WARN_IF(mUtterance->mState == SpeechSynthesisUtterance::STATE_ENDED)) {
+  if(NS_WARN_IF(mUtterance->mState != SpeechSynthesisUtterance::STATE_SPEAKING)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   // XXX: This should not be here, but it prevents a crash in MSG.
   if (mStream) {
     mStream->Destroy();
   }
 
   RefPtr<SpeechSynthesisUtterance> utterance = mUtterance;
 
   if (mSpeechSynthesis) {
     mSpeechSynthesis->OnEnd(this);
   }
 
-  if (utterance->mState == SpeechSynthesisUtterance::STATE_PENDING) {
-    utterance->mState = SpeechSynthesisUtterance::STATE_NONE;
-  } else {
-    utterance->mState = SpeechSynthesisUtterance::STATE_ENDED;
-    utterance->DispatchSpeechSynthesisEvent(NS_LITERAL_STRING("end"),
-                                            aCharIndex, aElapsedTime,
-                                            EmptyString());
-  }
+  utterance->mState = SpeechSynthesisUtterance::STATE_ENDED;
+  utterance->DispatchSpeechSynthesisEvent(NS_LITERAL_STRING("end"),
+                                          aCharIndex, aElapsedTime,
+                                          EmptyString());
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSpeechTask::DispatchPause(float aElapsedTime, uint32_t aCharIndex)
 {
   if (!mIndirectAudio) {
@@ -508,16 +504,22 @@ nsSpeechTask::DispatchError(float aElaps
 {
   LOG(LogLevel::Debug, ("nsSpeechTask::DispatchError"));
 
   if (!mIndirectAudio) {
     NS_WARNING("Can't call DispatchError() from a direct audio speech service");
     return NS_ERROR_FAILURE;
   }
 
+  return DispatchErrorInner(aElapsedTime, aCharIndex, aError);
+}
+
+nsresult
+nsSpeechTask::DispatchErrorInner(float aElapsedTime, uint32_t aCharIndex, uint32_t aError)
+{
   if (!mPreCanceled) {
     nsSynthVoiceRegistry::GetInstance()->SpeakNext();
   }
 
   return DispatchErrorImpl(aElapsedTime, aCharIndex, aError);
 }
 
 nsresult
@@ -527,19 +529,22 @@ nsSpeechTask::DispatchErrorImpl(float aE
   if(NS_WARN_IF(mUtterance->mState == SpeechSynthesisUtterance::STATE_ENDED)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   if (mSpeechSynthesis) {
     mSpeechSynthesis->OnEnd(this);
   }
 
-  mUtterance->mState = SpeechSynthesisUtterance::STATE_ENDED;
-  mUtterance->DispatchSpeechSynthesisErrorEvent(aCharIndex, aElapsedTime,
-                                                SpeechSynthesisErrorCode(aError));
+  RefPtr<SpeechSynthesisUtterance> utterance = mUtterance;
+  utterance->mState = (utterance->mState == SpeechSynthesisUtterance::STATE_SPEAKING) ?
+    SpeechSynthesisUtterance::STATE_ENDED : SpeechSynthesisUtterance::STATE_NONE;
+  utterance->DispatchSpeechSynthesisErrorEvent(aCharIndex, aElapsedTime,
+                                               SpeechSynthesisErrorCode(aError));
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSpeechTask::DispatchBoundary(const nsAString& aName,
                                float aElapsedTime, uint32_t aCharIndex)
 {
   if (!mIndirectAudio) {
@@ -655,17 +660,18 @@ nsSpeechTask::Cancel()
     mStream->Suspend();
   }
 
   if (!mInited) {
     mPreCanceled = true;
   }
 
   if (!mIndirectAudio) {
-    DispatchEndInner(GetCurrentTime(), GetCurrentCharOffset());
+    DispatchErrorInner(GetCurrentTime(), GetCurrentCharOffset(),
+                       uint32_t(SpeechSynthesisErrorCode::Interrupted));
   }
 }
 
 void
 nsSpeechTask::ForceEnd()
 {
   if (mStream) {
     mStream->Suspend();
--- a/dom/media/webspeech/synth/nsSpeechTask.h
+++ b/dom/media/webspeech/synth/nsSpeechTask.h
@@ -104,16 +104,18 @@ private:
   void End();
 
   void SendAudioImpl(RefPtr<mozilla::SharedBuffer>& aSamples, uint32_t aDataLen);
 
   nsresult DispatchStartInner();
 
   nsresult DispatchEndInner(float aElapsedTime, uint32_t aCharIndex);
 
+  nsresult DispatchErrorInner(float aElapsedTime, uint32_t aCharIndex, uint32_t aError);
+
   void CreateAudioChannelAgent();
 
   void DestroyAudioChannelAgent();
 
   RefPtr<SourceMediaStream> mStream;
 
   RefPtr<MediaInputPort> mPort;
 
--- a/dom/media/webspeech/synth/speechd/SpeechDispatcherService.cpp
+++ b/dom/media/webspeech/synth/speechd/SpeechDispatcherService.cpp
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "SpeechDispatcherService.h"
 
 #include "mozilla/dom/nsSpeechTask.h"
 #include "mozilla/dom/nsSynthVoiceRegistry.h"
+#include "mozilla/dom/SpeechSynthesisErrorEvent.h"
 #include "mozilla/Preferences.h"
 #include "nsEscape.h"
 #include "nsISupports.h"
 #include "nsPrintfCString.h"
 #include "nsReadableUtils.h"
 #include "nsServiceManagerUtils.h"
 #include "nsThreadUtils.h"
 #include "prlink.h"
@@ -246,16 +247,21 @@ SpeechDispatcherCallback::OnSpeechEvent(
       mTask->DispatchPause((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
       break;
 
     case SPD_EVENT_RESUME:
       mTask->DispatchResume((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
       break;
 
     case SPD_EVENT_CANCEL:
+      mTask->DispatchError((TimeStamp::Now() - mStartTime).ToSeconds(), 0,
+        uint32_t(SpeechSynthesisErrorCode::Interrupted));
+      remove = true;
+      break;
+
     case SPD_EVENT_END:
       mTask->DispatchEnd((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
       remove = true;
       break;
 
     case SPD_EVENT_INDEX_MARK:
       // Not yet supported
       break;
--- a/dom/media/webspeech/synth/test/file_global_queue_cancel.html
+++ b/dom/media/webspeech/synth/test/file_global_queue_cancel.html
@@ -36,17 +36,17 @@ https://bugzilla.mozilla.org/show_bug.cg
     utterance2.lang = 'it-IT-noend';
     var utterance3 = new win1.SpeechSynthesisUtterance("u3: hello, losers three");
 
     var utterance4 = new win2.SpeechSynthesisUtterance("u4: hello, losers same!");
     utterance4.lang = 'it-IT-noend';
     var utterance5 = new win2.SpeechSynthesisUtterance("u5: hello, losers too");
     utterance5.lang = 'it-IT-noend';
 
-    var eventOrder = ['start1', 'end1', 'start2', 'end2'];
+    var eventOrder = ['start1', 'end1', 'start2', 'error2'];
     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();
 
     });
@@ -56,18 +56,18 @@ https://bugzilla.mozilla.org/show_bug.cg
       testSynthState(win2, { pending: false });
     });
     utterance2.addEventListener('start', function(e) {
       is(eventOrder.shift(), 'start2', 'start2');
       testSynthState(win1, { speaking: true, pending: true });
       testSynthState(win2, { speaking: true, pending: false });
       win1.speechSynthesis.cancel();
     });
-    utterance2.addEventListener('end', function(e) {
-      is(eventOrder.shift(), 'end2', 'end2');
+    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 + '"');
     }
--- a/dom/media/webspeech/synth/test/file_indirect_service_events.html
+++ b/dom/media/webspeech/synth/test/file_indirect_service_events.html
@@ -45,17 +45,17 @@ function testFunc(done_cb) {
     });
 
     utterance.addEventListener('resume', function(e) {
       is(e.charIndex, 1, 'resume event charIndex matches service arguments');
       is(e.elapsedTime, 1.5, 'resume event elapsedTime matches service arguments');
       speechSynthesis.cancel();
     });
 
-    utterance.addEventListener('end', function(e) {
+    utterance.addEventListener('error', function(e) {
       ok(e.charIndex, 1, 'resume event charIndex matches service arguments');
       ok(e.elapsedTime, 1.5, 'end event elapsedTime matches service arguments');
       test_no_events();
     });
 
     info('start speak');
     speechSynthesis.speak(utterance);
   }
--- a/dom/media/webspeech/synth/test/file_speech_cancel.html
+++ b/dom/media/webspeech/synth/test/file_speech_cancel.html
@@ -22,17 +22,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 1150315 **/
 
 function testFunc(done_cb) {
-  var gotEndEvent = false;
+  var gotErrorEvent = false;
   // A long utterance that we will interrupt.
   var utterance = new SpeechSynthesisUtterance("Donec ac nunc feugiat, posuere " +
     "mauris id, pharetra velit. Donec fermentum orci nunc, sit amet maximus" +
     "dui tincidunt ut. Sed ultricies ac nisi a laoreet. Proin interdum," +
     "libero maximus hendrerit posuere, lorem risus egestas nisl, a" +
     "ultricies massa justo eu nisi. Duis mattis nibh a ligula tincidunt" +
     "tincidunt non eu erat. Sed bibendum varius vulputate. Cras leo magna," +
     "ornare ac posuere vel, luctus id metus. Mauris nec quam ac augue" +
@@ -58,23 +58,24 @@ function testFunc(done_cb) {
     "Curabitur velit lacus, mollis vel finibus et, molestie sit amet" +
     "sapien. Proin vitae dolor ac augue posuere efficitur ac scelerisque" +
     "diam. Nulla sed odio elit.");
   utterance2.addEventListener('start', function() {
     info('start');
     speechSynthesis.cancel();
     speechSynthesis.speak(utterance3);
   });
-  utterance2.addEventListener('end', function(e) {
-    gotEndEvent = true;
+  utterance2.addEventListener('error', function(e) {
+    gotErrorEvent = true;
+    is(e.error, "interrupted", "Error event is has right error.")
   });
 
   var utterance3 = new SpeechSynthesisUtterance("Hello, world 3!");
   utterance3.addEventListener('start', function() {
-    ok(gotEndEvent, "didn't get start event for this utterance");
+    ok(gotErrorEvent, "didn't get error event for previous utterance");
   });
   utterance3.addEventListener('end', done_cb);
 
   // Speak/cancel while paused (Bug 1187105)
   speechSynthesis.pause();
   speechSynthesis.speak(new SpeechSynthesisUtterance("hello."));
   ok(speechSynthesis.pending, "paused speechSynthesis has an utterance queued.");
   speechSynthesis.cancel();
--- a/dom/media/webspeech/synth/test/nsFakeSynthServices.cpp
+++ b/dom/media/webspeech/synth/test/nsFakeSynthServices.cpp
@@ -85,17 +85,18 @@ public:
     }
 
     return NS_OK;
   }
 
   NS_IMETHOD OnCancel() override
   {
     if (mTask) {
-      mTask->DispatchEnd(1.5, 1);
+      mTask->DispatchError(1.5, 1,
+                           uint32_t(SpeechSynthesisErrorCode::Interrupted));
     }
 
     return NS_OK;
   }
 
   NS_IMETHOD OnVolumeChanged(float aVolume) override
   {
     return NS_OK;
--- a/dom/media/webspeech/synth/windows/SapiService.cpp
+++ b/dom/media/webspeech/synth/windows/SapiService.cpp
@@ -8,16 +8,17 @@
 #include "SapiService.h"
 #include "nsServiceManagerUtils.h"
 #include "nsWin32Locale.h"
 #include "GeckoProfiler.h"
 #include "nsEscape.h"
 
 #include "mozilla/dom/nsSynthVoiceRegistry.h"
 #include "mozilla/dom/nsSpeechTask.h"
+#include "mozilla/dom/SpeechSynthesisErrorEvent.h"
 #include "mozilla/Preferences.h"
 
 namespace mozilla {
 namespace dom {
 
 StaticRefPtr<SapiService> SapiService::sSingleton;
 
 class SapiCallback final : public nsISpeechTaskCallback
@@ -126,19 +127,23 @@ void
 SapiCallback::OnSpeechEvent(const SPEVENT& speechEvent)
 {
   switch (speechEvent.eEventId) {
   case SPEI_START_INPUT_STREAM:
     mTask->DispatchStart();
     break;
   case SPEI_END_INPUT_STREAM:
     if (mSpeakTextLen) {
+      // mSpeakTextLen will be > 0 on any utterance except a cancel utterance.
       mCurrentIndex = mSpeakTextLen;
+      mTask->DispatchEnd(GetTickCount() - mStartingTime, mCurrentIndex);
+    } else {
+      mTask->DispatchError(GetTickCount() - mStartingTime, mCurrentIndex,
+        uint32_t(SpeechSynthesisErrorCode::Interrupted));
     }
-    mTask->DispatchEnd(GetTickCount() - mStartingTime, mCurrentIndex);
     mTask = nullptr;
     break;
   case SPEI_TTS_BOOKMARK:
     mCurrentIndex = static_cast<ULONG>(speechEvent.lParam) - mTextOffset;
     mTask->DispatchBoundary(NS_LITERAL_STRING("mark"),
                             GetTickCount() - mStartingTime, mCurrentIndex);
     break;
   case SPEI_WORD_BOUNDARY: