/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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 "nsISupportsPrimitives.h" #include "nsSpeechTask.h" #include "mozilla/Logging.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/SpeechSynthesisBinding.h" #include "mozilla/dom/WindowGlobalChild.h" #include "SpeechSynthesis.h" #include "nsContentUtils.h" #include "nsSynthVoiceRegistry.h" #include "mozilla/dom/Document.h" #include "nsIDocShell.h" #undef LOG mozilla::LogModule* GetSpeechSynthLog() { static mozilla::LazyLogModule sLog("SpeechSynthesis"); return sLog; } #define LOG(type, msg) MOZ_LOG(GetSpeechSynthLog(), type, msg) namespace mozilla::dom { NS_IMPL_CYCLE_COLLECTION_CLASS(SpeechSynthesis) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(SpeechSynthesis, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_UNLINK(mCurrentTask) NS_IMPL_CYCLE_COLLECTION_UNLINK(mSpeechQueue) tmp->mVoiceCache.Clear(); NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(SpeechSynthesis, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCurrentTask) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSpeechQueue) for (SpeechSynthesisVoice* voice : tmp->mVoiceCache.Values()) { cb.NoteXPCOMChild(voice); } NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechSynthesis) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) NS_IMPL_ADDREF_INHERITED(SpeechSynthesis, DOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(SpeechSynthesis, DOMEventTargetHelper) SpeechSynthesis::SpeechSynthesis(nsPIDOMWindowInner* aParent) : DOMEventTargetHelper(aParent), mHoldQueue(false), mInnerID(aParent->WindowID()) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->AddObserver(this, "inner-window-destroyed", true); obs->AddObserver(this, "synth-voices-changed", true); } } SpeechSynthesis::~SpeechSynthesis() = default; JSObject* SpeechSynthesis::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return SpeechSynthesis_Binding::Wrap(aCx, this, aGivenProto); } bool SpeechSynthesis::Pending() const { // If we don't have any task, nothing is pending. If we have only one task, // check if that task is currently pending. If we have more than one task, // then the tasks after the first one are definitely pending. return mSpeechQueue.Length() > 1 || (mSpeechQueue.Length() == 1 && (!mCurrentTask || mCurrentTask->IsPending())); } bool SpeechSynthesis::Speaking() const { // Check global speaking state if there is no active speaking task. return (!mSpeechQueue.IsEmpty() && HasSpeakingTask()) || nsSynthVoiceRegistry::GetInstance()->IsSpeaking(); } bool SpeechSynthesis::Paused() const { return mHoldQueue || (mCurrentTask && mCurrentTask->IsPrePaused()) || (!mSpeechQueue.IsEmpty() && mSpeechQueue.ElementAt(0)->IsPaused()); } bool SpeechSynthesis::HasEmptyQueue() const { return mSpeechQueue.Length() == 0; } bool SpeechSynthesis::HasVoices() const { uint32_t voiceCount = mVoiceCache.Count(); if (voiceCount == 0) { nsresult rv = nsSynthVoiceRegistry::GetInstance()->GetVoiceCount(&voiceCount); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } } return voiceCount != 0; } void SpeechSynthesis::Speak(SpeechSynthesisUtterance& aUtterance) { if (!mInnerID) { return; } mSpeechQueue.AppendElement(&aUtterance); if (mSpeechQueue.Length() == 1) { RefPtr wgc = WindowGlobalChild::GetByInnerWindowId(mInnerID); if (wgc) { wgc->BlockBFCacheFor(BFCacheStatus::HAS_ACTIVE_SPEECH_SYNTHESIS); } // If we only have one item in the queue, we aren't pre-paused, and // we have voices available, speak it. if (!mCurrentTask && !mHoldQueue && HasVoices()) { AdvanceQueue(); } } } void SpeechSynthesis::AdvanceQueue() { LOG(LogLevel::Debug, ("SpeechSynthesis::AdvanceQueue length=%zu", mSpeechQueue.Length())); if (mSpeechQueue.IsEmpty()) { return; } RefPtr utterance = mSpeechQueue.ElementAt(0); nsAutoString docLang; nsCOMPtr window = GetOwner(); if (Document* doc = window ? window->GetExtantDoc() : nullptr) { if (Element* elm = doc->GetHtmlElement()) { elm->GetLang(docLang); } } mCurrentTask = nsSynthVoiceRegistry::GetInstance()->SpeakUtterance(*utterance, docLang); if (mCurrentTask) { mCurrentTask->SetSpeechSynthesis(this); } } void SpeechSynthesis::Cancel() { if (!mSpeechQueue.IsEmpty() && HasSpeakingTask()) { // Remove all queued utterances except for current one, we will remove it // in OnEnd mSpeechQueue.RemoveLastElements(mSpeechQueue.Length() - 1); } else { mSpeechQueue.Clear(); } if (mCurrentTask) { mCurrentTask->Cancel(); } } void SpeechSynthesis::Pause() { if (Paused()) { return; } if (!mSpeechQueue.IsEmpty() && HasSpeakingTask()) { mCurrentTask->Pause(); } else { mHoldQueue = true; } } void SpeechSynthesis::Resume() { if (!Paused()) { return; } mHoldQueue = false; if (mCurrentTask) { mCurrentTask->Resume(); } else { AdvanceQueue(); } } void SpeechSynthesis::OnEnd(const nsSpeechTask* aTask) { MOZ_ASSERT(mCurrentTask == aTask); if (!mSpeechQueue.IsEmpty()) { mSpeechQueue.RemoveElementAt(0); if (mSpeechQueue.IsEmpty()) { RefPtr wgc = WindowGlobalChild::GetByInnerWindowId(mInnerID); if (wgc) { wgc->UnblockBFCacheFor(BFCacheStatus::HAS_ACTIVE_SPEECH_SYNTHESIS); } } } mCurrentTask = nullptr; AdvanceQueue(); } void SpeechSynthesis::GetVoices( nsTArray >& aResult) { aResult.Clear(); uint32_t voiceCount = 0; nsCOMPtr window = GetOwner(); nsCOMPtr docShell = window ? window->GetDocShell() : nullptr; if (nsContentUtils::ShouldResistFingerprinting(docShell, RFPTarget::SpeechSynthesis)) { return; } nsresult rv = nsSynthVoiceRegistry::GetInstance()->GetVoiceCount(&voiceCount); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsISupports* voiceParent = NS_ISUPPORTS_CAST(nsIObserver*, this); for (uint32_t i = 0; i < voiceCount; i++) { nsAutoString uri; rv = nsSynthVoiceRegistry::GetInstance()->GetVoice(i, uri); if (NS_FAILED(rv)) { NS_WARNING("Failed to retrieve voice from registry"); continue; } SpeechSynthesisVoice* voice = mVoiceCache.GetWeak(uri); if (!voice) { voice = new SpeechSynthesisVoice(voiceParent, uri); } aResult.AppendElement(voice); } mVoiceCache.Clear(); for (uint32_t i = 0; i < aResult.Length(); i++) { SpeechSynthesisVoice* voice = aResult[i]; mVoiceCache.InsertOrUpdate(voice->mUri, RefPtr{voice}); } } // For testing purposes, allows us to cancel the current task that is // misbehaving, and flush the queue. void SpeechSynthesis::ForceEnd() { if (mCurrentTask) { mCurrentTask->ForceEnd(); } } NS_IMETHODIMP SpeechSynthesis::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { MOZ_ASSERT(NS_IsMainThread()); if (strcmp(aTopic, "inner-window-destroyed") == 0) { nsCOMPtr wrapper = do_QueryInterface(aSubject); NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); uint64_t innerID; nsresult rv = wrapper->GetData(&innerID); NS_ENSURE_SUCCESS(rv, rv); if (innerID == mInnerID) { mInnerID = 0; Cancel(); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->RemoveObserver(this, "inner-window-destroyed"); } } } else if (strcmp(aTopic, "synth-voices-changed") == 0) { LOG(LogLevel::Debug, ("SpeechSynthesis::onvoiceschanged")); nsCOMPtr window = GetOwner(); nsCOMPtr docShell = window ? window->GetDocShell() : nullptr; if (!nsContentUtils::ShouldResistFingerprinting( docShell, RFPTarget::SpeechSynthesis)) { DispatchTrustedEvent(u"voiceschanged"_ns); // If we have a pending item, and voices become available, speak it. if (!mCurrentTask && !mHoldQueue && HasVoices()) { AdvanceQueue(); } } } return NS_OK; } } // namespace mozilla::dom