diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/audiochannel | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream/115.8.0esr.tar.xz firefox-esr-upstream/115.8.0esr.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/audiochannel')
-rw-r--r-- | dom/audiochannel/AudioChannelAgent.cpp | 281 | ||||
-rw-r--r-- | dom/audiochannel/AudioChannelAgent.h | 79 | ||||
-rw-r--r-- | dom/audiochannel/AudioChannelService.cpp | 625 | ||||
-rw-r--r-- | dom/audiochannel/AudioChannelService.h | 245 | ||||
-rw-r--r-- | dom/audiochannel/crashtests/1223734.html | 17 | ||||
-rw-r--r-- | dom/audiochannel/crashtests/1339930.html | 19 | ||||
-rw-r--r-- | dom/audiochannel/crashtests/crashtests.list | 2 | ||||
-rw-r--r-- | dom/audiochannel/moz.build | 32 | ||||
-rw-r--r-- | dom/audiochannel/nsIAudioChannelAgent.idl | 121 |
9 files changed, 1421 insertions, 0 deletions
diff --git a/dom/audiochannel/AudioChannelAgent.cpp b/dom/audiochannel/AudioChannelAgent.cpp new file mode 100644 index 0000000000..286888cc78 --- /dev/null +++ b/dom/audiochannel/AudioChannelAgent.cpp @@ -0,0 +1,281 @@ +/* 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 "AudioChannelAgent.h" +#include "AudioChannelService.h" +#include "mozilla/Preferences.h" +#include "nsContentUtils.h" +#include "mozilla/dom/Document.h" +#include "nsPIDOMWindow.h" + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION_CLASS(AudioChannelAgent) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioChannelAgent) + tmp->Shutdown(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AudioChannelAgent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioChannelAgent) + NS_INTERFACE_MAP_ENTRY(nsIAudioChannelAgent) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AudioChannelAgent) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AudioChannelAgent) + +AudioChannelAgent::AudioChannelAgent() + : mInnerWindowID(0), mIsRegToService(false) { + // Init service in the begining, it can help us to know whether there is any + // created media component via AudioChannelService::IsServiceStarted(). + RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate(); +} + +AudioChannelAgent::~AudioChannelAgent() { Shutdown(); } + +void AudioChannelAgent::Shutdown() { + if (mIsRegToService) { + NotifyStoppedPlaying(); + } +} + +NS_IMETHODIMP +AudioChannelAgent::Init(mozIDOMWindow* aWindow, + nsIAudioChannelAgentCallback* aCallback) { + return InitInternal(nsPIDOMWindowInner::From(aWindow), aCallback, + /* useWeakRef = */ false); +} + +NS_IMETHODIMP +AudioChannelAgent::InitWithWeakCallback( + mozIDOMWindow* aWindow, nsIAudioChannelAgentCallback* aCallback) { + return InitInternal(nsPIDOMWindowInner::From(aWindow), aCallback, + /* useWeakRef = */ true); +} + +nsresult AudioChannelAgent::FindCorrectWindow(nsPIDOMWindowInner* aWindow) { + mWindow = aWindow->GetInProcessScriptableTop(); + if (NS_WARN_IF(!mWindow)) { + return NS_OK; + } + + // From here we do an hack for nested iframes. + // The system app doesn't have access to the nested iframe objects so it + // cannot control the volume of the agents running in nested apps. What we do + // here is to assign those Agents to the top scriptable window of the parent + // iframe (what is controlled by the system app). + // For doing this we go recursively back into the chain of windows until we + // find apps that are not the system one. + nsCOMPtr<nsPIDOMWindowOuter> outerParent = mWindow->GetInProcessParent(); + if (!outerParent || outerParent == mWindow) { + return NS_OK; + } + + nsCOMPtr<nsPIDOMWindowInner> parent = outerParent->GetCurrentInnerWindow(); + if (!parent) { + return NS_OK; + } + + nsCOMPtr<Document> doc = parent->GetExtantDoc(); + if (!doc) { + return NS_OK; + } + + if (nsContentUtils::IsChromeDoc(doc)) { + return NS_OK; + } + + return FindCorrectWindow(parent); +} + +nsresult AudioChannelAgent::InitInternal( + nsPIDOMWindowInner* aWindow, nsIAudioChannelAgentCallback* aCallback, + bool aUseWeakRef) { + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_FAILURE; + } + + mInnerWindowID = aWindow->WindowID(); + + nsresult rv = FindCorrectWindow(aWindow); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aUseWeakRef) { + mWeakCallback = do_GetWeakReference(aCallback); + } else { + mCallback = aCallback; + } + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, InitInternal, this = %p, " + "owner = %p, hasCallback = %d\n", + this, mWindow.get(), (!!mCallback || !!mWeakCallback))); + + return NS_OK; +} + +void AudioChannelAgent::PullInitialUpdate() { + RefPtr<AudioChannelService> service = AudioChannelService::Get(); + MOZ_ASSERT(service); + MOZ_ASSERT(mIsRegToService); + + AudioPlaybackConfig config = service->GetMediaConfig(mWindow); + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, PullInitialUpdate, this=%p, " + "mute=%s, volume=%f, suspend=%s, audioCapturing=%s\n", + this, config.mMuted ? "true" : "false", config.mVolume, + SuspendTypeToStr(config.mSuspend), + config.mCapturedAudio ? "true" : "false")); + WindowVolumeChanged(config.mVolume, config.mMuted); + WindowSuspendChanged(config.mSuspend); + WindowAudioCaptureChanged(InnerWindowID(), config.mCapturedAudio); +} + +NS_IMETHODIMP +AudioChannelAgent::NotifyStartedPlaying(uint8_t aAudible) { + RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate(); + if (service == nullptr || mIsRegToService) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(AudioChannelService::AudibleState::eNotAudible == 0 && + AudioChannelService::AudibleState::eMaybeAudible == 1 && + AudioChannelService::AudibleState::eAudible == 2); + service->RegisterAudioChannelAgent( + this, static_cast<AudioChannelService::AudibleState>(aAudible)); + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, NotifyStartedPlaying, this = %p, audible = %s\n", + this, + AudibleStateToStr( + static_cast<AudioChannelService::AudibleState>(aAudible)))); + + mIsRegToService = true; + return NS_OK; +} + +NS_IMETHODIMP +AudioChannelAgent::NotifyStoppedPlaying() { + if (!mIsRegToService) { + return NS_ERROR_FAILURE; + } + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, NotifyStoppedPlaying, this = %p\n", this)); + + RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate(); + if (service) { + service->UnregisterAudioChannelAgent(this); + } + + mIsRegToService = false; + return NS_OK; +} + +NS_IMETHODIMP +AudioChannelAgent::NotifyStartedAudible(uint8_t aAudible, uint32_t aReason) { + MOZ_LOG( + AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, NotifyStartedAudible, this = %p, " + "audible = %s, reason = %s\n", + this, + AudibleStateToStr( + static_cast<AudioChannelService::AudibleState>(aAudible)), + AudibleChangedReasonToStr( + static_cast<AudioChannelService::AudibleChangedReasons>(aReason)))); + + RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate(); + if (NS_WARN_IF(!service)) { + return NS_ERROR_FAILURE; + } + + service->AudioAudibleChanged( + this, static_cast<AudioChannelService::AudibleState>(aAudible), + static_cast<AudioChannelService::AudibleChangedReasons>(aReason)); + return NS_OK; +} + +already_AddRefed<nsIAudioChannelAgentCallback> +AudioChannelAgent::GetCallback() { + nsCOMPtr<nsIAudioChannelAgentCallback> callback = mCallback; + if (!callback) { + callback = do_QueryReferent(mWeakCallback); + } + return callback.forget(); +} + +void AudioChannelAgent::WindowVolumeChanged(float aVolume, bool aMuted) { + nsCOMPtr<nsIAudioChannelAgentCallback> callback = GetCallback(); + if (!callback) { + return; + } + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, WindowVolumeChanged, this = %p, mute = %s, " + "volume = %f\n", + this, aMuted ? "true" : "false", aVolume)); + callback->WindowVolumeChanged(aVolume, aMuted); +} + +void AudioChannelAgent::WindowSuspendChanged(nsSuspendedTypes aSuspend) { + nsCOMPtr<nsIAudioChannelAgentCallback> callback = GetCallback(); + if (!callback) { + return; + } + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, WindowSuspendChanged, this = %p, " + "suspended = %s\n", + this, SuspendTypeToStr(aSuspend))); + callback->WindowSuspendChanged(aSuspend); +} + +AudioPlaybackConfig AudioChannelAgent::GetMediaConfig() const { + RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate(); + AudioPlaybackConfig config(1.0, false, nsISuspendedTypes::NONE_SUSPENDED); + if (service) { + config = service->GetMediaConfig(mWindow); + } + return config; +} + +uint64_t AudioChannelAgent::WindowID() const { + return mWindow ? mWindow->WindowID() : 0; +} + +uint64_t AudioChannelAgent::InnerWindowID() const { return mInnerWindowID; } + +void AudioChannelAgent::WindowAudioCaptureChanged(uint64_t aInnerWindowID, + bool aCapture) { + if (aInnerWindowID != mInnerWindowID) { + return; + } + + nsCOMPtr<nsIAudioChannelAgentCallback> callback = GetCallback(); + if (!callback) { + return; + } + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, WindowAudioCaptureChanged, this = %p, " + "capture = %d\n", + this, aCapture)); + + callback->WindowAudioCaptureChanged(aCapture); +} + +bool AudioChannelAgent::IsWindowAudioCapturingEnabled() const { + return GetMediaConfig().mCapturedAudio; +} + +bool AudioChannelAgent::IsPlayingStarted() const { return mIsRegToService; } diff --git a/dom/audiochannel/AudioChannelAgent.h b/dom/audiochannel/AudioChannelAgent.h new file mode 100644 index 0000000000..9682219844 --- /dev/null +++ b/dom/audiochannel/AudioChannelAgent.h @@ -0,0 +1,79 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_audio_channel_agent_h__ +#define mozilla_dom_audio_channel_agent_h__ + +#include "nsIAudioChannelAgent.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" +#include "nsIWeakReferenceUtils.h" + +class nsPIDOMWindowInner; +class nsPIDOMWindowOuter; + +namespace mozilla::dom { + +class AudioPlaybackConfig; + +/* Header file */ +class AudioChannelAgent : public nsIAudioChannelAgent { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIAUDIOCHANNELAGENT + + NS_DECL_CYCLE_COLLECTION_CLASS(AudioChannelAgent) + + AudioChannelAgent(); + + // nsIAudioChannelAgentCallback MUST call this function after calling + // NotifyStartedPlaying() to require the initial update for + // volume/suspend/audio-capturing which might set before starting the agent. + // Ex. starting the agent in a tab which has been muted before, so the agent + // should apply mute state to its callback. + void PullInitialUpdate(); + + uint64_t WindowID() const; + + bool IsWindowAudioCapturingEnabled() const; + bool IsPlayingStarted() const; + + private: + virtual ~AudioChannelAgent(); + + friend class AudioChannelService; + void WindowVolumeChanged(float aVolume, bool aMuted); + void WindowSuspendChanged(nsSuspendedTypes aSuspend); + void WindowAudioCaptureChanged(uint64_t aInnerWindowID, bool aCapture); + + nsPIDOMWindowOuter* Window() const { return mWindow; } + uint64_t InnerWindowID() const; + AudioPlaybackConfig GetMediaConfig() const; + + // Returns mCallback if that's non-null, or otherwise tries to get an + // nsIAudioChannelAgentCallback out of mWeakCallback. + already_AddRefed<nsIAudioChannelAgentCallback> GetCallback(); + + nsresult InitInternal(nsPIDOMWindowInner* aWindow, + nsIAudioChannelAgentCallback* aCallback, + bool aUseWeakRef); + + void Shutdown(); + + nsresult FindCorrectWindow(nsPIDOMWindowInner* aWindow); + + nsCOMPtr<nsPIDOMWindowOuter> mWindow; + nsCOMPtr<nsIAudioChannelAgentCallback> mCallback; + + nsWeakPtr mWeakCallback; + + uint64_t mInnerWindowID; + bool mIsRegToService; +}; + +} // namespace mozilla::dom + +#endif 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 diff --git a/dom/audiochannel/AudioChannelService.h b/dom/audiochannel/AudioChannelService.h new file mode 100644 index 0000000000..bf7fa387e2 --- /dev/null +++ b/dom/audiochannel/AudioChannelService.h @@ -0,0 +1,245 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_audiochannelservice_h__ +#define mozilla_dom_audiochannelservice_h__ + +#include "nsIObserver.h" +#include "nsTObserverArray.h" +#include "nsTArray.h" + +#include "AudioChannelAgent.h" +#include "nsAttrValue.h" +#include "mozilla/Logging.h" +#include "mozilla/UniquePtr.h" + +#include <functional> + +class nsPIDOMWindowOuter; +struct PRLogModuleInfo; + +namespace mozilla::dom { + +class AudioPlaybackConfig { + public: + AudioPlaybackConfig() + : mVolume(1.0), + mMuted(false), + mSuspend(nsISuspendedTypes::NONE_SUSPENDED), + mNumberOfAgents(0) {} + + AudioPlaybackConfig(float aVolume, bool aMuted, uint32_t aSuspended) + : mVolume(aVolume), + mMuted(aMuted), + mSuspend(aSuspended), + mNumberOfAgents(0) {} + + float mVolume; + bool mMuted; + uint32_t mSuspend; + bool mCapturedAudio = false; + uint32_t mNumberOfAgents; +}; + +class AudioChannelService final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + /** + * We use `AudibleState` to represent the audible state of an owner of audio + * channel agent. Those information in AudioChannelWindow could help us to + * determine if a tab is being audible or not, in order to tell Chrome JS to + * show the sound indicator or delayed autoplay icon on the tab bar. + * + * - Sound indicator + * When a tab is playing sound, we would show the sound indicator on tab bar + * to tell users that this tab is producing sound now. In addition, the sound + * indicator also give users an ablility to mute or unmute tab. + * + * When an AudioChannelWindow first contains an agent with state `eAudible`, + * or an AudioChannelWindow losts its last agent with state `eAudible`, we + * would notify Chrome JS about those changes, to tell them that a tab has + * been being audible or not, in order to display or remove the indicator for + * a corresponding tab. + * + * - Delayed autoplay icon (Play Tab icon) + * When we enable delaying autoplay, which is to postpone the autoplay media + * for unvisited tab until it first goes to foreground, or user click the + * play tab icon to resume the delayed media. + * + * When an AudioChannelWindow first contains an agent with state `eAudible` or + * `eMaybeAudible`, we would notify Chrome JS about this change, in order to + * show the delayed autoplay tab icon to user, which is used to notice user + * there is a media being delayed starting, and then user can click the play + * tab icon to resume the start of media, or visit that tab to resume delayed + * media automatically. + * + * According to our UX design, we don't show this icon for inaudible media. + * The reason of showing the icon for a tab, where the agent starts with state + * `eMaybeAudible`, is because some video might be silent in the beginning + * but would soon become audible later. + * + * --------------------------------------------------------------------------- + * + * eNotAudible : agent is not audible + * eMaybeAudible : agent is not audible now, but it might be audible later + * eAudible : agent is audible now + */ + enum AudibleState : uint8_t { + eNotAudible = 0, + eMaybeAudible = 1, + eAudible = 2 + }; + + enum AudioCaptureState : bool { eCapturing = true, eNotCapturing = false }; + + enum AudibleChangedReasons : uint32_t { + eVolumeChanged = 0, + eDataAudibleChanged = 1, + ePauseStateChanged = 2 + }; + + /** + * Returns the AudioChannelServce singleton. + * If AudioChannelService doesn't exist, create and return new one. + * Only to be called from main thread. + */ + static already_AddRefed<AudioChannelService> GetOrCreate(); + + /** + * Returns the AudioChannelService singleton if one exists. + * If AudioChannelService doesn't exist, returns null. + */ + static already_AddRefed<AudioChannelService> Get(); + + static LogModule* GetAudioChannelLog(); + + static bool IsEnableAudioCompeting(); + + /** + * Any audio channel agent that starts playing should register itself to + * this service, sharing the AudioChannel. + */ + void RegisterAudioChannelAgent(AudioChannelAgent* aAgent, + AudibleState aAudible); + + /** + * Any audio channel agent that stops playing should unregister itself to + * this service. + */ + void UnregisterAudioChannelAgent(AudioChannelAgent* aAgent); + + /** + * Return the state to indicate this audioChannel for his window should keep + * playing/muted/suspended. + */ + AudioPlaybackConfig GetMediaConfig(nsPIDOMWindowOuter* aWindow) const; + + /** + * Called this method when the audible state of the audio playback changed, + * it would dispatch the playback event to observers which want to know the + * actual audible state of the window. + */ + void AudioAudibleChanged(AudioChannelAgent* aAgent, AudibleState aAudible, + AudibleChangedReasons aReason); + + bool IsWindowActive(nsPIDOMWindowOuter* aWindow); + + void RefreshAgentsVolume(nsPIDOMWindowOuter* aWindow, float aVolume, + bool aMuted); + + // This method needs to know the inner window that wants to capture audio. We + // group agents per top outer window, but we can have multiple innerWindow per + // top outerWindow (subiframes, etc.) and we have to identify all the agents + // just for a particular innerWindow. + void SetWindowAudioCaptured(nsPIDOMWindowOuter* aWindow, + uint64_t aInnerWindowID, bool aCapture); + + void NotifyResumingDelayedMedia(nsPIDOMWindowOuter* aWindow); + + private: + AudioChannelService(); + ~AudioChannelService(); + + void RefreshAgents(nsPIDOMWindowOuter* aWindow, + const std::function<void(AudioChannelAgent*)>& aFunc); + + void RefreshAgentsSuspend(nsPIDOMWindowOuter* aWindow, + nsSuspendedTypes aSuspend); + + static void CreateServiceIfNeeded(); + + /** + * Shutdown the singleton. + */ + static void Shutdown(); + + void RefreshAgentsAudioFocusChanged(AudioChannelAgent* aAgent); + + class AudioChannelWindow final { + public: + explicit AudioChannelWindow(uint64_t aWindowID) + : mWindowID(aWindowID), + mIsAudioCaptured(false), + mShouldSendActiveMediaBlockStopEvent(false) {} + + void AudioAudibleChanged(AudioChannelAgent* aAgent, AudibleState aAudible, + AudibleChangedReasons aReason); + + void AppendAgent(AudioChannelAgent* aAgent, AudibleState aAudible); + void RemoveAgent(AudioChannelAgent* aAgent); + + void NotifyMediaBlockStop(nsPIDOMWindowOuter* aWindow); + + uint64_t mWindowID; + bool mIsAudioCaptured; + AudioPlaybackConfig mConfig; + + // Raw pointer because the AudioChannelAgent must unregister itself. + nsTObserverArray<AudioChannelAgent*> mAgents; + nsTObserverArray<AudioChannelAgent*> mAudibleAgents; + + // If we've dispatched "activeMediaBlockStart" event, we must dispatch + // another event "activeMediablockStop" when the window is resumed from + // suspend-block. + bool mShouldSendActiveMediaBlockStopEvent; + + private: + void AppendAudibleAgentIfNotContained(AudioChannelAgent* aAgent, + AudibleChangedReasons aReason); + void RemoveAudibleAgentIfContained(AudioChannelAgent* aAgent, + AudibleChangedReasons aReason); + + void AppendAgentAndIncreaseAgentsNum(AudioChannelAgent* aAgent); + void RemoveAgentAndReduceAgentsNum(AudioChannelAgent* aAgent); + + bool IsFirstAudibleAgent() const; + bool IsLastAudibleAgent() const; + + void NotifyAudioAudibleChanged(nsPIDOMWindowOuter* aWindow, + AudibleState aAudible, + AudibleChangedReasons aReason); + + void MaybeNotifyMediaBlockStart(AudioChannelAgent* aAgent); + }; + + AudioChannelWindow* GetOrCreateWindowData(nsPIDOMWindowOuter* aWindow); + + AudioChannelWindow* GetWindowData(uint64_t aWindowID) const; + + nsTObserverArray<UniquePtr<AudioChannelWindow>> mWindows; +}; + +const char* SuspendTypeToStr(const nsSuspendedTypes& aSuspend); +const char* AudibleStateToStr( + const AudioChannelService::AudibleState& aAudible); +const char* AudibleChangedReasonToStr( + const AudioChannelService::AudibleChangedReasons& aReason); + +} // namespace mozilla::dom + +#endif diff --git a/dom/audiochannel/crashtests/1223734.html b/dom/audiochannel/crashtests/1223734.html new file mode 100644 index 0000000000..1d001eccc4 --- /dev/null +++ b/dom/audiochannel/crashtests/1223734.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> + +function boom() { + var audio = document.createElement('audio'); + audio.loop = true; + audio.play(); + document.implementation.createDocument("", "", null).adoptNode(audio); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/dom/audiochannel/crashtests/1339930.html b/dom/audiochannel/crashtests/1339930.html new file mode 100644 index 0000000000..f817c85975 --- /dev/null +++ b/dom/audiochannel/crashtests/1339930.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> +window.onload = function(){ + let x = document.getElementsByTagName("iframe")[0]; + let w = window.open(); + let o = window.frames[0]; + x.remove(); + o.requestIdleCallback(function(){}); + w.close(); +}; +</script> +</head> +<body> +<iframe></iframe> +</body> +</html> diff --git a/dom/audiochannel/crashtests/crashtests.list b/dom/audiochannel/crashtests/crashtests.list new file mode 100644 index 0000000000..3af64ac466 --- /dev/null +++ b/dom/audiochannel/crashtests/crashtests.list @@ -0,0 +1,2 @@ +load 1223734.html
+load 1339930.html
diff --git a/dom/audiochannel/moz.build b/dom/audiochannel/moz.build new file mode 100644 index 0000000000..d006368e7f --- /dev/null +++ b/dom/audiochannel/moz.build @@ -0,0 +1,32 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Audio/Video: Playback") + +XPIDL_SOURCES += [ + "nsIAudioChannelAgent.idl", +] + +XPIDL_MODULE = "dom_audiochannel" + +EXPORTS += [ + "AudioChannelAgent.h", + "AudioChannelService.h", +] + +UNIFIED_SOURCES += [ + "AudioChannelAgent.cpp", + "AudioChannelService.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base/", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/audiochannel/nsIAudioChannelAgent.idl b/dom/audiochannel/nsIAudioChannelAgent.idl new file mode 100644 index 0000000000..69a8344449 --- /dev/null +++ b/dom/audiochannel/nsIAudioChannelAgent.idl @@ -0,0 +1,121 @@ +/* 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 "nsISupports.idl" + +interface mozIDOMWindow; + +typedef uint32_t nsSuspendedTypes; + +[scriptable, builtinclass, uuid(2822a840-f009-11e5-a837-0800200c9a66)] +interface nsISuspendedTypes : nsISupports +{ + /** + * The suspended enum is used for delaying autoplay video in non-visited tab + * + * Note: the "remote side" must control the AudioChannelAgent using + * nsIAudioChannelAgentCallback.windowSuspendChanged() callback instead using + * play/pause methods or any button in the webpage. + * + * - SUSPENDED_BLOCK + * It's used to prevent auto-playing media in inactive page in order to + * reduce the power consumption, and the media can't be resumed until the + * page becomes active again. It would change the internal state of + * MediaElement when it's being blocked/resumed, so it won't trigger the + * related JS event. eg. "play" and "pause" event. + */ + + const uint32_t NONE_SUSPENDED = 0; + const uint32_t SUSPENDED_BLOCK = 1; +}; + +[uuid(15c05894-408e-4798-b527-a8c32d9c5f8c)] +interface nsIAudioChannelAgentCallback : nsISupports +{ + /** + * Notified when the window volume/mute is changed + */ + void windowVolumeChanged(in float aVolume, in bool aMuted); + + /** + * Notified when the window needs to be suspended or resumed. + */ + void windowSuspendChanged(in uint32_t aSuspend); + + /** + * Notified when the capture state is changed. + */ + void windowAudioCaptureChanged(in bool aCapture); +}; + +/** + * This interface provides an agent for gecko components to participate + * in the audio channel service. Gecko components are responsible for + * 1. Notifying the agent when they start/stop using this channel. + * 2. Notifying the agent when they are audible. + * + * The agent will invoke a callback to notify Gecko components of + * 1. Changes to the playable status of this channel. + */ + +[uuid(4d212770-5d7b-446f-9394-632e351d96ee)] +interface nsIAudioChannelAgent : nsISupports +{ + const long AUDIO_AGENT_STATE_NORMAL = 0; + const long AUDIO_AGENT_STATE_MUTED = 1; + const long AUDIO_AGENT_STATE_FADED = 2; + + /** + * Initialize the agent with a channel type. + * Note: This function should only be called once. + * + * @param window + * The window + * @param callback + * 1. Once the playable status changes, agent uses this callback function + * to notify Gecko component. + * 2. The callback is allowed to be null. Ex: telephony doesn't need to + * listen change of the playable status. + * 3. The AudioChannelAgent keeps a strong reference to the callback + * object. + */ + void init(in mozIDOMWindow window, in nsIAudioChannelAgentCallback callback); + + /** + * This method is just like init(), except the audio channel agent keeps a + * weak reference to the callback object. + * + * In order for this to work, |callback| must implement + * nsISupportsWeakReference. + */ + void initWithWeakCallback(in mozIDOMWindow window, + in nsIAudioChannelAgentCallback callback); + + /** + * Notify the agent that we want to start playing. + * Note: Gecko component SHOULD call this function first then start to + * play audio stream only when return value is true. + */ + void notifyStartedPlaying(in uint8_t audible); + + /** + * Notify the agent we no longer want to play. + * + * Note : even if notifyStartedPlaying() returned false, the agent would + * still be registered with the audio channel service and receive callbacks + * for status changes. So notifyStoppedPlaying must still eventually be + * called to unregister the agent with the channel service. + */ + void notifyStoppedPlaying(); + + + /** + * Notify agent that we already start producing audible data. + * + * Note : sometime audio might become silent during playing, this method is used to + * notify the actually audible state to other services which want to know + * about that, ex. tab sound indicator. + */ + void notifyStartedAudible(in uint8_t audible, in uint32_t reason); +}; |