Bug 1178738 - Have cancel() dispatch an "interrupted" error on spoken utterance. r=smaug
☠☠ backed out by 8d2e6a40be15 ☠ ☠
authorEitan Isaacson <eitan@monotonous.org>
Wed, 18 May 2016 10:59:08 -0700
changeset 341240 4c66c0ddf56267ad34c5a3538a66d7f9fcfaafa2
parent 341239 6fe5dd5c3f33486f3dddf6c363bd72d7f7687d6a
child 341241 ab50796d261607416f6c9b785015d6601edaa348
push id1183
push userraliiev@mozilla.com
push dateMon, 05 Sep 2016 20:01:49 +0000
treeherdermozilla-release@3148731bed45 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1178738
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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: