1818#include " VideoUtils.h"
1919#include " mozilla/AbstractThread.h"
2020#include " mozilla/ClearOnShutdown.h"
21- #include " mozilla/ErrorNames.h"
2221#include " mozilla/MediaManager.h"
2322#include " mozilla/dom/AudioStreamTrack.h"
2423#include " mozilla/dom/BindingUtils.h"
@@ -138,7 +137,7 @@ SpeechRecognition::~SpeechRecognition() {
138137
139138 // Ensure backend is properly cleaned up
140139 if (mBackend ) {
141- mBackend ->Abort ();
140+ mBackend ->Abort (TrailingEvents::Skip );
142141 mBackend = nullptr ;
143142 }
144143}
@@ -151,7 +150,7 @@ JSObject* SpeechRecognition::WrapObject(JSContext* aCx,
151150void SpeechRecognition::DisconnectFromOwner () {
152151 AssertIsOnMainThread ();
153152 if (mBackend ) {
154- mBackend ->Abort ();
153+ mBackend ->Abort (TrailingEvents::Skip );
155154 mBackend = nullptr ;
156155 }
157156 Reset ();
@@ -178,6 +177,8 @@ void SpeechRecognition::Reset() {
178177 }
179178 }
180179 mStarted = false ;
180+ mStopping = false ;
181+ mAborting = false ;
181182 mBackendListening = false ;
182183 mStartDispatched = false ;
183184 // A track obtained via our own getUserMedia() call (the microphone path)
@@ -188,7 +189,6 @@ void SpeechRecognition::Reset() {
188189 }
189190 mTrack = nullptr ;
190191 mTrackIsOwned = false ;
191- mStopRecordingPromise = nullptr ;
192192 // The microphone path (Start() with no explicit track) registers mListener
193193 // on mStream; it must be unregistered before being cleared (see
194194 // DOMMediaStream::TrackListener). Without this, mListener/mStream survive a
@@ -211,9 +211,18 @@ void SpeechRecognition::PostResetAndEnd() {
211211 RefPtr<SpeechRecognition> self = this ;
212212 NS_DispatchToMainThread (NS_NewRunnableFunction(
213213 " SpeechRecognition::PostResetAndEnd" , [self = std::move(self)]() {
214+ // Don't end a session that started since this was queued: an "end"
215+ // handler can call start().
214216 if (self->mBackend ) {
215217 return ;
216218 }
219+ // Reset() cleared [[started]], so this session has already ended. A
220+ // newer session that both started and finished in between gets its
221+ // "end" from here, and its own runnable then no-ops - exactly one
222+ // "end" either way.
223+ if (!self->mStarted ) {
224+ return ;
225+ }
217226 self->ResetAndEnd ();
218227 }));
219228}
@@ -249,30 +258,6 @@ SpeechRecognition::StartRecording(RefPtr<AudioStreamTrack>& aTrack) {
249258 return NS_OK ;
250259}
251260
252- RefPtr<GenericNonExclusivePromise> SpeechRecognition::StopRecording () {
253- AssertIsOnMainThread ();
254- if (!mTrack ) {
255- // Recording wasn't started, or has already been stopped.
256- return GenericNonExclusivePromise::CreateAndResolve (true , __func__);
257- }
258-
259- if (mStopRecordingPromise ) {
260- return mStopRecordingPromise ;
261- }
262-
263- if (mBackend ) {
264- mBackend ->DetachFromTrack ();
265- }
266-
267- if (mTrackIsOwned ) {
268- mTrack ->Stop ();
269- }
270-
271- DispatchTrustedEvent (u" audioend" _ns);
272-
273- return nullptr ;
274- }
275-
276261already_AddRefed<SpeechGrammarList> SpeechRecognition::Grammars () const {
277262 RefPtr<SpeechGrammarList> speechGrammarList = mSpeechGrammarList ;
278263 return speechGrammarList.forget ();
@@ -626,17 +611,15 @@ void SpeechRecognition::StartImpl(MediaStreamTrack* aAudioTrack,
626611 // Step 4: processLocally is always true here (on-device recognition). If the
627612 // backend cannot start (local recognition unavailable for this lang), fire a
628613 // service-not-allowed error and abort. DispatchErrorAndEnd queues the event.
629- mBackend = new SpeechRecognitionBackend (this , graphRate, effectiveLang,
630- phrasesForBackend);
631- nsresult rv = mBackend ->Start ();
632- if (NS_FAILED (rv)) {
633- LOGE (" Failed to start backend: {} ({:x})" , GetStaticErrorName (rv),
634- static_cast <uint32_t >(rv));
635- mBackend = nullptr ;
614+ mBackend = SpeechRecognitionBackend::Create (this , graphRate, effectiveLang,
615+ phrasesForBackend);
616+ if (!mBackend ) {
617+ LOGE (" Failed to create the backend" );
636618 DispatchErrorAndEnd (SpeechRecognitionErrorCode::Service_not_allowed,
637619 " Local speech recognition is not available" _ns);
638620 return ;
639621 }
622+ mBackend ->Start ();
640623
641624 // Step 5: set [[started]] to true.
642625 mStarted = true ;
@@ -717,37 +700,67 @@ void SpeechRecognition::StartImpl(MediaStreamTrack* aAudioTrack,
717700
718701void SpeechRecognition::Stop () {
719702 AssertIsOnMainThread ();
720- // If not started, ignore, per spec
721- if (!mStarted ) {
703+ // https://webaudio.github.io/web-speech-api/#dom-speechrecognition-stop
704+ // "If the stop method is called on an object which is already stopped or
705+ // being stopped [...] the user agent must ignore the call."
706+ if (!mStarted || mStopping || !mBackend ) {
722707 return ;
723708 }
709+ mStopping = true ;
724710
725- if (mBackend ) {
726- // Stop the backend/session. This will dispatch soundend (if needed) and
727- // audioend via main thread runnables.
728- mBackend ->Stop ();
729- // We are conforming to spec semantics for stop(): finalize recognition and
730- // fire 'end'. Clear the backend to avoid late results after end.
731- mBackend = nullptr ;
711+ // Same section: "The speech service must attempt to return a recognition
712+ // result (or a nomatch) based on the audio that it has already collected."
713+ // So mBackend stays live - late results are wanted here, unlike on the
714+ // abort path - and "end" waits for OnSessionFinished(). Dispatches soundend
715+ // (if needed) and audioend via main thread runnables in the meantime.
716+ mBackend ->Stop ();
717+ }
718+
719+ void SpeechRecognition::OnSessionFinished (bool aProducedResult) {
720+ AssertIsOnMainThread ();
721+ LOG (" OnSessionFinished: producedResult={}" , aProducedResult);
722+ mBackend = nullptr ;
732723
733- // Ensure 'end' fires after the already-posted 'audioend' runnable from the
734- // backend. PostResetAndEnd() calls ResetAndEnd() on the next turn. mBackend
735- // was just cleared above and is only set again by a subsequent start();
736- // if that happened by the time this runs, this stale continuation must
737- // not reset the new session's state out from under it.
738- PostResetAndEnd ();
724+ if (!aProducedResult) {
725+ DispatchNoMatch ();
739726 }
727+ PostResetAndEnd ();
728+ }
729+
730+ void SpeechRecognition::DispatchNoMatch () {
731+ AssertIsOnMainThread ();
732+ // https://webaudio.github.io/web-speech-api/#eventdef-speechrecognition-nomatch
733+ // The event's results "may contain speech recognition results that are below
734+ // the confidence threshold or may be null"; the engine hands us nothing at
735+ // all in this case, so the list is empty.
736+ RootedDictionary<SpeechRecognitionEventInit> init (RootingCx ());
737+ init.mBubbles = true ;
738+ init.mCancelable = false ;
739+ init.mResultIndex = 0 ;
740+ init.mResults = new SpeechRecognitionResultList (this );
741+ init.mInterpretation = JS::NullValue ();
742+
743+ RefPtr<SpeechRecognitionEvent> domEvent =
744+ SpeechRecognitionEvent::Constructor (this , u" nomatch" _ns, init);
745+ domEvent->SetTrusted (true );
746+ DispatchEvent (*domEvent);
740747}
741748
742749void SpeechRecognition::Abort () {
743750 AssertIsOnMainThread ();
744- // If not started, ignore per spec
745- if (!mStarted ) {
751+ // https://webaudio.github.io/web-speech-api/#dom-speechrecognition-abort
752+ // "If the abort method is called on an object which is already stopped or
753+ // aborting (that is, start was never called on it, the end or error event
754+ // has fired on it, or abort was previously called on it), the user agent
755+ // must ignore the call." Without this, a second abort() would queue a second
756+ // reset-and-end, firing "end" twice.
757+ if (!mStarted || mAborting ) {
746758 return ;
747759 }
760+ mAborting = true ;
748761
749762 if (mBackend ) {
750- mBackend ->Abort ();
763+ mBackend ->Abort (TrailingEvents::Fire );
751764 // Clear backend after abort since no more results are expected
752765 mBackend = nullptr ;
753766 }
@@ -814,7 +827,7 @@ void SpeechRecognition::DispatchErrorAndEnd(
814827 return ;
815828 }
816829 if (mBackend ) {
817- mBackend ->Abort ();
830+ mBackend ->Abort (TrailingEvents::Skip );
818831 mBackend = nullptr ;
819832 }
820833 PostResetAndEnd ();
0 commit comments