diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /dom/audiochannel | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | uxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz |
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/audiochannel')
-rw-r--r-- | dom/audiochannel/AudioChannelAgent.cpp | 370 | ||||
-rw-r--r-- | dom/audiochannel/AudioChannelAgent.h | 86 | ||||
-rw-r--r-- | dom/audiochannel/AudioChannelService.cpp | 1437 | ||||
-rw-r--r-- | dom/audiochannel/AudioChannelService.h | 376 | ||||
-rw-r--r-- | dom/audiochannel/crashtests/1223734.html | 17 | ||||
-rw-r--r-- | dom/audiochannel/crashtests/crashtests.list | 1 | ||||
-rw-r--r-- | dom/audiochannel/moz.build | 30 | ||||
-rw-r--r-- | dom/audiochannel/nsIAudioChannelAgent.idl | 187 | ||||
-rw-r--r-- | dom/audiochannel/nsIAudioChannelService.idl | 29 |
9 files changed, 2533 insertions, 0 deletions
diff --git a/dom/audiochannel/AudioChannelAgent.cpp b/dom/audiochannel/AudioChannelAgent.cpp new file mode 100644 index 0000000000..700ecc3781 --- /dev/null +++ b/dom/audiochannel/AudioChannelAgent.cpp @@ -0,0 +1,370 @@ +/* -*- 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 "AudioChannelAgent.h" +#include "AudioChannelService.h" +#include "mozilla/Preferences.h" +#include "nsContentUtils.h" +#include "nsIDocument.h" +#include "nsIDOMWindow.h" +#include "nsPIDOMWindow.h" +#include "nsIURI.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() + : mAudioChannelType(AUDIO_AGENT_CHANNEL_ERROR) + , mInnerWindowID(0) + , mIsRegToService(false) +{ +} + +AudioChannelAgent::~AudioChannelAgent() +{ + Shutdown(); +} + +void +AudioChannelAgent::Shutdown() +{ + if (mIsRegToService) { + NotifyStoppedPlaying(); + } +} + +NS_IMETHODIMP AudioChannelAgent::GetAudioChannelType(int32_t *aAudioChannelType) +{ + *aAudioChannelType = mAudioChannelType; + return NS_OK; +} + +NS_IMETHODIMP +AudioChannelAgent::Init(mozIDOMWindow* aWindow, int32_t aChannelType, + nsIAudioChannelAgentCallback *aCallback) +{ + return InitInternal(nsPIDOMWindowInner::From(aWindow), aChannelType, + aCallback, /* useWeakRef = */ false); +} + +NS_IMETHODIMP +AudioChannelAgent::InitWithWeakCallback(mozIDOMWindow* aWindow, + int32_t aChannelType, + nsIAudioChannelAgentCallback *aCallback) +{ + return InitInternal(nsPIDOMWindowInner::From(aWindow), aChannelType, + aCallback, /* useWeakRef = */ true); +} + +nsresult +AudioChannelAgent::FindCorrectWindow(nsPIDOMWindowInner* aWindow) +{ + MOZ_ASSERT(aWindow->IsInnerWindow()); + + mWindow = aWindow->GetScriptableTop(); + 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->GetParent(); + if (!outerParent || outerParent == mWindow) { + return NS_OK; + } + + nsCOMPtr<nsPIDOMWindowInner> parent = outerParent->GetCurrentInnerWindow(); + if (!parent) { + return NS_OK; + } + + nsCOMPtr<nsIDocument> doc = parent->GetExtantDoc(); + if (!doc) { + return NS_OK; + } + + if (nsContentUtils::IsChromeDoc(doc)) { + return NS_OK; + } + + nsAdoptingCString systemAppUrl = + mozilla::Preferences::GetCString("b2g.system_startup_url"); + if (!systemAppUrl) { + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + nsCOMPtr<nsIURI> uri; + principal->GetURI(getter_AddRefs(uri)); + + if (uri) { + nsAutoCString spec; + uri->GetSpec(spec); + + if (spec.Equals(systemAppUrl)) { + return NS_OK; + } + } + + return FindCorrectWindow(parent); +} + +nsresult +AudioChannelAgent::InitInternal(nsPIDOMWindowInner* aWindow, + int32_t aChannelType, + nsIAudioChannelAgentCallback *aCallback, + bool aUseWeakRef) +{ + // We syncd the enum of channel type between nsIAudioChannelAgent.idl and + // AudioChannelBinding.h the same. + MOZ_ASSERT(int(AUDIO_AGENT_CHANNEL_NORMAL) == int(AudioChannel::Normal) && + int(AUDIO_AGENT_CHANNEL_CONTENT) == int(AudioChannel::Content) && + int(AUDIO_AGENT_CHANNEL_NOTIFICATION) == int(AudioChannel::Notification) && + int(AUDIO_AGENT_CHANNEL_ALARM) == int(AudioChannel::Alarm) && + int(AUDIO_AGENT_CHANNEL_TELEPHONY) == int(AudioChannel::Telephony) && + int(AUDIO_AGENT_CHANNEL_RINGER) == int(AudioChannel::Ringer) && + int(AUDIO_AGENT_CHANNEL_SYSTEM) == int(AudioChannel::System) && + int(AUDIO_AGENT_CHANNEL_PUBLICNOTIFICATION) == int(AudioChannel::Publicnotification), + "Enum of channel on nsIAudioChannelAgent.idl should be the same with AudioChannelBinding.h"); + + if (mAudioChannelType != AUDIO_AGENT_CHANNEL_ERROR || + aChannelType > AUDIO_AGENT_CHANNEL_SYSTEM || + aChannelType < AUDIO_AGENT_CHANNEL_NORMAL) { + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(aWindow->IsInnerWindow()); + mInnerWindowID = aWindow->WindowID(); + + nsresult rv = FindCorrectWindow(aWindow); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mAudioChannelType = aChannelType; + + if (aUseWeakRef) { + mWeakCallback = do_GetWeakReference(aCallback); + } else { + mCallback = aCallback; + } + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, InitInternal, this = %p, type = %d, " + "owner = %p, hasCallback = %d\n", this, mAudioChannelType, + mWindow.get(), (!!mCallback || !!mWeakCallback))); + + return NS_OK; +} + +NS_IMETHODIMP +AudioChannelAgent::NotifyStartedPlaying(AudioPlaybackConfig* aConfig, + uint8_t aAudible) +{ + if (NS_WARN_IF(!aConfig)) { + return NS_ERROR_FAILURE; + } + + RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate(); + if (mAudioChannelType == AUDIO_AGENT_CHANNEL_ERROR || + 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)); + + AudioPlaybackConfig config = service->GetMediaConfig(mWindow, + mAudioChannelType); + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, NotifyStartedPlaying, this = %p, " + "audible = %d, mute = %d, volume = %f, suspend = %d\n", this, + aAudible, config.mMuted, config.mVolume, config.mSuspend)); + + aConfig->SetConfig(config.mVolume, config.mMuted, config.mSuspend); + mIsRegToService = true; + return NS_OK; +} + +NS_IMETHODIMP +AudioChannelAgent::NotifyStoppedPlaying() +{ + if (mAudioChannelType == AUDIO_AGENT_CHANNEL_ERROR || + !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 = %d, reason = %d\n", this, aAudible, 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() +{ + nsCOMPtr<nsIAudioChannelAgentCallback> callback = GetCallback(); + if (!callback) { + return; + } + + AudioPlaybackConfig config = GetMediaConfig(); + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, WindowVolumeChanged, this = %p, mute = %d, " + "volume = %f\n", this, config.mMuted, config.mVolume)); + + callback->WindowVolumeChanged(config.mVolume, config.mMuted); +} + +void +AudioChannelAgent::WindowSuspendChanged(nsSuspendedTypes aSuspend) +{ + nsCOMPtr<nsIAudioChannelAgentCallback> callback = GetCallback(); + if (!callback) { + return; + } + + if (!IsDisposableSuspend(aSuspend)) { + aSuspend = GetMediaConfig().mSuspend; + } + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelAgent, WindowSuspendChanged, this = %p, " + "suspended = %d\n", this, aSuspend)); + + callback->WindowSuspendChanged(aSuspend); +} + +AudioPlaybackConfig +AudioChannelAgent::GetMediaConfig() +{ + RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate(); + AudioPlaybackConfig config(1.0, false, nsISuspendedTypes::NONE_SUSPENDED); + if (service) { + config = service->GetMediaConfig(mWindow, mAudioChannelType); + } + return config; +} + +bool +AudioChannelAgent::IsDisposableSuspend(nsSuspendedTypes aSuspend) const +{ + return (aSuspend == nsISuspendedTypes::SUSPENDED_PAUSE_DISPOSABLE || + aSuspend == nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE); +} + +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::IsPlayingStarted() const +{ + return mIsRegToService; +} + +bool +AudioChannelAgent::ShouldBlockMedia() const +{ + return mWindow ? + mWindow->GetMediaSuspend() == nsISuspendedTypes::SUSPENDED_BLOCK : false; +} diff --git a/dom/audiochannel/AudioChannelAgent.h b/dom/audiochannel/AudioChannelAgent.h new file mode 100644 index 0000000000..f7e776d858 --- /dev/null +++ b/dom/audiochannel/AudioChannelAgent.h @@ -0,0 +1,86 @@ +/* -*- 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 "nsWeakPtr.h" + +#define NS_AUDIOCHANNELAGENT_CONTRACTID "@mozilla.org/audiochannelagent;1" +// f27688e2-3dd7-11e2-904e-10bf48d64bd4 +#define NS_AUDIOCHANNELAGENT_CID {0xf27688e2, 0x3dd7, 0x11e2, \ + {0x90, 0x4e, 0x10, 0xbf, 0x48, 0xd6, 0x4b, 0xd4}} + +class nsPIDOMWindowInner; +class nsPIDOMWindowOuter; + +namespace mozilla { +namespace 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(); + + void WindowVolumeChanged(); + void WindowSuspendChanged(nsSuspendedTypes aSuspend); + void WindowAudioCaptureChanged(uint64_t aInnerWindowID, bool aCapture); + + nsPIDOMWindowOuter* Window() const + { + return mWindow; + } + + uint64_t WindowID() const; + uint64_t InnerWindowID() const; + + bool IsPlayingStarted() const; + bool ShouldBlockMedia() const; + +private: + virtual ~AudioChannelAgent(); + + AudioPlaybackConfig GetMediaConfig(); + bool IsDisposableSuspend(nsSuspendedTypes aSuspend) 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, int32_t aAudioAgentType, + nsIAudioChannelAgentCallback* aCallback, + bool aUseWeakRef); + + void Shutdown(); + + nsresult FindCorrectWindow(nsPIDOMWindowInner* aWindow); + + nsCOMPtr<nsPIDOMWindowOuter> mWindow; + nsCOMPtr<nsIAudioChannelAgentCallback> mCallback; + + nsWeakPtr mWeakCallback; + + int32_t mAudioChannelType; + uint64_t mInnerWindowID; + bool mIsRegToService; +}; + +} // namespace dom +} // namespace mozilla + + +#endif diff --git a/dom/audiochannel/AudioChannelService.cpp b/dom/audiochannel/AudioChannelService.cpp new file mode 100644 index 0000000000..87cde41e9d --- /dev/null +++ b/dom/audiochannel/AudioChannelService.cpp @@ -0,0 +1,1437 @@ +/* -*- 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/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/TabParent.h" + +#include "nsContentUtils.h" +#include "nsIScriptSecurityManager.h" +#include "nsISupportsPrimitives.h" +#include "nsThreadUtils.h" +#include "nsHashPropertyBag.h" +#include "nsComponentManagerUtils.h" +#include "nsGlobalWindow.h" +#include "nsPIDOMWindow.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" + +#ifdef MOZ_WIDGET_GONK +#include "nsJSUtils.h" +#include "SpeakerManagerService.h" +#endif + +#include "mozilla/Preferences.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::hal; + +namespace { + +// If true, any new AudioChannelAgent will be muted when created. +bool sAudioChannelMutedByDefault = false; +bool sAudioChannelCompeting = false; +bool sAudioChannelCompetingAllAgents = false; +bool sXPCOMShuttingDown = false; + +class NotifyChannelActiveRunnable final : public Runnable +{ +public: + NotifyChannelActiveRunnable(uint64_t aWindowID, AudioChannel aAudioChannel, + bool aActive) + : mWindowID(aWindowID) + , mAudioChannel(aAudioChannel) + , mActive(aActive) + {} + + NS_IMETHOD Run() override + { + nsCOMPtr<nsIObserverService> observerService = + services::GetObserverService(); + if (NS_WARN_IF(!observerService)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsISupportsPRUint64> wrapper = + do_CreateInstance(NS_SUPPORTS_PRUINT64_CONTRACTID); + if (NS_WARN_IF(!wrapper)) { + return NS_ERROR_FAILURE; + } + + wrapper->SetData(mWindowID); + + nsAutoString name; + AudioChannelService::GetAudioChannelString(mAudioChannel, name); + + nsAutoCString topic; + topic.Assign("audiochannel-activity-"); + topic.Append(NS_ConvertUTF16toUTF8(name)); + + observerService->NotifyObservers(wrapper, topic.get(), + mActive + ? u"active" + : u"inactive"); + + // TODO : remove b2g related event in bug1299390. + observerService->NotifyObservers(wrapper, + "media-playback", + mActive + ? u"active" + : u"inactive"); + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("NotifyChannelActiveRunnable, type = %d, active = %d\n", + mAudioChannel, mActive)); + + return NS_OK; + } + +private: + const uint64_t mWindowID; + const AudioChannel mAudioChannel; + const bool mActive; +}; + +bool +IsParentProcess() +{ + return XRE_GetProcessType() == GeckoProcessType_Default; +} + +class AudioPlaybackRunnable final : public Runnable +{ +public: + AudioPlaybackRunnable(nsPIDOMWindowOuter* aWindow, bool aActive, + AudioChannelService::AudibleChangedReasons aReason) + : 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 = %d, reason = %d\n", + mActive, mReason)); + + return NS_OK; + } + +private: + void GetActiveState(nsAString& astate) + { + if (mActive) { + CopyASCIItoUTF16("active", astate); + } else { + if(mReason == AudioChannelService::AudibleChangedReasons::ePauseStateChanged) { + CopyASCIItoUTF16("inactive-pause", astate); + } else { + CopyASCIItoUTF16("inactive-nonaudible", astate); + } + } + } + + nsCOMPtr<nsPIDOMWindowOuter> mWindow; + bool mActive; + AudioChannelService::AudibleChangedReasons mReason; +}; + +bool +IsEnableAudioCompetingForAllAgents() +{ + // In general, the audio competing should only be for audible media and it + // helps user can focus on one media at the same time. However, we hope to + // treat all media as the same in the mobile device. First reason is we have + // media control on fennec and we just want to control one media at once time. + // Second reason is to reduce the bandwidth, avoiding to play any non-audible + // media in background which user doesn't notice about. +#ifdef MOZ_WIDGET_ANDROID + return true; +#else + return sAudioChannelCompetingAllAgents; +#endif +} + +} // anonymous namespace + +StaticRefPtr<AudioChannelService> gAudioChannelService; + +// Mappings from 'mozaudiochannel' attribute strings to an enumeration. +static const nsAttrValue::EnumTable kMozAudioChannelAttributeTable[] = { + { "normal", (int16_t)AudioChannel::Normal }, + { "content", (int16_t)AudioChannel::Content }, + { "notification", (int16_t)AudioChannel::Notification }, + { "alarm", (int16_t)AudioChannel::Alarm }, + { "telephony", (int16_t)AudioChannel::Telephony }, + { "ringer", (int16_t)AudioChannel::Ringer }, + { "publicnotification", (int16_t)AudioChannel::Publicnotification }, + { "system", (int16_t)AudioChannel::System }, + { nullptr, 0 } +}; + +/* 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 */ PRLogModuleInfo* +AudioChannelService::GetAudioChannelLog() +{ + static PRLogModuleInfo *gAudioChannelLog; + if (!gAudioChannelLog) { + gAudioChannelLog = PR_NewLogModule("AudioChannel"); + } + 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"); + + if (IsParentProcess()) { + obs->RemoveObserver(gAudioChannelService, "ipc:content-shutdown"); + +#ifdef MOZ_WIDGET_GONK + // To monitor the volume settings based on audio channel. + obs->RemoveObserver(gAudioChannelService, "mozsettings-changed"); +#endif + } + } + + gAudioChannelService->mWindows.Clear(); + gAudioChannelService->mPlayingChildren.Clear(); + gAudioChannelService->mTabParents.Clear(); +#ifdef MOZ_WIDGET_GONK + gAudioChannelService->mSpeakerManager.Clear(); +#endif + + gAudioChannelService = nullptr; + } +} + +/* static */ bool +AudioChannelService::IsEnableAudioCompeting() +{ + CreateServiceIfNeeded(); + return sAudioChannelCompeting; +} + +NS_INTERFACE_MAP_BEGIN(AudioChannelService) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAudioChannelService) + NS_INTERFACE_MAP_ENTRY(nsIAudioChannelService) + NS_INTERFACE_MAP_ENTRY(nsIObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(AudioChannelService) +NS_IMPL_RELEASE(AudioChannelService) + +AudioChannelService::AudioChannelService() + : mDefChannelChildID(CONTENT_PROCESS_ID_UNKNOWN) + , mTelephonyChannel(false) + , mContentOrNormalChannel(false) + , mAnyChannel(false) +{ + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->AddObserver(this, "xpcom-shutdown", false); + obs->AddObserver(this, "outer-window-destroyed", false); + if (IsParentProcess()) { + obs->AddObserver(this, "ipc:content-shutdown", false); + +#ifdef MOZ_WIDGET_GONK + // To monitor the volume settings based on audio channel. + obs->AddObserver(this, "mozsettings-changed", false); +#endif + } + } + + Preferences::AddBoolVarCache(&sAudioChannelMutedByDefault, + "dom.audiochannel.mutedByDefault"); + Preferences::AddBoolVarCache(&sAudioChannelCompeting, + "dom.audiochannel.audioCompeting"); + Preferences::AddBoolVarCache(&sAudioChannelCompetingAllAgents, + "dom.audiochannel.audioCompeting.allAgents"); +} + +AudioChannelService::~AudioChannelService() +{ +} + +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(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); + + MaybeSendStatusUpdate(); +} + +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); + +#ifdef MOZ_WIDGET_GONK + bool active = AnyAudioChannelIsActive(); + for (uint32_t i = 0; i < mSpeakerManager.Length(); i++) { + mSpeakerManager[i]->SetAudioChannelActive(active); + } +#endif + + MaybeSendStatusUpdate(); +} + +void +AudioChannelService::RegisterTabParent(TabParent* aTabParent) +{ + MOZ_ASSERT(aTabParent); + MOZ_ASSERT(!mTabParents.Contains(aTabParent)); + mTabParents.AppendElement(aTabParent); +} + +void +AudioChannelService::UnregisterTabParent(TabParent* aTabParent) +{ + MOZ_ASSERT(aTabParent); + mTabParents.RemoveElement(aTabParent); +} + +AudioPlaybackConfig +AudioChannelService::GetMediaConfig(nsPIDOMWindowOuter* aWindow, + uint32_t aAudioChannel) const +{ + MOZ_ASSERT(!aWindow || aWindow->IsOuterWindow()); + MOZ_ASSERT(aAudioChannel < NUMBER_OF_AUDIO_CHANNELS); + + AudioPlaybackConfig config(1.0, false, + nsISuspendedTypes::NONE_SUSPENDED); + + if (!aWindow || !aWindow->IsOuterWindow()) { + config.SetConfig(0.0, true, + 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->mChannels[aAudioChannel].mVolume; + config.mMuted = config.mMuted || winData->mChannels[aAudioChannel].mMuted; + config.mSuspend = winData->mOwningAudioFocus ? + config.mSuspend : nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE; + } + + config.mVolume *= window->GetAudioVolume(); + config.mMuted = config.mMuted || window->GetAudioMuted(); + if (window->GetMediaSuspend() != nsISuspendedTypes::NONE_SUSPENDED) { + config.mSuspend = window->GetMediaSuspend(); + } + + nsCOMPtr<nsPIDOMWindowOuter> win = window->GetScriptableParentOrNull(); + if (!win) { + break; + } + + window = do_QueryInterface(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); + } +} + +bool +AudioChannelService::TelephonyChannelIsActive() +{ + nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator windowsIter(mWindows); + while (windowsIter.HasMore()) { + AudioChannelWindow* next = windowsIter.GetNext(); + if (next->mChannels[(uint32_t)AudioChannel::Telephony].mNumberOfAgents != 0 && + !next->mChannels[(uint32_t)AudioChannel::Telephony].mMuted) { + return true; + } + } + + if (IsParentProcess()) { + nsTObserverArray<nsAutoPtr<AudioChannelChildStatus>>::ForwardIterator + childrenIter(mPlayingChildren); + while (childrenIter.HasMore()) { + AudioChannelChildStatus* child = childrenIter.GetNext(); + if (child->mActiveTelephonyChannel) { + return true; + } + } + } + + return false; +} + +bool +AudioChannelService::ContentOrNormalChannelIsActive() +{ + // This method is meant to be used just by the child to send status update. + MOZ_ASSERT(!IsParentProcess()); + + nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator iter(mWindows); + while (iter.HasMore()) { + AudioChannelWindow* next = iter.GetNext(); + if (next->mChannels[(uint32_t)AudioChannel::Content].mNumberOfAgents > 0 || + next->mChannels[(uint32_t)AudioChannel::Normal].mNumberOfAgents > 0) { + return true; + } + } + return false; +} + +AudioChannelService::AudioChannelChildStatus* +AudioChannelService::GetChildStatus(uint64_t aChildID) const +{ + nsTObserverArray<nsAutoPtr<AudioChannelChildStatus>>::ForwardIterator + iter(mPlayingChildren); + while (iter.HasMore()) { + AudioChannelChildStatus* child = iter.GetNext(); + if (child->mChildID == aChildID) { + return child; + } + } + + return nullptr; +} + +void +AudioChannelService::RemoveChildStatus(uint64_t aChildID) +{ + nsTObserverArray<nsAutoPtr<AudioChannelChildStatus>>::ForwardIterator + iter(mPlayingChildren); + while (iter.HasMore()) { + nsAutoPtr<AudioChannelChildStatus>& child = iter.GetNext(); + if (child->mChildID == aChildID) { + mPlayingChildren.RemoveElement(child); + break; + } + } +} + +bool +AudioChannelService::ProcessContentOrNormalChannelIsActive(uint64_t aChildID) +{ + AudioChannelChildStatus* child = GetChildStatus(aChildID); + if (!child) { + return false; + } + + return child->mActiveContentOrNormalChannel; +} + +bool +AudioChannelService::AnyAudioChannelIsActive() +{ + nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator iter(mWindows); + while (iter.HasMore()) { + AudioChannelWindow* next = iter.GetNext(); + for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) { + if (next->mChannels[kMozAudioChannelAttributeTable[i].value].mNumberOfAgents + != 0) { + return true; + } + } + } + + if (IsParentProcess()) { + return !mPlayingChildren.IsEmpty(); + } + + return false; +} + +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; + } + + nsAutoPtr<AudioChannelWindow> winData; + { + nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator + iter(mWindows); + while (iter.HasMore()) { + nsAutoPtr<AudioChannelWindow>& next = iter.GetNext(); + if (next->mWindowID == outerID) { + uint32_t pos = mWindows.IndexOf(next); + winData = next.forget(); + mWindows.RemoveElementAt(pos); + break; + } + } + } + + if (winData) { + nsTObserverArray<AudioChannelAgent*>::ForwardIterator + iter(winData->mAgents); + while (iter.HasMore()) { + iter.GetNext()->WindowVolumeChanged(); + } + } + +#ifdef MOZ_WIDGET_GONK + bool active = AnyAudioChannelIsActive(); + for (uint32_t i = 0; i < mSpeakerManager.Length(); i++) { + mSpeakerManager[i]->SetAudioChannelActive(active); + } +#endif + } else if (!strcmp(aTopic, "ipc:content-shutdown")) { + nsCOMPtr<nsIPropertyBag2> props = do_QueryInterface(aSubject); + if (!props) { + NS_WARNING("ipc:content-shutdown message without property bag as subject"); + return NS_OK; + } + + uint64_t childID = 0; + nsresult rv = props->GetPropertyAsUint64(NS_LITERAL_STRING("childID"), + &childID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mDefChannelChildID == childID) { + SetDefaultVolumeControlChannelInternal(-1, false, childID); + mDefChannelChildID = CONTENT_PROCESS_ID_UNKNOWN; + } + + RemoveChildStatus(childID); + } + + return NS_OK; +} + +void +AudioChannelService::RefreshAgentsVolumeAndPropagate(AudioChannel aAudioChannel, + nsPIDOMWindowOuter* aWindow) +{ + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsOuterWindow()); + + nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetScriptableTop(); + if (!topWindow) { + return; + } + + AudioChannelWindow* winData = GetWindowData(topWindow->WindowID()); + if (!winData) { + return; + } + + for (uint32_t i = 0; i < mTabParents.Length(); ++i) { + mTabParents[i]->AudioChannelChangeNotification(aWindow, aAudioChannel, + winData->mChannels[(uint32_t)aAudioChannel].mVolume, + winData->mChannels[(uint32_t)aAudioChannel].mMuted); + } + + RefreshAgentsVolume(aWindow); +} + +void +AudioChannelService::RefreshAgents(nsPIDOMWindowOuter* aWindow, + mozilla::function<void(AudioChannelAgent*)> aFunc) +{ + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsOuterWindow()); + + nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetScriptableTop(); + if (!topWindow) { + return; + } + + AudioChannelWindow* winData = GetWindowData(topWindow->WindowID()); + if (!winData) { + return; + } + + nsTObserverArray<AudioChannelAgent*>::ForwardIterator + iter(winData->mAgents); + while (iter.HasMore()) { + aFunc(iter.GetNext()); + } +} + +void +AudioChannelService::RefreshAgentsVolume(nsPIDOMWindowOuter* aWindow) +{ + RefreshAgents(aWindow, [] (AudioChannelAgent* agent) { + agent->WindowVolumeChanged(); + }); +} + +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_ASSERT(aWindow->IsOuterWindow()); + + MOZ_LOG(GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelService, SetWindowAudioCaptured, window = %p, " + "aCapture = %d\n", aWindow, aCapture)); + + nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetScriptableTop(); + 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; + nsTObserverArray<AudioChannelAgent*>::ForwardIterator + iter(winData->mAgents); + while (iter.HasMore()) { + iter.GetNext()->WindowAudioCaptureChanged(aInnerWindowID, aCapture); + } + } +} + +/* static */ const nsAttrValue::EnumTable* +AudioChannelService::GetAudioChannelTable() +{ + return kMozAudioChannelAttributeTable; +} + +/* static */ AudioChannel +AudioChannelService::GetAudioChannel(const nsAString& aChannel) +{ + for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) { + if (aChannel.EqualsASCII(kMozAudioChannelAttributeTable[i].tag)) { + return static_cast<AudioChannel>(kMozAudioChannelAttributeTable[i].value); + } + } + + return AudioChannel::Normal; +} + +/* static */ AudioChannel +AudioChannelService::GetDefaultAudioChannel() +{ + nsAutoString audioChannel(Preferences::GetString("media.defaultAudioChannel")); + if (audioChannel.IsEmpty()) { + return AudioChannel::Normal; + } + + for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) { + if (audioChannel.EqualsASCII(kMozAudioChannelAttributeTable[i].tag)) { + return static_cast<AudioChannel>(kMozAudioChannelAttributeTable[i].value); + } + } + + return AudioChannel::Normal; +} + +/* static */ void +AudioChannelService::GetAudioChannelString(AudioChannel aChannel, + nsAString& aString) +{ + aString.AssignASCII("normal"); + + for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) { + if (aChannel == + static_cast<AudioChannel>(kMozAudioChannelAttributeTable[i].value)) { + aString.AssignASCII(kMozAudioChannelAttributeTable[i].tag); + break; + } + } +} + +/* static */ void +AudioChannelService::GetDefaultAudioChannelString(nsAString& aString) +{ + aString.AssignASCII("normal"); + + nsAutoString audioChannel(Preferences::GetString("media.defaultAudioChannel")); + if (!audioChannel.IsEmpty()) { + for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) { + if (audioChannel.EqualsASCII(kMozAudioChannelAttributeTable[i].tag)) { + aString = audioChannel; + break; + } + } + } +} + +AudioChannelService::AudioChannelWindow* +AudioChannelService::GetOrCreateWindowData(nsPIDOMWindowOuter* aWindow) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsOuterWindow()); + + AudioChannelWindow* winData = GetWindowData(aWindow->WindowID()); + if (!winData) { + winData = new AudioChannelWindow(aWindow->WindowID()); + mWindows.AppendElement(winData); + } + + return winData; +} + +AudioChannelService::AudioChannelWindow* +AudioChannelService::GetWindowData(uint64_t aWindowID) const +{ + nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator + iter(mWindows); + while (iter.HasMore()) { + AudioChannelWindow* next = iter.GetNext(); + if (next->mWindowID == aWindowID) { + return next; + } + } + + return nullptr; +} + +float +AudioChannelService::GetAudioChannelVolume(nsPIDOMWindowOuter* aWindow, + AudioChannel aAudioChannel) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsOuterWindow()); + + AudioChannelWindow* winData = GetOrCreateWindowData(aWindow); + return winData->mChannels[(uint32_t)aAudioChannel].mVolume; +} + +NS_IMETHODIMP +AudioChannelService::GetAudioChannelVolume(mozIDOMWindowProxy* aWindow, + unsigned short aAudioChannel, + float* aVolume) +{ + MOZ_ASSERT(NS_IsMainThread()); + + auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop(); + *aVolume = GetAudioChannelVolume(window, (AudioChannel)aAudioChannel); + return NS_OK; +} + +void +AudioChannelService::SetAudioChannelVolume(nsPIDOMWindowOuter* aWindow, + AudioChannel aAudioChannel, + float aVolume) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsOuterWindow()); + + MOZ_LOG(GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelService, SetAudioChannelVolume, window = %p, type = %d, " + "volume = %f\n", aWindow, aAudioChannel, aVolume)); + + AudioChannelWindow* winData = GetOrCreateWindowData(aWindow); + winData->mChannels[(uint32_t)aAudioChannel].mVolume = aVolume; + RefreshAgentsVolumeAndPropagate(aAudioChannel, aWindow); +} + +NS_IMETHODIMP +AudioChannelService::SetAudioChannelVolume(mozIDOMWindowProxy* aWindow, + unsigned short aAudioChannel, + float aVolume) +{ + MOZ_ASSERT(NS_IsMainThread()); + + auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop(); + SetAudioChannelVolume(window, (AudioChannel)aAudioChannel, aVolume); + return NS_OK; +} + +bool +AudioChannelService::GetAudioChannelMuted(nsPIDOMWindowOuter* aWindow, + AudioChannel aAudioChannel) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsOuterWindow()); + + AudioChannelWindow* winData = GetOrCreateWindowData(aWindow); + return winData->mChannels[(uint32_t)aAudioChannel].mMuted; +} + +NS_IMETHODIMP +AudioChannelService::GetAudioChannelMuted(mozIDOMWindowProxy* aWindow, + unsigned short aAudioChannel, + bool* aMuted) +{ + MOZ_ASSERT(NS_IsMainThread()); + + auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop(); + *aMuted = GetAudioChannelMuted(window, (AudioChannel)aAudioChannel); + return NS_OK; +} + +void +AudioChannelService::SetAudioChannelMuted(nsPIDOMWindowOuter* aWindow, + AudioChannel aAudioChannel, + bool aMuted) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsOuterWindow()); + + MOZ_LOG(GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelService, SetAudioChannelMuted, window = %p, type = %d, " + "mute = %d\n", aWindow, aAudioChannel, aMuted)); + + if (aAudioChannel == AudioChannel::System) { + // Workaround for bug1183033, system channel type can always playback. + return; + } + + AudioChannelWindow* winData = GetOrCreateWindowData(aWindow); + winData->mChannels[(uint32_t)aAudioChannel].mMuted = aMuted; + RefreshAgentsVolumeAndPropagate(aAudioChannel, aWindow); +} + +NS_IMETHODIMP +AudioChannelService::SetAudioChannelMuted(mozIDOMWindowProxy* aWindow, + unsigned short aAudioChannel, + bool aMuted) +{ + MOZ_ASSERT(NS_IsMainThread()); + + auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop(); + SetAudioChannelMuted(window, (AudioChannel)aAudioChannel, aMuted); + return NS_OK; +} + +bool +AudioChannelService::IsAudioChannelActive(nsPIDOMWindowOuter* aWindow, + AudioChannel aAudioChannel) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsOuterWindow()); + + AudioChannelWindow* winData = GetOrCreateWindowData(aWindow); + return !!winData->mChannels[(uint32_t)aAudioChannel].mNumberOfAgents; +} + +NS_IMETHODIMP +AudioChannelService::IsAudioChannelActive(mozIDOMWindowProxy* aWindow, + unsigned short aAudioChannel, + bool* aActive) +{ + MOZ_ASSERT(NS_IsMainThread()); + + auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop(); + *aActive = IsAudioChannelActive(window, (AudioChannel)aAudioChannel); + return NS_OK; +} +void +AudioChannelService::SetDefaultVolumeControlChannel(int32_t aChannel, + bool aVisible) +{ + SetDefaultVolumeControlChannelInternal(aChannel, aVisible, + CONTENT_PROCESS_ID_MAIN); +} + +void +AudioChannelService::SetDefaultVolumeControlChannelInternal(int32_t aChannel, + bool aVisible, + uint64_t aChildID) +{ + if (!IsParentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + if (cc) { + cc->SendAudioChannelChangeDefVolChannel(aChannel, aVisible); + } + + return; + } + + // If this child is in the background and mDefChannelChildID is set to + // others then it means other child in the foreground already set it's + // own default channel. + if (!aVisible && mDefChannelChildID != aChildID) { + return; + } + + // Workaround for the call screen app. The call screen app is running on the + // main process, that will results in wrong visible state. Because we use the + // docshell's active state as visible state, the main process is always + // active. Therefore, we will see the strange situation that the visible + // state of the call screen is always true. If the mDefChannelChildID is set + // to others then it means other child in the foreground already set it's + // own default channel already. + // Summary : + // Child process : foreground app always can set type. + // Parent process : check the mDefChannelChildID. + else if (aChildID == CONTENT_PROCESS_ID_MAIN && + mDefChannelChildID != CONTENT_PROCESS_ID_UNKNOWN) { + return; + } + + mDefChannelChildID = aVisible ? aChildID : CONTENT_PROCESS_ID_UNKNOWN; + nsAutoString channelName; + + if (aChannel == -1) { + channelName.AssignASCII("unknown"); + } else { + GetAudioChannelString(static_cast<AudioChannel>(aChannel), channelName); + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "default-volume-channel-changed", + channelName.get()); + } +} + +void +AudioChannelService::MaybeSendStatusUpdate() +{ + if (IsParentProcess()) { + return; + } + + bool telephonyChannel = TelephonyChannelIsActive(); + bool contentOrNormalChannel = ContentOrNormalChannelIsActive(); + bool anyChannel = AnyAudioChannelIsActive(); + + if (telephonyChannel == mTelephonyChannel && + contentOrNormalChannel == mContentOrNormalChannel && + anyChannel == mAnyChannel) { + return; + } + + mTelephonyChannel = telephonyChannel; + mContentOrNormalChannel = contentOrNormalChannel; + mAnyChannel = anyChannel; + + ContentChild* cc = ContentChild::GetSingleton(); + if (cc) { + cc->SendAudioChannelServiceStatus(telephonyChannel, contentOrNormalChannel, + anyChannel); + } +} + +void +AudioChannelService::ChildStatusReceived(uint64_t aChildID, + bool aTelephonyChannel, + bool aContentOrNormalChannel, + bool aAnyChannel) +{ + if (!aAnyChannel) { + RemoveChildStatus(aChildID); + return; + } + + AudioChannelChildStatus* data = GetChildStatus(aChildID); + if (!data) { + data = new AudioChannelChildStatus(aChildID); + mPlayingChildren.AppendElement(data); + } + + data->mActiveTelephonyChannel = aTelephonyChannel; + data->mActiveContentOrNormalChannel = aContentOrNormalChannel; +} + +void +AudioChannelService::RefreshAgentsAudioFocusChanged(AudioChannelAgent* aAgent) +{ + MOZ_ASSERT(aAgent); + + nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator + iter(mWindows); + while (iter.HasMore()) { + AudioChannelWindow* winData = iter.GetNext(); + if (winData->mOwningAudioFocus) { + winData->AudioFocusChanged(aAgent); + } + } +} + +void +AudioChannelService::AudioChannelWindow::RequestAudioFocus(AudioChannelAgent* aAgent) +{ + MOZ_ASSERT(aAgent); + + // Don't need to check audio focus for window-less agent. + if (!aAgent->Window()) { + return; + } + + // We already have the audio focus. No operation is needed. + if (mOwningAudioFocus) { + return; + } + + // Only foreground window can request audio focus, but it would still own the + // audio focus even it goes to background. Audio focus would be abandoned + // only when other foreground window starts audio competing. + // One exception is if the pref "media.block-autoplay-until-in-foreground" + // is on and the background page is the non-visited before. Because the media + // in that page would be blocked until the page is going to foreground. + mOwningAudioFocus = (!(aAgent->Window()->IsBackground()) || + aAgent->Window()->GetMediaSuspend() == nsISuspendedTypes::SUSPENDED_BLOCK) ; + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelWindow, RequestAudioFocus, this = %p, " + "agent = %p, owning audio focus = %d\n", + this, aAgent, mOwningAudioFocus)); +} + +void +AudioChannelService::AudioChannelWindow::NotifyAudioCompetingChanged(AudioChannelAgent* aAgent) +{ + // This function may be called after RemoveAgentAndReduceAgentsNum(), so the + // agent may be not contained in mAgent. In addition, the agent would still + // be alive because we have kungFuDeathGrip in UnregisterAudioChannelAgent(). + MOZ_ASSERT(aAgent); + + RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate(); + MOZ_ASSERT(service); + + if (!service->IsEnableAudioCompeting()) { + return; + } + + if (!IsAgentInvolvingInAudioCompeting(aAgent)) { + return; + } + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelWindow, NotifyAudioCompetingChanged, this = %p, " + "agent = %p\n", + this, aAgent)); + + service->RefreshAgentsAudioFocusChanged(aAgent); +} + +bool +AudioChannelService::AudioChannelWindow::IsAgentInvolvingInAudioCompeting(AudioChannelAgent* aAgent) const +{ + MOZ_ASSERT(aAgent); + + if(!mOwningAudioFocus) { + return false; + } + + if (IsAudioCompetingInSameTab()) { + return false; + } + + // TODO : add MediaSession::ambient kind, because it doens't interact with + // other kinds. + return true; +} + +bool +AudioChannelService::AudioChannelWindow::IsAudioCompetingInSameTab() const +{ + bool hasMultipleActiveAgents = IsEnableAudioCompetingForAllAgents() ? + mAgents.Length() > 1 : mAudibleAgents.Length() > 1; + return mOwningAudioFocus && hasMultipleActiveAgents; +} + +void +AudioChannelService::AudioChannelWindow::AudioFocusChanged(AudioChannelAgent* aNewPlayingAgent) +{ + // This agent isn't always known for the current window, because it can comes + // from other window. + MOZ_ASSERT(aNewPlayingAgent); + + if (IsInactiveWindow()) { + // These would happen in two situations, + // (1) Audio in page A was ended, and another page B want to play audio. + // Page A should abandon its focus. + // (2) Audio was paused by remote-control, page should still own the focus. + mOwningAudioFocus = IsContainingPlayingAgent(aNewPlayingAgent); + } else { + nsTObserverArray<AudioChannelAgent*>::ForwardIterator + iter(IsEnableAudioCompetingForAllAgents() ? mAgents : mAudibleAgents); + while (iter.HasMore()) { + AudioChannelAgent* agent = iter.GetNext(); + MOZ_ASSERT(agent); + + // Don't need to update the playing state of new playing agent. + if (agent == aNewPlayingAgent) { + continue; + } + + uint32_t type = GetCompetingBehavior(agent, + aNewPlayingAgent->AudioChannelType()); + + // If window will be suspended, it needs to abandon the audio focus + // because only one window can own audio focus at a time. However, we + // would support multiple audio focus at the same time in the future. + mOwningAudioFocus = (type == nsISuspendedTypes::NONE_SUSPENDED); + + // TODO : support other behaviors which are definded in MediaSession API. + switch (type) { + case nsISuspendedTypes::NONE_SUSPENDED: + case nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE: + agent->WindowSuspendChanged(type); + break; + } + } + } + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelWindow, AudioFocusChanged, this = %p, " + "OwningAudioFocus = %d\n", this, mOwningAudioFocus)); +} + +bool +AudioChannelService::AudioChannelWindow::IsContainingPlayingAgent(AudioChannelAgent* aAgent) const +{ + return (aAgent->WindowID() == mWindowID); +} + +uint32_t +AudioChannelService::AudioChannelWindow::GetCompetingBehavior(AudioChannelAgent* aAgent, + int32_t aIncomingChannelType) const +{ + MOZ_ASSERT(aAgent); + MOZ_ASSERT(IsEnableAudioCompetingForAllAgents() ? + mAgents.Contains(aAgent) : mAudibleAgents.Contains(aAgent)); + + uint32_t competingBehavior = nsISuspendedTypes::NONE_SUSPENDED; + int32_t presentChannelType = aAgent->AudioChannelType(); + + // TODO : add other competing cases for MediaSession API + if (presentChannelType == int32_t(AudioChannel::Normal) && + aIncomingChannelType == int32_t(AudioChannel::Normal)) { + competingBehavior = nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE; + } + + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("AudioChannelWindow, GetCompetingBehavior, this = %p, " + "present type = %d, incoming channel = %d, behavior = %d\n", + this, presentChannelType, aIncomingChannelType, competingBehavior)); + + return competingBehavior; +} + +/* static */ bool +AudioChannelService::IsAudioChannelMutedByDefault() +{ + CreateServiceIfNeeded(); + return sAudioChannelMutedByDefault; +} + +void +AudioChannelService::AudioChannelWindow::AppendAgent(AudioChannelAgent* aAgent, + AudibleState aAudible) +{ + MOZ_ASSERT(aAgent); + + RequestAudioFocus(aAgent); + AppendAgentAndIncreaseAgentsNum(aAgent); + AudioCapturedChanged(aAgent, AudioCaptureState::eCapturing); + if (aAudible == AudibleState::eAudible) { + AudioAudibleChanged(aAgent, + AudibleState::eAudible, + AudibleChangedReasons::eDataAudibleChanged); + } else if (IsEnableAudioCompetingForAllAgents() && + aAudible != AudibleState::eAudible) { + NotifyAudioCompetingChanged(aAgent); + } +} + +void +AudioChannelService::AudioChannelWindow::RemoveAgent(AudioChannelAgent* aAgent) +{ + MOZ_ASSERT(aAgent); + + RemoveAgentAndReduceAgentsNum(aAgent); + AudioCapturedChanged(aAgent, AudioCaptureState::eNotCapturing); + AudioAudibleChanged(aAgent, + AudibleState::eNotAudible, + AudibleChangedReasons::ePauseStateChanged); +} + +void +AudioChannelService::AudioChannelWindow::AppendAgentAndIncreaseAgentsNum(AudioChannelAgent* aAgent) +{ + MOZ_ASSERT(aAgent); + MOZ_ASSERT(!mAgents.Contains(aAgent)); + + int32_t channel = aAgent->AudioChannelType(); + mAgents.AppendElement(aAgent); + + ++mChannels[channel].mNumberOfAgents; + + // The first one, we must inform the BrowserElementAudioChannel. + if (mChannels[channel].mNumberOfAgents == 1) { + NotifyChannelActive(aAgent->WindowID(), + static_cast<AudioChannel>(channel), + true); + } +} + +void +AudioChannelService::AudioChannelWindow::RemoveAgentAndReduceAgentsNum(AudioChannelAgent* aAgent) +{ + MOZ_ASSERT(aAgent); + MOZ_ASSERT(mAgents.Contains(aAgent)); + + int32_t channel = aAgent->AudioChannelType(); + mAgents.RemoveElement(aAgent); + + MOZ_ASSERT(mChannels[channel].mNumberOfAgents > 0); + --mChannels[channel].mNumberOfAgents; + + if (mChannels[channel].mNumberOfAgents == 0) { + NotifyChannelActive(aAgent->WindowID(), + static_cast<AudioChannel>(channel), + false); + } +} + +void +AudioChannelService::AudioChannelWindow::AudioCapturedChanged(AudioChannelAgent* aAgent, + AudioCaptureState aCapture) +{ + MOZ_ASSERT(aAgent); + + if (mIsAudioCaptured) { + aAgent->WindowAudioCaptureChanged(aAgent->InnerWindowID(), aCapture); + } +} + +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::eAudible) { + NotifyAudioCompetingChanged(aAgent); + } else if (aAudible != AudibleState::eNotAudible) { + MaybeNotifyMediaBlocked(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(); +} + +bool +AudioChannelService::AudioChannelWindow::IsInactiveWindow() const +{ + return IsEnableAudioCompetingForAllAgents() ? + mAudibleAgents.IsEmpty() && mAgents.IsEmpty() : 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::NotifyChannelActive(uint64_t aWindowID, + AudioChannel aChannel, + bool aActive) +{ + RefPtr<NotifyChannelActiveRunnable> runnable = + new NotifyChannelActiveRunnable(aWindowID, aChannel, aActive); + DebugOnly<nsresult> rv = NS_DispatchToCurrentThread(runnable); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToCurrentThread failed"); +} + +void +AudioChannelService::AudioChannelWindow::MaybeNotifyMediaBlocked(AudioChannelAgent* aAgent) +{ + nsCOMPtr<nsPIDOMWindowOuter> window = aAgent->Window(); + if (!window) { + return; + } + + MOZ_ASSERT(window->IsOuterWindow()); + if (window->GetMediaSuspend() != nsISuspendedTypes::SUSPENDED_BLOCK) { + return; + } + + NS_DispatchToCurrentThread(NS_NewRunnableFunction([window] () -> void { + nsCOMPtr<nsIObserverService> observerService = + services::GetObserverService(); + if (NS_WARN_IF(!observerService)) { + return; + } + + observerService->NotifyObservers(ToSupports(window), + "audio-playback", + u"block"); + }) + ); +} diff --git a/dom/audiochannel/AudioChannelService.h b/dom/audiochannel/AudioChannelService.h new file mode 100644 index 0000000000..b16832b5fa --- /dev/null +++ b/dom/audiochannel/AudioChannelService.h @@ -0,0 +1,376 @@ +/* -*- 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 "nsIAudioChannelService.h" +#include "nsAutoPtr.h" +#include "nsIObserver.h" +#include "nsTObserverArray.h" +#include "nsTArray.h" + +#include "AudioChannelAgent.h" +#include "nsAttrValue.h" +#include "mozilla/dom/AudioChannelBinding.h" +#include "mozilla/Function.h" + +class nsIRunnable; +class nsPIDOMWindowOuter; +struct PRLogModuleInfo; + +namespace mozilla { +namespace dom { + +#ifdef MOZ_WIDGET_GONK +class SpeakerManagerService; +#endif + +class TabParent; + +#define NUMBER_OF_AUDIO_CHANNELS (uint32_t)AudioChannel::EndGuard_ + +class AudioPlaybackConfig +{ +public: + AudioPlaybackConfig() + : mVolume(1.0) + , mMuted(false) + , mSuspend(nsISuspendedTypes::NONE_SUSPENDED) + {} + + AudioPlaybackConfig(float aVolume, bool aMuted, uint32_t aSuspended) + : mVolume(aVolume) + , mMuted(aMuted) + , mSuspend(aSuspended) + {} + + void SetConfig(float aVolume, bool aMuted, uint32_t aSuspended) + { + mVolume = aVolume; + mMuted = aMuted; + mSuspend = aSuspended; + } + + float mVolume; + bool mMuted; + uint32_t mSuspend; +}; + +class AudioChannelService final : public nsIAudioChannelService + , public nsIObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIAUDIOCHANNELSERVICE + + /** + * 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 AudioChannelServce is not exist, create and return new one. + * Only to be called from main thread. + */ + static already_AddRefed<AudioChannelService> GetOrCreate(); + + static bool IsAudioChannelMutedByDefault(); + + static PRLogModuleInfo* 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); + + /** + * For nested iframes. + */ + void RegisterTabParent(TabParent* aTabParent); + void UnregisterTabParent(TabParent* aTabParent); + + /** + * Return the state to indicate this audioChannel for his window should keep + * playing/muted/suspended. + */ + AudioPlaybackConfig GetMediaConfig(nsPIDOMWindowOuter* aWindow, + uint32_t aAudioChannel) 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); + + /* Methods for the BrowserElementAudioChannel */ + float GetAudioChannelVolume(nsPIDOMWindowOuter* aWindow, AudioChannel aChannel); + + void SetAudioChannelVolume(nsPIDOMWindowOuter* aWindow, AudioChannel aChannel, + float aVolume); + + bool GetAudioChannelMuted(nsPIDOMWindowOuter* aWindow, AudioChannel aChannel); + + void SetAudioChannelMuted(nsPIDOMWindowOuter* aWindow, AudioChannel aChannel, + bool aMuted); + + bool IsAudioChannelActive(nsPIDOMWindowOuter* aWindow, AudioChannel aChannel); + + /** + * Return true if there is a telephony channel active in this process + * or one of its subprocesses. + */ + bool TelephonyChannelIsActive(); + + /** + * Return true if a normal or content channel is active for the given + * process ID. + */ + bool ProcessContentOrNormalChannelIsActive(uint64_t aChildID); + + /*** + * AudioChannelManager calls this function to notify the default channel used + * to adjust volume when there is no any active channel. if aChannel is -1, + * the default audio channel will be used. Otherwise aChannel is casted to + * AudioChannel enum. + */ + virtual void SetDefaultVolumeControlChannel(int32_t aChannel, + bool aVisible); + + bool AnyAudioChannelIsActive(); + + void RefreshAgentsVolume(nsPIDOMWindowOuter* aWindow); + void RefreshAgentsSuspend(nsPIDOMWindowOuter* aWindow, + nsSuspendedTypes aSuspend); + + void RefreshAgentsVolumeAndPropagate(AudioChannel aAudioChannel, + nsPIDOMWindowOuter* aWindow); + + // 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); + +#ifdef MOZ_WIDGET_GONK + void RegisterSpeakerManager(SpeakerManagerService* aSpeakerManager) + { + if (!mSpeakerManager.Contains(aSpeakerManager)) { + mSpeakerManager.AppendElement(aSpeakerManager); + } + } + + void UnregisterSpeakerManager(SpeakerManagerService* aSpeakerManager) + { + mSpeakerManager.RemoveElement(aSpeakerManager); + } +#endif + + static const nsAttrValue::EnumTable* GetAudioChannelTable(); + static AudioChannel GetAudioChannel(const nsAString& aString); + static AudioChannel GetDefaultAudioChannel(); + static void GetAudioChannelString(AudioChannel aChannel, nsAString& aString); + static void GetDefaultAudioChannelString(nsAString& aString); + + void Notify(uint64_t aWindowID); + + void ChildStatusReceived(uint64_t aChildID, bool aTelephonyChannel, + bool aContentOrNormalChannel, bool aAnyChannel); + +private: + AudioChannelService(); + ~AudioChannelService(); + + void RefreshAgents(nsPIDOMWindowOuter* aWindow, + mozilla::function<void(AudioChannelAgent*)> aFunc); + + static void CreateServiceIfNeeded(); + + /** + * Shutdown the singleton. + */ + static void Shutdown(); + + void MaybeSendStatusUpdate(); + + bool ContentOrNormalChannelIsActive(); + + /* Send the default-volume-channel-changed notification */ + void SetDefaultVolumeControlChannelInternal(int32_t aChannel, + bool aVisible, uint64_t aChildID); + + void RefreshAgentsAudioFocusChanged(AudioChannelAgent* aAgent); + + class AudioChannelConfig final : public AudioPlaybackConfig + { + public: + AudioChannelConfig() + : AudioPlaybackConfig(1.0, IsAudioChannelMutedByDefault(), + nsISuspendedTypes::NONE_SUSPENDED) + , mNumberOfAgents(0) + {} + + uint32_t mNumberOfAgents; + }; + + class AudioChannelWindow final + { + public: + explicit AudioChannelWindow(uint64_t aWindowID) + : mWindowID(aWindowID) + , mIsAudioCaptured(false) + , mOwningAudioFocus(!AudioChannelService::IsEnableAudioCompeting()) + { + // Workaround for bug1183033, system channel type can always playback. + mChannels[(int16_t)AudioChannel::System].mMuted = false; + } + + void AudioFocusChanged(AudioChannelAgent* aNewPlayingAgent); + void AudioAudibleChanged(AudioChannelAgent* aAgent, + AudibleState aAudible, + AudibleChangedReasons aReason); + + void AppendAgent(AudioChannelAgent* aAgent, AudibleState aAudible); + void RemoveAgent(AudioChannelAgent* aAgent); + + uint64_t mWindowID; + bool mIsAudioCaptured; + AudioChannelConfig mChannels[NUMBER_OF_AUDIO_CHANNELS]; + + // Raw pointer because the AudioChannelAgent must unregister itself. + nsTObserverArray<AudioChannelAgent*> mAgents; + nsTObserverArray<AudioChannelAgent*> mAudibleAgents; + + // Owning audio focus when the window starts playing audible sound, and + // lose audio focus when other windows starts playing. + bool mOwningAudioFocus; + + private: + void AudioCapturedChanged(AudioChannelAgent* aAgent, + AudioCaptureState aCapture); + + 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 NotifyChannelActive(uint64_t aWindowID, AudioChannel aChannel, + bool aActive); + void MaybeNotifyMediaBlocked(AudioChannelAgent* aAgent); + + void RequestAudioFocus(AudioChannelAgent* aAgent); + // We need to do audio competing only when the new incoming agent started. + void NotifyAudioCompetingChanged(AudioChannelAgent* aAgent); + + uint32_t GetCompetingBehavior(AudioChannelAgent* aAgent, + int32_t aIncomingChannelType) const; + bool IsAgentInvolvingInAudioCompeting(AudioChannelAgent* aAgent) const; + bool IsAudioCompetingInSameTab() const; + bool IsContainingPlayingAgent(AudioChannelAgent* aAgent) const; + + bool IsInactiveWindow() const; + }; + + AudioChannelWindow* + GetOrCreateWindowData(nsPIDOMWindowOuter* aWindow); + + AudioChannelWindow* + GetWindowData(uint64_t aWindowID) const; + + struct AudioChannelChildStatus final + { + explicit AudioChannelChildStatus(uint64_t aChildID) + : mChildID(aChildID) + , mActiveTelephonyChannel(false) + , mActiveContentOrNormalChannel(false) + {} + + uint64_t mChildID; + bool mActiveTelephonyChannel; + bool mActiveContentOrNormalChannel; + }; + + AudioChannelChildStatus* + GetChildStatus(uint64_t aChildID) const; + + void + RemoveChildStatus(uint64_t aChildID); + + nsTObserverArray<nsAutoPtr<AudioChannelWindow>> mWindows; + + nsTObserverArray<nsAutoPtr<AudioChannelChildStatus>> mPlayingChildren; + +#ifdef MOZ_WIDGET_GONK + nsTArray<SpeakerManagerService*> mSpeakerManager; +#endif + + // Raw pointers because TabParents must unregister themselves. + nsTArray<TabParent*> mTabParents; + + nsCOMPtr<nsIRunnable> mRunnable; + + uint64_t mDefChannelChildID; + + // These boolean are used to know if we have to send an status update to the + // service running in the main process. + bool mTelephonyChannel; + bool mContentOrNormalChannel; + bool mAnyChannel; + + // This is needed for IPC comunication between + // AudioChannelServiceChild and this class. + friend class ContentParent; + friend class ContentChild; +}; + +} // namespace dom +} // namespace mozilla + +#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/crashtests.list b/dom/audiochannel/crashtests/crashtests.list new file mode 100644 index 0000000000..f877939416 --- /dev/null +++ b/dom/audiochannel/crashtests/crashtests.list @@ -0,0 +1 @@ +load 1223734.html
diff --git a/dom/audiochannel/moz.build b/dom/audiochannel/moz.build new file mode 100644 index 0000000000..46923db3c7 --- /dev/null +++ b/dom/audiochannel/moz.build @@ -0,0 +1,30 @@ +# -*- 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/. + +XPIDL_SOURCES += [ + 'nsIAudioChannelAgent.idl', + 'nsIAudioChannelService.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..820f8da852 --- /dev/null +++ b/dom/audiochannel/nsIAudioChannelAgent.idl @@ -0,0 +1,187 @@ +/* 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 in three different situations, + * - platform audio focus (Fennec/B2G) + * - remote media control (Fennec) + * - block auto-play video in non-active page + * + * Note: the "remote side" must control the AudioChannelAgent using + * nsIAudioChannelAgentCallback.windowSuspendChanged() callback instead using + * play/pause methods or any button in the webpage. + * + * - SUSPENDED_PAUSE : + * It's used when transiently losing audio focus, the media can't be resumed + * until we gain the audio focus again. It would change the internal state of + * MediaElement when it's being suspended/resumed, and it would trigger the + * related JS event. eg. "play" and "pause" event. + * + * - 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. + * + * - SUSPENDED_PAUSE_DISPOSABLE + * It's used for remote media-control to pause the playing media and when we + * lose audio focus permanently. It's disposable suspended, so the media can + * be resumed arbitrary after that. Same as SUSPENDED_PAUSE, it would change + * the internal state of MediaElement when it's being suspended. + * + * - SUSPENDED_STOP_DISPOSABLE + * It's used for remote media-control to stop the playing media. The remote + * control would disappear after stopping the media, so we would disconnect + * the audio channel agent. It's disposable suspended, so the media can be + * resumed arbitrary after that. Same as SUSPENDED_PAUSE, it would change + * the internal state of MediaElement when it's being suspended. + */ + + const uint32_t NONE_SUSPENDED = 0; + const uint32_t SUSPENDED_PAUSE = 1; + const uint32_t SUSPENDED_BLOCK = 2; + const uint32_t SUSPENDED_PAUSE_DISPOSABLE = 3; + const uint32_t SUSPENDED_STOP_DISPOSABLE = 4; +}; + +%{C++ +namespace mozilla { +namespace dom { +// It's defined in dom/audiochannel/AudioChannelService.h. +class AudioPlaybackConfig; +} +} +%} +[ptr] native AudioPlaybackConfig(mozilla::dom::AudioPlaybackConfig); + +[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. Indicating what channel type they are using (via the init() member + * function). + * 2. Notifying the agent when they start/stop using this channel. + * 3. 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(ab7e21c0-970c-11e5-a837-0800200c9a66)] +interface nsIAudioChannelAgent : nsISupports +{ + const long AUDIO_AGENT_CHANNEL_NORMAL = 0; + const long AUDIO_AGENT_CHANNEL_CONTENT = 1; + const long AUDIO_AGENT_CHANNEL_NOTIFICATION = 2; + const long AUDIO_AGENT_CHANNEL_ALARM = 3; + const long AUDIO_AGENT_CHANNEL_TELEPHONY = 4; + const long AUDIO_AGENT_CHANNEL_RINGER = 5; + const long AUDIO_AGENT_CHANNEL_PUBLICNOTIFICATION = 6; + const long AUDIO_AGENT_CHANNEL_SYSTEM = 7; + + const long AUDIO_AGENT_CHANNEL_ERROR = 1000; + + const long AUDIO_AGENT_STATE_NORMAL = 0; + const long AUDIO_AGENT_STATE_MUTED = 1; + const long AUDIO_AGENT_STATE_FADED = 2; + + /** + * Before init() is called, this returns AUDIO_AGENT_CHANNEL_ERROR. + */ + readonly attribute long audioChannelType; + + %{C++ + inline int32_t AudioChannelType() { + int32_t channel; + return NS_SUCCEEDED(GetAudioChannelType(&channel)) ? channel : AUDIO_AGENT_CHANNEL_ERROR; + } + %} + + /** + * Initialize the agent with a channel type. + * Note: This function should only be called once. + * + * @param window + * The window + * @param channelType + * Audio Channel Type listed as above + * @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 long channelType, + 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 long channelType, + 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. + * + * @param config + * It contains the playback related states (volume/mute/suspend) + */ + void notifyStartedPlaying(in AudioPlaybackConfig config, 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); +}; diff --git a/dom/audiochannel/nsIAudioChannelService.idl b/dom/audiochannel/nsIAudioChannelService.idl new file mode 100644 index 0000000000..2c154a84b3 --- /dev/null +++ b/dom/audiochannel/nsIAudioChannelService.idl @@ -0,0 +1,29 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 mozIDOMWindowProxy; + +[scriptable, builtinclass, uuid(5cb24dbc-36c7-46a4-9966-ac73141dc795)] +interface nsIAudioChannelService : nsISupports +{ + float getAudioChannelVolume(in mozIDOMWindowProxy window, + in unsigned short audioChannel); + + void setAudioChannelVolume(in mozIDOMWindowProxy window, + in unsigned short audioChannel, + in float volume); + + boolean getAudioChannelMuted(in mozIDOMWindowProxy window, + in unsigned short audioChannel); + + void setAudioChannelMuted(in mozIDOMWindowProxy window, + in unsigned short audioChannel, + in boolean muted); + + boolean isAudioChannelActive(in mozIDOMWindowProxy window, + in unsigned short audioChannel); +}; |