Bug 1257738 - part1 : implement the audio competing mechanism.
MozReview-Commit-ID: E5d62oD4jIq
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -1082,13 +1082,15 @@ pref("dom.presentation.device.name", "Fi
// Enable notification of performance timing
pref("dom.performance.enable_notify_performance_timing", true);
// Multi-screen
pref("b2g.multiscreen.chrome_remote_url", "chrome://b2g/content/shell_remote.html");
pref("b2g.multiscreen.system_remote_url", "index_remote.html");
+// Audio competing between tabs
+pref("dom.audiochannel.audioCompeting", false);
// Because we can't have nice things.
#ifdef MOZ_GRAPHENE
#include ../graphene/graphene.js
#endif
--- a/dom/audiochannel/AudioChannelService.cpp
+++ b/dom/audiochannel/AudioChannelService.cpp
@@ -17,16 +17,17 @@
#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
@@ -36,16 +37,17 @@
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 sXPCOMShuttingDown = false;
class NotifyChannelActiveRunnable final : public Runnable
{
public:
NotifyChannelActiveRunnable(uint64_t aWindowID, AudioChannel aAudioChannel,
bool aActive)
: mWindowID(aWindowID)
@@ -205,16 +207,23 @@ AudioChannelService::Shutdown()
#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)
@@ -236,16 +245,18 @@ AudioChannelService::AudioChannelService
// 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");
}
AudioChannelService::~AudioChannelService()
{
}
void
AudioChannelService::RegisterAudioChannelAgent(AudioChannelAgent* aAgent,
@@ -331,24 +342,25 @@ AudioChannelService::GetMediaConfig(nsPI
// 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 the mSuspend is already suspended, we don't need to set it again.
- config.mSuspend = (config.mSuspend == nsISuspendedTypes::NONE_SUSPENDED) ?
- window->GetMediaSuspend() : config.mSuspend;
+ if (window->GetMediaSuspend() != nsISuspendedTypes::NONE_SUSPENDED) {
+ config.mSuspend = window->GetMediaSuspend();
+ }
nsCOMPtr<nsPIDOMWindowOuter> win = window->GetScriptableParentOrNull();
if (!win) {
break;
}
window = do_QueryInterface(win);
@@ -989,32 +1001,205 @@ AudioChannelService::ChildStatusReceived
data = new AudioChannelChildStatus(aChildID);
mPlayingChildren.AppendElement(data);
}
data->mActiveTelephonyChannel = aTelephonyChannel;
data->mActiveContentOrNormalChannel = aContentOrNormalChannel;
}
+void
+AudioChannelService::RefreshAgentsAudioFocusChanged(AudioChannelAgent* aAgent,
+ bool aActive)
+{
+ MOZ_ASSERT(aAgent);
+
+ nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator
+ iter(mWindows);
+ while (iter.HasMore()) {
+ AudioChannelWindow* winData = iter.GetNext();
+ if (winData->mOwningAudioFocus) {
+ winData->AudioFocusChanged(aAgent, aActive);
+ }
+ }
+}
+
+void
+AudioChannelService::AudioChannelWindow::RequestAudioFocus(AudioChannelAgent* aAgent)
+{
+ MOZ_ASSERT(aAgent);
+
+ // 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.
+ mOwningAudioFocus = !(aAgent->Window()->IsBackground());
+
+ 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,
+ bool aActive)
+{
+ // 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, active = %d\n",
+ this, aAgent, aActive));
+
+ service->RefreshAgentsAudioFocusChanged(aAgent, aActive);
+}
+
+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
+{
+ return (mOwningAudioFocus && mAudibleAgents.Length() > 1);
+}
+
+void
+AudioChannelService::AudioChannelWindow::AudioFocusChanged(AudioChannelAgent* aNewPlayingAgent,
+ bool aActive)
+{
+ // This agent isn't always known for the current window, because it can comes
+ // from other window.
+ MOZ_ASSERT(aNewPlayingAgent);
+
+ if (mAudibleAgents.IsEmpty()) {
+ // 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(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(),
+ aActive);
+
+ // 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,
+ bool aIncomingChannelActive) const
+{
+ MOZ_ASSERT(aAgent);
+ MOZ_ASSERT(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) &&
+ aIncomingChannelActive) {
+ 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);
- AudioAudibleChanged(aAgent, aAudible);
+ if (aAudible) {
+ AudioAudibleChanged(aAgent, AudibleState::eAudible);
+ }
}
void
AudioChannelService::AudioChannelWindow::RemoveAgent(AudioChannelAgent* aAgent)
{
MOZ_ASSERT(aAgent);
RemoveAgentAndReduceAgentsNum(aAgent);
@@ -1077,16 +1262,18 @@ AudioChannelService::AudioChannelWindow:
{
MOZ_ASSERT(aAgent);
if (aAudible) {
AppendAudibleAgentIfNotContained(aAgent);
} else {
RemoveAudibleAgentIfContained(aAgent);
}
+
+ NotifyAudioCompetingChanged(aAgent, aAudible);
}
void
AudioChannelService::AudioChannelWindow::AppendAudibleAgentIfNotContained(AudioChannelAgent* aAgent)
{
MOZ_ASSERT(aAgent);
MOZ_ASSERT(mAgents.Contains(aAgent));
--- a/dom/audiochannel/AudioChannelService.h
+++ b/dom/audiochannel/AudioChannelService.h
@@ -84,16 +84,18 @@ public:
* 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);
/**
@@ -215,52 +217,61 @@ private:
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,
+ bool aActive);
+
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)
+ : 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, bool aActive);
void AudioAudibleChanged(AudioChannelAgent* aAgent, AudibleState aAudible);
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);
void RemoveAudibleAgentIfContained(AudioChannelAgent* aAgent);
void AppendAgentAndIncreaseAgentsNum(AudioChannelAgent* aAgent);
@@ -268,16 +279,26 @@ private:
bool IsFirstAudibleAgent() const;
bool IsLastAudibleAgent() const;
void NotifyAudioAudibleChanged(nsPIDOMWindowOuter* aWindow,
AudibleState aAudible);
void NotifyChannelActive(uint64_t aWindowID, AudioChannel aChannel,
bool aActive);
+
+ void RequestAudioFocus(AudioChannelAgent* aAgent);
+ void NotifyAudioCompetingChanged(AudioChannelAgent* aAgent, bool aActive);
+
+ uint32_t GetCompetingBehavior(AudioChannelAgent* aAgent,
+ int32_t aIncomingChannelType,
+ bool aIncomingChannelActive) const;
+ bool IsAgentInvolvingInAudioCompeting(AudioChannelAgent* aAgent) const;
+ bool IsAudioCompetingInSameTab() const;
+ bool IsContainingPlayingAgent(AudioChannelAgent* aAgent) const;
};
AudioChannelWindow*
GetOrCreateWindowData(nsPIDOMWindowOuter* aWindow);
AudioChannelWindow*
GetWindowData(uint64_t aWindowID) const;
--- a/dom/audiochannel/moz.build
+++ b/dom/audiochannel/moz.build
@@ -16,11 +16,15 @@ EXPORTS += [
'AudioChannelService.h',
]
UNIFIED_SOURCES += [
'AudioChannelAgent.cpp',
'AudioChannelService.cpp',
]
+LOCAL_INCLUDES += [
+ '/dom/base/',
+]
+
include('/ipc/chromium/chromium-config.mozbuild')
FINAL_LIBRARY = 'xul'
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -918,11 +918,12 @@ pref("identity.fxaccounts.remote.oauth.u
// Token server used by Firefox Account-authenticated Sync.
pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
// Enable Presentation API
pref("dom.presentation.enabled", true);
pref("dom.presentation.discovery.enabled", true);
+pref("dom.audiochannel.audioCompeting", true);
// TODO : remove it after landing bug1242874 because now it's the only way to
// suspend the MediaElement.
pref("media.useAudioChannelAPI", true);
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5397,16 +5397,18 @@ pref("dom.input.fallbackUploadDir", "");
pref("plugins.rewrite_youtube_embeds", true);
// Disable browser frames by default
pref("dom.mozBrowserFramesEnabled", false);
// Is support for 'color-adjust' CSS property enabled?
pref("layout.css.color-adjust.enabled", true);
+pref("dom.audiochannel.audioCompeting", false);
+
// Disable Node.rootNode in release builds.
#ifdef RELEASE_BUILD
pref("dom.node.rootNode.enabled", false);
#else
pref("dom.node.rootNode.enabled", true);
#endif
// Once bug 1276272 is resolved, we will trun this preference to default ON in