summaryrefslogtreecommitdiffstats
path: root/dom/audiochannel
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/audiochannel
parentInitial commit. (diff)
downloadfirefox-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.cpp281
-rw-r--r--dom/audiochannel/AudioChannelAgent.h79
-rw-r--r--dom/audiochannel/AudioChannelService.cpp625
-rw-r--r--dom/audiochannel/AudioChannelService.h245
-rw-r--r--dom/audiochannel/crashtests/1223734.html17
-rw-r--r--dom/audiochannel/crashtests/1339930.html19
-rw-r--r--dom/audiochannel/crashtests/crashtests.list2
-rw-r--r--dom/audiochannel/moz.build32
-rw-r--r--dom/audiochannel/nsIAudioChannelAgent.idl121
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);
+};