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"
23 mozilla::LogModule
* GetSpeechSynthLog() {
24 static mozilla::LazyLogModule
sLog("SpeechSynthesis");
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
,
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
,
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
),
62 mInnerID(aParent
->WindowID()) {
63 MOZ_ASSERT(NS_IsMainThread());
65 nsCOMPtr
<nsIObserverService
> obs
= mozilla::services::GetObserverService();
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) {
108 nsSynthVoiceRegistry::GetInstance()->GetVoiceCount(&voiceCount
);
109 if (NS_WARN_IF(NS_FAILED(rv
))) {
114 return voiceCount
!= 0;
117 void SpeechSynthesis::Speak(SpeechSynthesisUtterance
& aUtterance
) {
122 mSpeechQueue
.AppendElement(&aUtterance
);
124 if (mSpeechQueue
.Length() == 1) {
125 RefPtr
<WindowGlobalChild
> wgc
=
126 WindowGlobalChild::GetByInnerWindowId(mInnerID
);
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()) {
139 void SpeechSynthesis::AdvanceQueue() {
141 ("SpeechSynthesis::AdvanceQueue length=%zu", mSpeechQueue
.Length()));
143 if (mSpeechQueue
.IsEmpty()) {
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
);
158 nsSynthVoiceRegistry::GetInstance()->SpeakUtterance(*utterance
, docLang
);
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
169 mSpeechQueue
.RemoveLastElements(mSpeechQueue
.Length() - 1);
171 mSpeechQueue
.Clear();
175 mCurrentTask
->Cancel();
179 void SpeechSynthesis::Pause() {
184 if (!mSpeechQueue
.IsEmpty() && HasSpeakingTask()) {
185 mCurrentTask
->Pause();
191 void SpeechSynthesis::Resume() {
199 mCurrentTask
->Resume();
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
);
214 wgc
->UnblockBFCacheFor(BFCacheStatus::HAS_ACTIVE_SPEECH_SYNTHESIS
);
219 mCurrentTask
= nullptr;
223 void SpeechSynthesis::GetVoices(
224 nsTArray
<RefPtr
<SpeechSynthesisVoice
> >& aResult
) {
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
)) {
235 nsresult rv
= nsSynthVoiceRegistry::GetInstance()->GetVoiceCount(&voiceCount
);
236 if (NS_WARN_IF(NS_FAILED(rv
))) {
240 nsISupports
* voiceParent
= NS_ISUPPORTS_CAST(nsIObserver
*, this);
242 for (uint32_t i
= 0; i
< voiceCount
; i
++) {
244 rv
= nsSynthVoiceRegistry::GetInstance()->GetVoice(i
, uri
);
247 NS_WARNING("Failed to retrieve voice from registry");
251 SpeechSynthesisVoice
* voice
= mVoiceCache
.GetWeak(uri
);
254 voice
= new SpeechSynthesisVoice(voiceParent
, uri
);
257 aResult
.AppendElement(voice
);
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() {
272 mCurrentTask
->ForceEnd();
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
);
286 nsresult rv
= wrapper
->GetData(&innerID
);
287 NS_ENSURE_SUCCESS(rv
, rv
);
289 if (innerID
== mInnerID
) {
293 nsCOMPtr
<nsIObserverService
> obs
=
294 mozilla::services::GetObserverService();
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()) {
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();
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,
327 mSpeechQueue
.Clear();
334 } // namespace mozilla::dom