/* -*- Mode: C++; tab-width: 8; 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 "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" #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 observerService = services::GetObserverService(); if (NS_WARN_IF(!observerService)) { return NS_ERROR_FAILURE; } nsCOMPtr 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 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 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 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::GetOrCreate() { if (sXPCOMShuttingDown) { return nullptr; } CreateServiceIfNeeded(); RefPtr 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 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"); } } gAudioChannelService->mWindows.Clear(); gAudioChannelService->mPlayingChildren.Clear(); gAudioChannelService->mTabParents.Clear(); 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 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); } } 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 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 kungFuDeathGrip(aAgent); winData->RemoveAgent(aAgent); 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 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 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>::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>::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>::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>::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>::ForwardIterator iter(mPlayingChildren); while (iter.HasMore()) { nsAutoPtr& 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>::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 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 winData; { nsTObserverArray>::ForwardIterator iter(mWindows); while (iter.HasMore()) { nsAutoPtr& next = iter.GetNext(); if (next->mWindowID == outerID) { uint32_t pos = mWindows.IndexOf(next); winData = next.forget(); mWindows.RemoveElementAt(pos); break; } } } if (winData) { nsTObserverArray::ForwardIterator iter(winData->mAgents); while (iter.HasMore()) { iter.GetNext()->WindowVolumeChanged(); } } } else if (!strcmp(aTopic, "ipc:content-shutdown")) { nsCOMPtr 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 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 aFunc) { MOZ_ASSERT(aWindow); MOZ_ASSERT(aWindow->IsOuterWindow()); nsCOMPtr topWindow = aWindow->GetScriptableTop(); if (!topWindow) { return; } AudioChannelWindow* winData = GetWindowData(topWindow->WindowID()); if (!winData) { return; } nsTObserverArray::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 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::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(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(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(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>::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(aChannel), channelName); } nsCOMPtr 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>::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 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::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(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(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 runnable = new AudioPlaybackRunnable(aWindow, aAudible == AudibleState::eAudible, aReason); DebugOnly 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 runnable = new NotifyChannelActiveRunnable(aWindowID, aChannel, aActive); DebugOnly rv = NS_DispatchToCurrentThread(runnable); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToCurrentThread failed"); } void AudioChannelService::AudioChannelWindow::MaybeNotifyMediaBlocked(AudioChannelAgent* aAgent) { nsCOMPtr window = aAgent->Window(); if (!window) { return; } MOZ_ASSERT(window->IsOuterWindow()); if (window->GetMediaSuspend() != nsISuspendedTypes::SUSPENDED_BLOCK) { return; } NS_DispatchToCurrentThread(NS_NewRunnableFunction([window] () -> void { nsCOMPtr observerService = services::GetObserverService(); if (NS_WARN_IF(!observerService)) { return; } observerService->NotifyObservers(ToSupports(window), "audio-playback", u"block"); }) ); }