diff options
Diffstat (limited to 'dom/audiochannel/AudioChannelService.cpp')
-rw-r--r-- | dom/audiochannel/AudioChannelService.cpp | 625 |
1 files changed, 625 insertions, 0 deletions
diff --git a/dom/audiochannel/AudioChannelService.cpp b/dom/audiochannel/AudioChannelService.cpp new file mode 100644 index 0000000000..a29bab62ef --- /dev/null +++ b/dom/audiochannel/AudioChannelService.cpp @@ -0,0 +1,625 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "AudioChannelService.h" + +#include "base/basictypes.h" + +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/Document.h" + +#include "nsContentUtils.h" +#include "nsISupportsPrimitives.h" +#include "nsThreadUtils.h" +#include "nsHashPropertyBag.h" +#include "nsComponentManagerUtils.h" +#include "nsGlobalWindow.h" +#include "nsPIDOMWindow.h" +#include "nsServiceManagerUtils.h" + +#include "mozilla/Preferences.h" + +using namespace mozilla; +using namespace mozilla::dom; + +mozilla::LazyLogModule gAudioChannelLog("AudioChannel"); + +namespace { + +bool sXPCOMShuttingDown = false; + +class AudioPlaybackRunnable final : public Runnable { + public: + AudioPlaybackRunnable(nsPIDOMWindowOuter* aWindow, bool aActive, + AudioChannelService::AudibleChangedReasons aReason) + : mozilla::Runnable("AudioPlaybackRunnable"), + mWindow(aWindow), + mActive(aActive), + mReason(aReason) {} + + NS_IMETHOD Run() override { + nsCOMPtr<nsIObserverService> observerService = + services::GetObserverService(); + if (NS_WARN_IF(!observerService)) { + return NS_ERROR_FAILURE; + } + + nsAutoString state; + GetActiveState(state); + + observerService->NotifyObservers(ToSupports(mWindow), "audio-playback", + state.get()); + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioPlaybackRunnable, active = %s, reason = %s\n", + mActive ? "true" : "false", AudibleChangedReasonToStr(mReason))); + + return NS_OK; + } + + private: + void GetActiveState(nsAString& aState) { + if (mActive) { + aState.AssignLiteral("active"); + } else { + if (mReason == + AudioChannelService::AudibleChangedReasons::ePauseStateChanged) { + aState.AssignLiteral("inactive-pause"); + } else { + aState.AssignLiteral("inactive-nonaudible"); + } + } + } + + nsCOMPtr<nsPIDOMWindowOuter> mWindow; + bool mActive; + AudioChannelService::AudibleChangedReasons mReason; +}; + +} // anonymous namespace + +namespace mozilla::dom { + +const char* SuspendTypeToStr(const nsSuspendedTypes& aSuspend) { + MOZ_ASSERT(aSuspend == nsISuspendedTypes::NONE_SUSPENDED || + aSuspend == nsISuspendedTypes::SUSPENDED_BLOCK); + + switch (aSuspend) { + case nsISuspendedTypes::NONE_SUSPENDED: + return "none"; + case nsISuspendedTypes::SUSPENDED_BLOCK: + return "block"; + default: + return "unknown"; + } +} + +const char* AudibleStateToStr( + const AudioChannelService::AudibleState& aAudible) { + MOZ_ASSERT(aAudible == AudioChannelService::AudibleState::eNotAudible || + aAudible == AudioChannelService::AudibleState::eMaybeAudible || + aAudible == AudioChannelService::AudibleState::eAudible); + + switch (aAudible) { + case AudioChannelService::AudibleState::eNotAudible: + return "not-audible"; + case AudioChannelService::AudibleState::eMaybeAudible: + return "maybe-audible"; + case AudioChannelService::AudibleState::eAudible: + return "audible"; + default: + return "unknown"; + } +} + +const char* AudibleChangedReasonToStr( + const AudioChannelService::AudibleChangedReasons& aReason) { + MOZ_ASSERT( + aReason == AudioChannelService::AudibleChangedReasons::eVolumeChanged || + aReason == + AudioChannelService::AudibleChangedReasons::eDataAudibleChanged || + aReason == + AudioChannelService::AudibleChangedReasons::ePauseStateChanged); + + switch (aReason) { + case AudioChannelService::AudibleChangedReasons::eVolumeChanged: + return "volume"; + case AudioChannelService::AudibleChangedReasons::eDataAudibleChanged: + return "data-audible"; + case AudioChannelService::AudibleChangedReasons::ePauseStateChanged: + return "pause-state"; + default: + return "unknown"; + } +} + +StaticRefPtr<AudioChannelService> gAudioChannelService; + +/* static */ +void AudioChannelService::CreateServiceIfNeeded() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!gAudioChannelService) { + gAudioChannelService = new AudioChannelService(); + } +} + +/* static */ +already_AddRefed<AudioChannelService> AudioChannelService::GetOrCreate() { + if (sXPCOMShuttingDown) { + return nullptr; + } + + CreateServiceIfNeeded(); + RefPtr<AudioChannelService> service = gAudioChannelService.get(); + return service.forget(); +} + +/* static */ +already_AddRefed<AudioChannelService> AudioChannelService::Get() { + if (sXPCOMShuttingDown) { + return nullptr; + } + + RefPtr<AudioChannelService> service = gAudioChannelService.get(); + return service.forget(); +} + +/* static */ +LogModule* AudioChannelService::GetAudioChannelLog() { + return gAudioChannelLog; +} + +/* static */ +void AudioChannelService::Shutdown() { + if (gAudioChannelService) { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(gAudioChannelService, "xpcom-shutdown"); + obs->RemoveObserver(gAudioChannelService, "outer-window-destroyed"); + } + + gAudioChannelService->mWindows.Clear(); + + gAudioChannelService = nullptr; + } +} + +NS_INTERFACE_MAP_BEGIN(AudioChannelService) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsIObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(AudioChannelService) +NS_IMPL_RELEASE(AudioChannelService) + +AudioChannelService::AudioChannelService() { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->AddObserver(this, "xpcom-shutdown", false); + obs->AddObserver(this, "outer-window-destroyed", false); + } +} + +AudioChannelService::~AudioChannelService() = default; + +void AudioChannelService::RegisterAudioChannelAgent(AudioChannelAgent* aAgent, + AudibleState aAudible) { + MOZ_ASSERT(aAgent); + + uint64_t windowID = aAgent->WindowID(); + AudioChannelWindow* winData = GetWindowData(windowID); + if (!winData) { + winData = new AudioChannelWindow(windowID); + mWindows.AppendElement(WrapUnique(winData)); + } + + // To make sure agent would be alive because AppendAgent() would trigger the + // callback function of AudioChannelAgentOwner that means the agent might be + // released in their callback. + RefPtr<AudioChannelAgent> kungFuDeathGrip(aAgent); + winData->AppendAgent(aAgent, aAudible); +} + +void AudioChannelService::UnregisterAudioChannelAgent( + AudioChannelAgent* aAgent) { + MOZ_ASSERT(aAgent); + + AudioChannelWindow* winData = GetWindowData(aAgent->WindowID()); + if (!winData) { + return; + } + + // To make sure agent would be alive because AppendAgent() would trigger the + // callback function of AudioChannelAgentOwner that means the agent might be + // released in their callback. + RefPtr<AudioChannelAgent> kungFuDeathGrip(aAgent); + winData->RemoveAgent(aAgent); +} + +AudioPlaybackConfig AudioChannelService::GetMediaConfig( + nsPIDOMWindowOuter* aWindow) const { + AudioPlaybackConfig config(1.0, false, nsISuspendedTypes::NONE_SUSPENDED); + + if (!aWindow) { + config.mVolume = 0.0; + config.mMuted = true; + config.mSuspend = nsISuspendedTypes::SUSPENDED_BLOCK; + return config; + } + + AudioChannelWindow* winData = nullptr; + nsCOMPtr<nsPIDOMWindowOuter> window = aWindow; + + // The volume must be calculated based on the window hierarchy. Here we go up + // to the top window and we calculate the volume and the muted flag. + do { + winData = GetWindowData(window->WindowID()); + if (winData) { + config.mVolume *= winData->mConfig.mVolume; + config.mMuted = config.mMuted || winData->mConfig.mMuted; + config.mCapturedAudio = winData->mIsAudioCaptured; + } + + config.mMuted = config.mMuted || window->GetAudioMuted(); + if (window->ShouldDelayMediaFromStart()) { + config.mSuspend = nsISuspendedTypes::SUSPENDED_BLOCK; + } + + nsCOMPtr<nsPIDOMWindowOuter> win = + window->GetInProcessScriptableParentOrNull(); + if (!win) { + break; + } + + window = win; + + // If there is no parent, or we are the toplevel we don't continue. + } while (window && window != aWindow); + + return config; +} + +void AudioChannelService::AudioAudibleChanged(AudioChannelAgent* aAgent, + AudibleState aAudible, + AudibleChangedReasons aReason) { + MOZ_ASSERT(aAgent); + + uint64_t windowID = aAgent->WindowID(); + AudioChannelWindow* winData = GetWindowData(windowID); + if (winData) { + winData->AudioAudibleChanged(aAgent, aAudible, aReason); + } +} + +NS_IMETHODIMP +AudioChannelService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, "xpcom-shutdown")) { + sXPCOMShuttingDown = true; + Shutdown(); + } else if (!strcmp(aTopic, "outer-window-destroyed")) { + nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject); + NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); + + uint64_t outerID; + nsresult rv = wrapper->GetData(&outerID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + UniquePtr<AudioChannelWindow> winData; + { + nsTObserverArray<UniquePtr<AudioChannelWindow>>::ForwardIterator iter( + mWindows); + while (iter.HasMore()) { + auto& next = iter.GetNext(); + if (next->mWindowID == outerID) { + winData = std::move(next); + iter.Remove(); + break; + } + } + } + + if (winData) { + for (AudioChannelAgent* agent : winData->mAgents.ForwardRange()) { + agent->WindowVolumeChanged(winData->mConfig.mVolume, + winData->mConfig.mMuted); + } + } + } + + return NS_OK; +} + +void AudioChannelService::RefreshAgents( + nsPIDOMWindowOuter* aWindow, + const std::function<void(AudioChannelAgent*)>& aFunc) { + MOZ_ASSERT(aWindow); + + nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetInProcessScriptableTop(); + if (!topWindow) { + return; + } + + AudioChannelWindow* winData = GetWindowData(topWindow->WindowID()); + if (!winData) { + return; + } + + for (AudioChannelAgent* agent : winData->mAgents.ForwardRange()) { + aFunc(agent); + } +} + +void AudioChannelService::RefreshAgentsVolume(nsPIDOMWindowOuter* aWindow, + float aVolume, bool aMuted) { + RefreshAgents(aWindow, [aVolume, aMuted](AudioChannelAgent* agent) { + agent->WindowVolumeChanged(aVolume, aMuted); + }); +} + +void AudioChannelService::RefreshAgentsSuspend(nsPIDOMWindowOuter* aWindow, + nsSuspendedTypes aSuspend) { + RefreshAgents(aWindow, [aSuspend](AudioChannelAgent* agent) { + agent->WindowSuspendChanged(aSuspend); + }); +} + +void AudioChannelService::SetWindowAudioCaptured(nsPIDOMWindowOuter* aWindow, + uint64_t aInnerWindowID, + bool aCapture) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + + MOZ_LOG(GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelService, SetWindowAudioCaptured, window = %p, " + "aCapture = %d\n", + aWindow, aCapture)); + + nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetInProcessScriptableTop(); + if (!topWindow) { + return; + } + + AudioChannelWindow* winData = GetWindowData(topWindow->WindowID()); + + // This can happen, but only during shutdown, because the the outer window + // changes ScriptableTop, so that its ID is different. + // In this case either we are capturing, and it's too late because the window + // has been closed anyways, or we are un-capturing, and everything has already + // been cleaned up by the HTMLMediaElements or the AudioContexts. + if (!winData) { + return; + } + + if (aCapture != winData->mIsAudioCaptured) { + winData->mIsAudioCaptured = aCapture; + for (AudioChannelAgent* agent : winData->mAgents.ForwardRange()) { + agent->WindowAudioCaptureChanged(aInnerWindowID, aCapture); + } + } +} + +AudioChannelService::AudioChannelWindow* +AudioChannelService::GetOrCreateWindowData(nsPIDOMWindowOuter* aWindow) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + + AudioChannelWindow* winData = GetWindowData(aWindow->WindowID()); + if (!winData) { + winData = new AudioChannelWindow(aWindow->WindowID()); + mWindows.AppendElement(WrapUnique(winData)); + } + + return winData; +} + +AudioChannelService::AudioChannelWindow* AudioChannelService::GetWindowData( + uint64_t aWindowID) const { + const auto [begin, end] = mWindows.NonObservingRange(); + const auto foundIt = std::find_if(begin, end, [aWindowID](const auto& next) { + return next->mWindowID == aWindowID; + }); + return foundIt != end ? foundIt->get() : nullptr; +} + +bool AudioChannelService::IsWindowActive(nsPIDOMWindowOuter* aWindow) { + MOZ_ASSERT(NS_IsMainThread()); + + auto* window = nsPIDOMWindowOuter::From(aWindow)->GetInProcessScriptableTop(); + if (!window) { + return false; + } + + AudioChannelWindow* winData = GetWindowData(window->WindowID()); + if (!winData) { + return false; + } + + return !winData->mAudibleAgents.IsEmpty(); +} + +void AudioChannelService::NotifyResumingDelayedMedia( + nsPIDOMWindowOuter* aWindow) { + MOZ_ASSERT(aWindow); + + nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetInProcessScriptableTop(); + if (!topWindow) { + return; + } + + AudioChannelWindow* winData = GetWindowData(topWindow->WindowID()); + if (!winData) { + return; + } + + winData->NotifyMediaBlockStop(aWindow); + RefreshAgentsSuspend(aWindow, nsISuspendedTypes::NONE_SUSPENDED); +} + +void AudioChannelService::AudioChannelWindow::AppendAgent( + AudioChannelAgent* aAgent, AudibleState aAudible) { + MOZ_ASSERT(aAgent); + + AppendAgentAndIncreaseAgentsNum(aAgent); + AudioAudibleChanged(aAgent, aAudible, + AudibleChangedReasons::eDataAudibleChanged); +} + +void AudioChannelService::AudioChannelWindow::RemoveAgent( + AudioChannelAgent* aAgent) { + MOZ_ASSERT(aAgent); + + RemoveAgentAndReduceAgentsNum(aAgent); + AudioAudibleChanged(aAgent, AudibleState::eNotAudible, + AudibleChangedReasons::ePauseStateChanged); +} + +void AudioChannelService::AudioChannelWindow::NotifyMediaBlockStop( + nsPIDOMWindowOuter* aWindow) { + if (mShouldSendActiveMediaBlockStopEvent) { + mShouldSendActiveMediaBlockStopEvent = false; + nsCOMPtr<nsPIDOMWindowOuter> window = aWindow; + NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "dom::AudioChannelService::AudioChannelWindow::NotifyMediaBlockStop", + [window]() -> void { + nsCOMPtr<nsIObserverService> observerService = + services::GetObserverService(); + if (NS_WARN_IF(!observerService)) { + return; + } + + observerService->NotifyObservers(ToSupports(window), "audio-playback", + u"activeMediaBlockStop"); + })); + } +} + +void AudioChannelService::AudioChannelWindow::AppendAgentAndIncreaseAgentsNum( + AudioChannelAgent* aAgent) { + MOZ_ASSERT(aAgent); + MOZ_ASSERT(!mAgents.Contains(aAgent)); + + mAgents.AppendElement(aAgent); + + ++mConfig.mNumberOfAgents; +} + +void AudioChannelService::AudioChannelWindow::RemoveAgentAndReduceAgentsNum( + AudioChannelAgent* aAgent) { + MOZ_ASSERT(aAgent); + MOZ_ASSERT(mAgents.Contains(aAgent)); + + mAgents.RemoveElement(aAgent); + + MOZ_ASSERT(mConfig.mNumberOfAgents > 0); + --mConfig.mNumberOfAgents; +} + +void AudioChannelService::AudioChannelWindow::AudioAudibleChanged( + AudioChannelAgent* aAgent, AudibleState aAudible, + AudibleChangedReasons aReason) { + MOZ_ASSERT(aAgent); + + if (aAudible == AudibleState::eAudible) { + AppendAudibleAgentIfNotContained(aAgent, aReason); + } else { + RemoveAudibleAgentIfContained(aAgent, aReason); + } + + if (aAudible != AudibleState::eNotAudible) { + MaybeNotifyMediaBlockStart(aAgent); + } +} + +void AudioChannelService::AudioChannelWindow::AppendAudibleAgentIfNotContained( + AudioChannelAgent* aAgent, AudibleChangedReasons aReason) { + MOZ_ASSERT(aAgent); + MOZ_ASSERT(mAgents.Contains(aAgent)); + + if (!mAudibleAgents.Contains(aAgent)) { + mAudibleAgents.AppendElement(aAgent); + if (IsFirstAudibleAgent()) { + NotifyAudioAudibleChanged(aAgent->Window(), AudibleState::eAudible, + aReason); + } + } +} + +void AudioChannelService::AudioChannelWindow::RemoveAudibleAgentIfContained( + AudioChannelAgent* aAgent, AudibleChangedReasons aReason) { + MOZ_ASSERT(aAgent); + + if (mAudibleAgents.Contains(aAgent)) { + mAudibleAgents.RemoveElement(aAgent); + if (IsLastAudibleAgent()) { + NotifyAudioAudibleChanged(aAgent->Window(), AudibleState::eNotAudible, + aReason); + } + } +} + +bool AudioChannelService::AudioChannelWindow::IsFirstAudibleAgent() const { + return (mAudibleAgents.Length() == 1); +} + +bool AudioChannelService::AudioChannelWindow::IsLastAudibleAgent() const { + return mAudibleAgents.IsEmpty(); +} + +void AudioChannelService::AudioChannelWindow::NotifyAudioAudibleChanged( + nsPIDOMWindowOuter* aWindow, AudibleState aAudible, + AudibleChangedReasons aReason) { + RefPtr<AudioPlaybackRunnable> runnable = new AudioPlaybackRunnable( + aWindow, aAudible == AudibleState::eAudible, aReason); + DebugOnly<nsresult> rv = NS_DispatchToCurrentThread(runnable); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToCurrentThread failed"); +} + +void AudioChannelService::AudioChannelWindow::MaybeNotifyMediaBlockStart( + AudioChannelAgent* aAgent) { + nsCOMPtr<nsPIDOMWindowOuter> window = aAgent->Window(); + if (!window) { + return; + } + + nsCOMPtr<nsPIDOMWindowInner> inner = window->GetCurrentInnerWindow(); + if (!inner) { + return; + } + + nsCOMPtr<Document> doc = inner->GetExtantDoc(); + if (!doc) { + return; + } + + if (!window->ShouldDelayMediaFromStart() || !doc->Hidden()) { + return; + } + + if (!mShouldSendActiveMediaBlockStopEvent) { + mShouldSendActiveMediaBlockStopEvent = true; + NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "dom::AudioChannelService::AudioChannelWindow::" + "MaybeNotifyMediaBlockStart", + [window]() -> void { + nsCOMPtr<nsIObserverService> observerService = + services::GetObserverService(); + if (NS_WARN_IF(!observerService)) { + return; + } + + observerService->NotifyObservers(ToSupports(window), "audio-playback", + u"activeMediaBlockStart"); + })); + } +} + +} // namespace mozilla::dom |