Bug 1943761 - Add class alignment to the mozsearch analysis file. r=asuth
[gecko.git] / dom / media / webspeech / synth / SpeechSynthesis.cpp
blob02fe03232cd1df9bbf273a21bb284dd505cc73f4
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "nsISupportsPrimitives.h"
8 #include "nsSpeechTask.h"
9 #include "mozilla/Logging.h"
11 #include "mozilla/dom/Element.h"
13 #include "mozilla/dom/SpeechSynthesisBinding.h"
14 #include "mozilla/dom/WindowGlobalChild.h"
15 #include "SpeechSynthesis.h"
16 #include "nsContentUtils.h"
17 #include "nsSynthVoiceRegistry.h"
18 #include "mozilla/dom/Document.h"
19 #include "nsIDocShell.h"
20 #include "nsGlobalWindowInner.h"
22 #undef LOG
23 mozilla::LogModule* GetSpeechSynthLog() {
24 static mozilla::LazyLogModule sLog("SpeechSynthesis");
26 return sLog;
28 #define LOG(type, msg) MOZ_LOG(GetSpeechSynthLog(), type, msg)
30 namespace mozilla::dom {
32 NS_IMPL_CYCLE_COLLECTION_CLASS(SpeechSynthesis)
34 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(SpeechSynthesis,
35 DOMEventTargetHelper)
36 NS_IMPL_CYCLE_COLLECTION_UNLINK(mCurrentTask)
37 NS_IMPL_CYCLE_COLLECTION_UNLINK(mSpeechQueue)
38 tmp->mVoiceCache.Clear();
39 NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
40 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
42 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(SpeechSynthesis,
43 DOMEventTargetHelper)
44 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCurrentTask)
45 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSpeechQueue)
46 for (SpeechSynthesisVoice* voice : tmp->mVoiceCache.Values()) {
47 cb.NoteXPCOMChild(voice);
49 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
51 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechSynthesis)
52 NS_INTERFACE_MAP_ENTRY(nsIObserver)
53 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
54 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
56 NS_IMPL_ADDREF_INHERITED(SpeechSynthesis, DOMEventTargetHelper)
57 NS_IMPL_RELEASE_INHERITED(SpeechSynthesis, DOMEventTargetHelper)
59 SpeechSynthesis::SpeechSynthesis(nsPIDOMWindowInner* aParent)
60 : DOMEventTargetHelper(aParent),
61 mHoldQueue(false),
62 mInnerID(aParent->WindowID()) {
63 MOZ_ASSERT(NS_IsMainThread());
65 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
66 if (obs) {
67 obs->AddObserver(this, "inner-window-destroyed", true);
68 obs->AddObserver(this, "synth-voices-changed", true);
69 obs->AddObserver(this, "synth-voices-error", true);
73 SpeechSynthesis::~SpeechSynthesis() = default;
75 JSObject* SpeechSynthesis::WrapObject(JSContext* aCx,
76 JS::Handle<JSObject*> aGivenProto) {
77 return SpeechSynthesis_Binding::Wrap(aCx, this, aGivenProto);
80 bool SpeechSynthesis::Pending() const {
81 // If we don't have any task, nothing is pending. If we have only one task,
82 // check if that task is currently pending. If we have more than one task,
83 // then the tasks after the first one are definitely pending.
84 return mSpeechQueue.Length() > 1 ||
85 (mSpeechQueue.Length() == 1 &&
86 (!mCurrentTask || mCurrentTask->IsPending()));
89 bool SpeechSynthesis::Speaking() const {
90 // Check global speaking state if there is no active speaking task.
91 return (!mSpeechQueue.IsEmpty() && HasSpeakingTask()) ||
92 nsSynthVoiceRegistry::GetInstance()->IsSpeaking();
95 bool SpeechSynthesis::Paused() const {
96 return mHoldQueue || (mCurrentTask && mCurrentTask->IsPrePaused()) ||
97 (!mSpeechQueue.IsEmpty() && mSpeechQueue.ElementAt(0)->IsPaused());
100 bool SpeechSynthesis::HasEmptyQueue() const {
101 return mSpeechQueue.Length() == 0;
104 bool SpeechSynthesis::HasVoices() const {
105 uint32_t voiceCount = mVoiceCache.Count();
106 if (voiceCount == 0) {
107 nsresult rv =
108 nsSynthVoiceRegistry::GetInstance()->GetVoiceCount(&voiceCount);
109 if (NS_WARN_IF(NS_FAILED(rv))) {
110 return false;
114 return voiceCount != 0;
117 void SpeechSynthesis::Speak(SpeechSynthesisUtterance& aUtterance) {
118 if (!mInnerID) {
119 return;
122 mSpeechQueue.AppendElement(&aUtterance);
124 if (mSpeechQueue.Length() == 1) {
125 RefPtr<WindowGlobalChild> wgc =
126 WindowGlobalChild::GetByInnerWindowId(mInnerID);
127 if (wgc) {
128 wgc->BlockBFCacheFor(BFCacheStatus::HAS_ACTIVE_SPEECH_SYNTHESIS);
131 // If we only have one item in the queue, we aren't pre-paused, and
132 // we have voices available, speak it.
133 if (!mCurrentTask && !mHoldQueue && HasVoices()) {
134 AdvanceQueue();
139 void SpeechSynthesis::AdvanceQueue() {
140 LOG(LogLevel::Debug,
141 ("SpeechSynthesis::AdvanceQueue length=%zu", mSpeechQueue.Length()));
143 if (mSpeechQueue.IsEmpty()) {
144 return;
147 RefPtr<SpeechSynthesisUtterance> utterance = mSpeechQueue.ElementAt(0);
149 nsAutoString docLang;
150 nsCOMPtr<nsPIDOMWindowInner> window = GetOwnerWindow();
151 if (Document* doc = window ? window->GetExtantDoc() : nullptr) {
152 if (Element* elm = doc->GetHtmlElement()) {
153 elm->GetLang(docLang);
157 mCurrentTask =
158 nsSynthVoiceRegistry::GetInstance()->SpeakUtterance(*utterance, docLang);
160 if (mCurrentTask) {
161 mCurrentTask->SetSpeechSynthesis(this);
165 void SpeechSynthesis::Cancel() {
166 if (!mSpeechQueue.IsEmpty() && HasSpeakingTask()) {
167 // Remove all queued utterances except for current one, we will remove it
168 // in OnEnd
169 mSpeechQueue.RemoveLastElements(mSpeechQueue.Length() - 1);
170 } else {
171 mSpeechQueue.Clear();
174 if (mCurrentTask) {
175 mCurrentTask->Cancel();
179 void SpeechSynthesis::Pause() {
180 if (Paused()) {
181 return;
184 if (!mSpeechQueue.IsEmpty() && HasSpeakingTask()) {
185 mCurrentTask->Pause();
186 } else {
187 mHoldQueue = true;
191 void SpeechSynthesis::Resume() {
192 if (!Paused()) {
193 return;
196 mHoldQueue = false;
198 if (mCurrentTask) {
199 mCurrentTask->Resume();
200 } else {
201 AdvanceQueue();
205 void SpeechSynthesis::OnEnd(const nsSpeechTask* aTask) {
206 MOZ_ASSERT(mCurrentTask == aTask);
208 if (!mSpeechQueue.IsEmpty()) {
209 mSpeechQueue.RemoveElementAt(0);
210 if (mSpeechQueue.IsEmpty()) {
211 RefPtr<WindowGlobalChild> wgc =
212 WindowGlobalChild::GetByInnerWindowId(mInnerID);
213 if (wgc) {
214 wgc->UnblockBFCacheFor(BFCacheStatus::HAS_ACTIVE_SPEECH_SYNTHESIS);
219 mCurrentTask = nullptr;
220 AdvanceQueue();
223 void SpeechSynthesis::GetVoices(
224 nsTArray<RefPtr<SpeechSynthesisVoice> >& aResult) {
225 aResult.Clear();
226 uint32_t voiceCount = 0;
227 nsCOMPtr<nsPIDOMWindowInner> window = GetOwnerWindow();
228 nsCOMPtr<nsIDocShell> docShell = window ? window->GetDocShell() : nullptr;
230 if (nsContentUtils::ShouldResistFingerprinting(docShell,
231 RFPTarget::SpeechSynthesis)) {
232 return;
235 nsresult rv = nsSynthVoiceRegistry::GetInstance()->GetVoiceCount(&voiceCount);
236 if (NS_WARN_IF(NS_FAILED(rv))) {
237 return;
240 nsISupports* voiceParent = NS_ISUPPORTS_CAST(nsIObserver*, this);
242 for (uint32_t i = 0; i < voiceCount; i++) {
243 nsAutoString uri;
244 rv = nsSynthVoiceRegistry::GetInstance()->GetVoice(i, uri);
246 if (NS_FAILED(rv)) {
247 NS_WARNING("Failed to retrieve voice from registry");
248 continue;
251 SpeechSynthesisVoice* voice = mVoiceCache.GetWeak(uri);
253 if (!voice) {
254 voice = new SpeechSynthesisVoice(voiceParent, uri);
257 aResult.AppendElement(voice);
260 mVoiceCache.Clear();
262 for (uint32_t i = 0; i < aResult.Length(); i++) {
263 SpeechSynthesisVoice* voice = aResult[i];
264 mVoiceCache.InsertOrUpdate(voice->mUri, RefPtr{voice});
268 // For testing purposes, allows us to cancel the current task that is
269 // misbehaving, and flush the queue.
270 void SpeechSynthesis::ForceEnd() {
271 if (mCurrentTask) {
272 mCurrentTask->ForceEnd();
276 NS_IMETHODIMP
277 SpeechSynthesis::Observe(nsISupports* aSubject, const char* aTopic,
278 const char16_t* aData) {
279 MOZ_ASSERT(NS_IsMainThread());
281 if (strcmp(aTopic, "inner-window-destroyed") == 0) {
282 nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject);
283 NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE);
285 uint64_t innerID;
286 nsresult rv = wrapper->GetData(&innerID);
287 NS_ENSURE_SUCCESS(rv, rv);
289 if (innerID == mInnerID) {
290 mInnerID = 0;
291 Cancel();
293 nsCOMPtr<nsIObserverService> obs =
294 mozilla::services::GetObserverService();
295 if (obs) {
296 obs->RemoveObserver(this, "inner-window-destroyed");
299 } else if (strcmp(aTopic, "synth-voices-changed") == 0) {
300 LOG(LogLevel::Debug, ("SpeechSynthesis::onvoiceschanged"));
301 nsCOMPtr<nsPIDOMWindowInner> window = GetOwnerWindow();
302 nsCOMPtr<nsIDocShell> docShell = window ? window->GetDocShell() : nullptr;
304 if (!nsContentUtils::ShouldResistFingerprinting(
305 docShell, RFPTarget::SpeechSynthesis)) {
306 DispatchTrustedEvent(u"voiceschanged"_ns);
307 // If we have a pending item, and voices become available, speak it.
308 if (!mCurrentTask && !mHoldQueue && HasVoices()) {
309 AdvanceQueue();
312 } else if (strcmp(aTopic, "synth-voices-error") == 0) {
313 NS_WARNING("SpeechSynthesis::Observe: synth-voices-error");
314 LOG(LogLevel::Debug, ("SpeechSynthesis::onvoiceserror"));
315 nsCOMPtr<nsPIDOMWindowInner> window = GetOwnerWindow();
317 nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
318 if (obs) {
319 obs->NotifyObservers(window, "chrome-synth-voices-error", aData);
322 if (!mSpeechQueue.IsEmpty()) {
323 for (RefPtr<SpeechSynthesisUtterance>& utterance : mSpeechQueue) {
324 utterance->DispatchSpeechSynthesisEvent(u"error"_ns, 0, nullptr, 0,
325 u""_ns);
327 mSpeechQueue.Clear();
331 return NS_OK;
334 } // namespace mozilla::dom