Line data Source code
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/. */
6 :
7 : #include "nsISupportsPrimitives.h"
8 : #include "nsSpeechTask.h"
9 : #include "mozilla/Logging.h"
10 : #include "mozilla/SizePrintfMacros.h"
11 :
12 : #include "mozilla/dom/ContentChild.h"
13 : #include "mozilla/dom/Element.h"
14 :
15 : #include "mozilla/dom/SpeechSynthesisBinding.h"
16 : #include "SpeechSynthesis.h"
17 : #include "nsSynthVoiceRegistry.h"
18 : #include "nsIDocument.h"
19 :
20 : #undef LOG
21 : mozilla::LogModule*
22 0 : GetSpeechSynthLog()
23 : {
24 : static mozilla::LazyLogModule sLog("SpeechSynthesis");
25 :
26 0 : return sLog;
27 : }
28 : #define LOG(type, msg) MOZ_LOG(GetSpeechSynthLog(), type, msg)
29 :
30 : namespace mozilla {
31 : namespace dom {
32 :
33 : NS_IMPL_CYCLE_COLLECTION_CLASS(SpeechSynthesis)
34 :
35 0 : NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(SpeechSynthesis, DOMEventTargetHelper)
36 0 : NS_IMPL_CYCLE_COLLECTION_UNLINK(mCurrentTask)
37 0 : NS_IMPL_CYCLE_COLLECTION_UNLINK(mSpeechQueue)
38 0 : tmp->mVoiceCache.Clear();
39 0 : NS_IMPL_CYCLE_COLLECTION_UNLINK_END
40 :
41 0 : NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(SpeechSynthesis, DOMEventTargetHelper)
42 0 : NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCurrentTask)
43 0 : NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSpeechQueue)
44 0 : for (auto iter = tmp->mVoiceCache.Iter(); !iter.Done(); iter.Next()) {
45 0 : SpeechSynthesisVoice* voice = iter.UserData();
46 0 : cb.NoteXPCOMChild(voice);
47 : }
48 0 : NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
49 :
50 0 : NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(SpeechSynthesis)
51 0 : NS_INTERFACE_MAP_ENTRY(nsIObserver)
52 0 : NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
53 0 : NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
54 :
55 0 : NS_IMPL_ADDREF_INHERITED(SpeechSynthesis, DOMEventTargetHelper)
56 0 : NS_IMPL_RELEASE_INHERITED(SpeechSynthesis, DOMEventTargetHelper)
57 :
58 0 : SpeechSynthesis::SpeechSynthesis(nsPIDOMWindowInner* aParent)
59 : : DOMEventTargetHelper(aParent)
60 : , mHoldQueue(false)
61 0 : , mInnerID(aParent->WindowID())
62 : {
63 0 : MOZ_ASSERT(aParent->IsInnerWindow());
64 0 : MOZ_ASSERT(NS_IsMainThread());
65 :
66 0 : nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
67 0 : if (obs) {
68 0 : obs->AddObserver(this, "inner-window-destroyed", true);
69 0 : obs->AddObserver(this, "synth-voices-changed", true);
70 : }
71 :
72 0 : }
73 :
74 0 : SpeechSynthesis::~SpeechSynthesis()
75 : {
76 0 : }
77 :
78 : JSObject*
79 0 : SpeechSynthesis::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
80 : {
81 0 : return SpeechSynthesisBinding::Wrap(aCx, this, aGivenProto);
82 : }
83 :
84 : bool
85 0 : SpeechSynthesis::Pending() const
86 : {
87 0 : switch (mSpeechQueue.Length()) {
88 : case 0:
89 0 : return false;
90 :
91 : case 1:
92 0 : return mSpeechQueue.ElementAt(0)->GetState() == SpeechSynthesisUtterance::STATE_PENDING;
93 :
94 : default:
95 0 : return true;
96 : }
97 : }
98 :
99 : bool
100 0 : SpeechSynthesis::Speaking() const
101 : {
102 0 : if (!mSpeechQueue.IsEmpty() &&
103 0 : mSpeechQueue.ElementAt(0)->GetState() == SpeechSynthesisUtterance::STATE_SPEAKING) {
104 0 : return true;
105 : }
106 :
107 : // Returns global speaking state if global queue is enabled. Or false.
108 0 : return nsSynthVoiceRegistry::GetInstance()->IsSpeaking();
109 : }
110 :
111 : bool
112 0 : SpeechSynthesis::Paused() const
113 : {
114 0 : return mHoldQueue || (mCurrentTask && mCurrentTask->IsPrePaused()) ||
115 0 : (!mSpeechQueue.IsEmpty() && mSpeechQueue.ElementAt(0)->IsPaused());
116 : }
117 :
118 : bool
119 0 : SpeechSynthesis::HasEmptyQueue() const
120 : {
121 0 : return mSpeechQueue.Length() == 0;
122 : }
123 :
124 0 : bool SpeechSynthesis::HasVoices() const
125 : {
126 0 : uint32_t voiceCount = mVoiceCache.Count();
127 0 : if (voiceCount == 0) {
128 0 : nsresult rv = nsSynthVoiceRegistry::GetInstance()->GetVoiceCount(&voiceCount);
129 0 : if(NS_WARN_IF(NS_FAILED(rv))) {
130 0 : return false;
131 : }
132 : }
133 :
134 0 : return voiceCount != 0;
135 : }
136 :
137 : void
138 0 : SpeechSynthesis::Speak(SpeechSynthesisUtterance& aUtterance)
139 : {
140 0 : if (aUtterance.mState != SpeechSynthesisUtterance::STATE_NONE) {
141 : // XXX: Should probably raise an error
142 0 : return;
143 : }
144 :
145 0 : mSpeechQueue.AppendElement(&aUtterance);
146 0 : aUtterance.mState = SpeechSynthesisUtterance::STATE_PENDING;
147 :
148 : // If we only have one item in the queue, we aren't pre-paused, and
149 : // we have voices available, speak it.
150 0 : if (mSpeechQueue.Length() == 1 && !mCurrentTask && !mHoldQueue && HasVoices()) {
151 0 : AdvanceQueue();
152 : }
153 : }
154 :
155 : void
156 0 : SpeechSynthesis::AdvanceQueue()
157 : {
158 0 : LOG(LogLevel::Debug,
159 : ("SpeechSynthesis::AdvanceQueue length=%" PRIuSIZE, mSpeechQueue.Length()));
160 :
161 0 : if (mSpeechQueue.IsEmpty()) {
162 0 : return;
163 : }
164 :
165 0 : RefPtr<SpeechSynthesisUtterance> utterance = mSpeechQueue.ElementAt(0);
166 :
167 0 : nsAutoString docLang;
168 0 : nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
169 0 : nsIDocument* doc = window ? window->GetExtantDoc() : nullptr;
170 :
171 0 : if (doc) {
172 0 : Element* elm = doc->GetHtmlElement();
173 :
174 0 : if (elm) {
175 0 : elm->GetLang(docLang);
176 : }
177 : }
178 :
179 : mCurrentTask =
180 0 : nsSynthVoiceRegistry::GetInstance()->SpeakUtterance(*utterance, docLang);
181 :
182 0 : if (mCurrentTask) {
183 0 : mCurrentTask->SetSpeechSynthesis(this);
184 : }
185 :
186 0 : return;
187 : }
188 :
189 : void
190 0 : SpeechSynthesis::Cancel()
191 : {
192 0 : if (!mSpeechQueue.IsEmpty() &&
193 0 : mSpeechQueue.ElementAt(0)->GetState() == SpeechSynthesisUtterance::STATE_SPEAKING) {
194 : // Remove all queued utterances except for current one, we will remove it
195 : // in OnEnd
196 0 : mSpeechQueue.RemoveElementsAt(1, mSpeechQueue.Length() - 1);
197 : } else {
198 0 : mSpeechQueue.Clear();
199 : }
200 :
201 0 : if (mCurrentTask) {
202 0 : mCurrentTask->Cancel();
203 : }
204 0 : }
205 :
206 : void
207 0 : SpeechSynthesis::Pause()
208 : {
209 0 : if (Paused()) {
210 0 : return;
211 : }
212 :
213 0 : if (mCurrentTask && !mSpeechQueue.IsEmpty() &&
214 0 : mSpeechQueue.ElementAt(0)->GetState() != SpeechSynthesisUtterance::STATE_ENDED) {
215 0 : mCurrentTask->Pause();
216 : } else {
217 0 : mHoldQueue = true;
218 : }
219 : }
220 :
221 : void
222 0 : SpeechSynthesis::Resume()
223 : {
224 0 : if (!Paused()) {
225 0 : return;
226 : }
227 :
228 0 : if (mCurrentTask) {
229 0 : mCurrentTask->Resume();
230 : } else {
231 0 : mHoldQueue = false;
232 0 : AdvanceQueue();
233 : }
234 : }
235 :
236 : void
237 0 : SpeechSynthesis::OnEnd(const nsSpeechTask* aTask)
238 : {
239 0 : MOZ_ASSERT(mCurrentTask == aTask);
240 :
241 0 : if (!mSpeechQueue.IsEmpty()) {
242 0 : mSpeechQueue.RemoveElementAt(0);
243 : }
244 :
245 0 : mCurrentTask = nullptr;
246 0 : AdvanceQueue();
247 0 : }
248 :
249 : void
250 0 : SpeechSynthesis::GetVoices(nsTArray< RefPtr<SpeechSynthesisVoice> >& aResult)
251 : {
252 0 : aResult.Clear();
253 0 : uint32_t voiceCount = 0;
254 :
255 0 : nsresult rv = nsSynthVoiceRegistry::GetInstance()->GetVoiceCount(&voiceCount);
256 0 : if(NS_WARN_IF(NS_FAILED(rv))) {
257 0 : return;
258 : }
259 :
260 0 : nsISupports* voiceParent = NS_ISUPPORTS_CAST(nsIObserver*, this);
261 :
262 0 : for (uint32_t i = 0; i < voiceCount; i++) {
263 0 : nsAutoString uri;
264 0 : rv = nsSynthVoiceRegistry::GetInstance()->GetVoice(i, uri);
265 :
266 0 : if (NS_FAILED(rv)) {
267 0 : NS_WARNING("Failed to retrieve voice from registry");
268 0 : continue;
269 : }
270 :
271 0 : SpeechSynthesisVoice* voice = mVoiceCache.GetWeak(uri);
272 :
273 0 : if (!voice) {
274 0 : voice = new SpeechSynthesisVoice(voiceParent, uri);
275 : }
276 :
277 0 : aResult.AppendElement(voice);
278 : }
279 :
280 0 : mVoiceCache.Clear();
281 :
282 0 : for (uint32_t i = 0; i < aResult.Length(); i++) {
283 0 : SpeechSynthesisVoice* voice = aResult[i];
284 0 : mVoiceCache.Put(voice->mUri, voice);
285 : }
286 : }
287 :
288 : // For testing purposes, allows us to cancel the current task that is
289 : // misbehaving, and flush the queue.
290 : void
291 0 : SpeechSynthesis::ForceEnd()
292 : {
293 0 : if (mCurrentTask) {
294 0 : mCurrentTask->ForceEnd();
295 : }
296 0 : }
297 :
298 : NS_IMETHODIMP
299 0 : SpeechSynthesis::Observe(nsISupports* aSubject, const char* aTopic,
300 : const char16_t* aData)
301 : {
302 0 : MOZ_ASSERT(NS_IsMainThread());
303 :
304 :
305 0 : if (strcmp(aTopic, "inner-window-destroyed") == 0) {
306 0 : nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject);
307 0 : NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE);
308 :
309 : uint64_t innerID;
310 0 : nsresult rv = wrapper->GetData(&innerID);
311 0 : NS_ENSURE_SUCCESS(rv, rv);
312 :
313 0 : if (innerID == mInnerID) {
314 0 : Cancel();
315 :
316 0 : nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
317 0 : if (obs) {
318 0 : obs->RemoveObserver(this, "inner-window-destroyed");
319 : }
320 : }
321 0 : } else if (strcmp(aTopic, "synth-voices-changed") == 0) {
322 0 : LOG(LogLevel::Debug, ("SpeechSynthesis::onvoiceschanged"));
323 0 : DispatchTrustedEvent(NS_LITERAL_STRING("voiceschanged"));
324 : // If we have a pending item, and voices become available, speak it.
325 0 : if (!mCurrentTask && !mHoldQueue && HasVoices()) {
326 0 : AdvanceQueue();
327 : }
328 : }
329 :
330 0 : return NS_OK;
331 : }
332 :
333 : } // namespace dom
334 : } // namespace mozilla
|