Commit 31479d42 authored by Becca Hughes's avatar Becca Hughes Committed by Commit Bot

[Audio Focus] Add grouped audio focus

Add grouped audio focus. This means that a media session
can provide an unguessable token when requesting audio
focus. This means that the media session service will
treat all media sessions with the same group id as the
same session when it comes to audio focus.

BUG=906285

Change-Id: I184f8de58cfe3cec67db0bb5877f3b352c794f13
Reviewed-on: https://chromium-review.googlesource.com/c/1342939
Commit-Queue: Becca Hughes <beccahughes@chromium.org>
Reviewed-by: default avatarChrome Cunningham <chcunningham@chromium.org>
Reviewed-by: default avatarTommy Steimel <steimel@chromium.org>
Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#611805}
parent 38ee168d
......@@ -4,6 +4,8 @@
#include "content/browser/media/session/audio_focus_delegate.h"
#include "base/no_destructor.h"
#include "base/unguessable_token.h"
#include "content/browser/media/session/media_session_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/service_manager_connection.h"
......@@ -20,6 +22,12 @@ namespace {
const char kAudioFocusSourceName[] = "web";
static const base::UnguessableToken& GetBrowserGroupId() {
static const base::NoDestructor<base::UnguessableToken> token(
base::UnguessableToken::Create());
return *token;
}
// AudioFocusDelegateDefault is the default implementation of
// AudioFocusDelegate which only handles audio focus between WebContents.
class AudioFocusDelegateDefault : public AudioFocusDelegate {
......@@ -91,9 +99,12 @@ AudioFocusDelegateDefault::RequestAudioFocus(AudioFocusType audio_focus_type) {
media_session::mojom::MediaSessionPtr media_session;
media_session_->BindToMojoRequest(mojo::MakeRequest(&media_session));
audio_focus_ptr_->RequestAudioFocus(
audio_focus_ptr_->RequestGroupedAudioFocus(
mojo::MakeRequest(&request_client_ptr_), std::move(media_session),
session_info_.Clone(), audio_focus_type,
media_session_->audio_focus_group_id() == base::UnguessableToken::Null()
? GetBrowserGroupId()
: media_session_->audio_focus_group_id(),
base::BindOnce(&AudioFocusDelegateDefault::FinishAudioFocusRequest,
base::Unretained(this), audio_focus_type));
}
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
#include "base/command_line.h"
#include "base/unguessable_token.h"
#include "content/browser/media/session/media_session_impl.h"
#include "content/browser/media/session/mock_media_session_player_observer.h"
#include "content/public/common/service_manager_connection.h"
......@@ -93,7 +94,9 @@ class AudioFocusDelegateDefaultBrowserTest : public ContentBrowserTest {
audio_focus_ptr_.FlushForTesting();
}
void Run(WebContents* start_contents, WebContents* interrupt_contents) {
void Run(WebContents* start_contents,
WebContents* interrupt_contents,
bool use_separate_group_id) {
std::unique_ptr<MockMediaSessionPlayerObserver>
player_observer(new MockMediaSessionPlayerObserver);
......@@ -104,6 +107,10 @@ class AudioFocusDelegateDefaultBrowserTest : public ContentBrowserTest {
MediaSessionImpl::Get(interrupt_contents);
EXPECT_TRUE(other_media_session);
if (use_separate_group_id)
other_media_session->SetAudioFocusGroupId(
base::UnguessableToken::Create());
player_observer->StartNewPlayer();
{
......@@ -139,7 +146,9 @@ class AudioFocusDelegateDefaultBrowserTest : public ContentBrowserTest {
{
MediaSessionStateObserver state_observer(media_session);
state_observer.WaitForState(
media_session::mojom::MediaSessionInfo::SessionState::kSuspended);
use_separate_group_id
? media_session::mojom::MediaSessionInfo::SessionState::kSuspended
: media_session::mojom::MediaSessionInfo::SessionState::kActive);
}
{
......@@ -186,20 +195,28 @@ class AudioFocusDelegateDefaultBrowserTest : public ContentBrowserTest {
// Two windows from the same BrowserContext.
IN_PROC_BROWSER_TEST_F(AudioFocusDelegateDefaultBrowserTest,
ActiveWebContentsPauseOthers) {
Run(shell()->web_contents(), CreateBrowser()->web_contents());
ActiveWebContentsPausesOthers) {
Run(shell()->web_contents(), CreateBrowser()->web_contents(), false);
}
// Two windows with different group ids.
IN_PROC_BROWSER_TEST_F(AudioFocusDelegateDefaultBrowserTest,
ActiveWebContentsPausesOtherWithGroupId) {
Run(shell()->web_contents(), CreateBrowser()->web_contents(), true);
}
// Regular BrowserContext is interrupted by OffTheRecord one.
IN_PROC_BROWSER_TEST_F(AudioFocusDelegateDefaultBrowserTest,
RegularBrowserInterruptsOffTheRecord) {
Run(shell()->web_contents(), CreateOffTheRecordBrowser()->web_contents());
Run(shell()->web_contents(), CreateOffTheRecordBrowser()->web_contents(),
false);
}
// OffTheRecord BrowserContext is interrupted by regular one.
IN_PROC_BROWSER_TEST_F(AudioFocusDelegateDefaultBrowserTest,
OffTheRecordInterruptsRegular) {
Run(CreateOffTheRecordBrowser()->web_contents(), shell()->web_contents());
Run(CreateOffTheRecordBrowser()->web_contents(), shell()->web_contents(),
false);
}
} // namespace content
......@@ -499,6 +499,11 @@ void MediaSessionImpl::SetDuckingVolumeMultiplier(double multiplier) {
ducking_volume_multiplier_ = base::ClampToRange(multiplier, 0.0, 1.0);
}
void MediaSessionImpl::SetAudioFocusGroupId(
const base::UnguessableToken& group_id) {
audio_focus_group_id_ = group_id;
}
void MediaSessionImpl::StartDucking() {
if (is_ducking_)
return;
......
......@@ -197,6 +197,12 @@ class MediaSessionImpl : public MediaSession,
// Set the volume multiplier applied during ducking.
CONTENT_EXPORT void SetDuckingVolumeMultiplier(double multiplier) override;
// Set the audio focus group id for this media session. Sessions in the same
// group can share audio focus. Setting this to null will use the browser
// default value.
CONTENT_EXPORT void SetAudioFocusGroupId(
const base::UnguessableToken& group_id);
// Suspend the media session.
// |type| represents the origin of the request.
CONTENT_EXPORT void Suspend(MediaSession::SuspendType suspend_type) override;
......@@ -232,6 +238,10 @@ class MediaSessionImpl : public MediaSession,
// Skip to the next track.
CONTENT_EXPORT void NextTrack() override;
const base::UnguessableToken& audio_focus_group_id() const {
return audio_focus_group_id_;
}
private:
friend class content::WebContentsUserData<MediaSessionImpl>;
friend class ::MediaSessionImplBrowserTest;
......@@ -355,6 +365,8 @@ class MediaSessionImpl : public MediaSession,
// StopDucking().
bool is_ducking_;
base::UnguessableToken audio_focus_group_id_ = base::UnguessableToken::Null();
double ducking_volume_multiplier_;
base::CallbackList<void(State)> media_session_state_listeners_;
......
......@@ -25,9 +25,11 @@ class AudioFocusManager::StackRow : public mojom::AudioFocusRequestClient {
mojom::MediaSessionInfoPtr session_info,
mojom::AudioFocusType audio_focus_type,
RequestId id,
const std::string& source_name)
const std::string& source_name,
const base::UnguessableToken& group_id)
: id_(id),
source_name_(source_name),
group_id_(group_id),
metrics_helper_(source_name),
session_(std::move(session)),
session_info_(std::move(session_info)),
......@@ -106,6 +108,8 @@ class AudioFocusManager::StackRow : public mojom::AudioFocusRequestClient {
const std::string& source_name() const { return source_name_; }
const base::UnguessableToken& group_id() const { return group_id_; }
mojom::AudioFocusRequestStatePtr ToAudioFocusRequestState() const {
auto request = mojom::AudioFocusRequestState::New();
request->session_info = session_info_.Clone();
......@@ -135,6 +139,7 @@ class AudioFocusManager::StackRow : public mojom::AudioFocusRequestClient {
const RequestId id_;
const std::string source_name_;
const base::UnguessableToken group_id_;
AudioFocusManagerMetricsHelper metrics_helper_;
bool encountered_error_ = false;
......@@ -156,11 +161,23 @@ void AudioFocusManager::RequestAudioFocus(
mojom::MediaSessionInfoPtr session_info,
mojom::AudioFocusType type,
RequestAudioFocusCallback callback) {
RequestGroupedAudioFocus(
std::move(request), std::move(media_session), std::move(session_info),
type, base::UnguessableToken::Create(), std::move(callback));
}
void AudioFocusManager::RequestGroupedAudioFocus(
mojom::AudioFocusRequestClientRequest request,
mojom::MediaSessionPtr media_session,
mojom::MediaSessionInfoPtr session_info,
mojom::AudioFocusType type,
const base::UnguessableToken& group_id,
RequestGroupedAudioFocusCallback callback) {
RequestAudioFocusInternal(
std::make_unique<StackRow>(
this, std::move(request), std::move(media_session),
std::move(session_info), type, base::UnguessableToken::Create(),
GetBindingSourceName()),
GetBindingSourceName(), group_id),
type, std::move(callback));
}
......@@ -180,7 +197,20 @@ void AudioFocusManager::GetDebugInfoForRequest(
if (row->id() != request_id)
continue;
row->session()->GetDebugInfo(std::move(callback));
row->session()->GetDebugInfo(base::BindOnce(
[](const base::UnguessableToken& group_id,
GetDebugInfoForRequestCallback callback,
mojom::MediaSessionDebugInfoPtr info) {
// Inject the |group_id| into the state string. This is because in
// some cases the group id is automatically generated by the media
// session service so the session is unaware of it.
if (!info->state.empty())
info->state += " ";
info->state += "GroupId=" + group_id.ToString();
std::move(callback).Run(std::move(info));
},
row->group_id(), std::move(callback)));
return;
}
......@@ -264,7 +294,7 @@ void AudioFocusManager::RequestAudioFocusInternal(
// If audio focus is enabled then we should enforce this request and make sure
// the new active session is not ducking.
if (IsAudioFocusEnforcementEnabled()) {
EnforceAudioFocusRequest(type);
EnforceAudioFocusRequest(type, row->group_id());
row->session()->StopDucking();
}
......@@ -286,7 +316,9 @@ void AudioFocusManager::RequestAudioFocusInternal(
std::move(callback).Run();
}
void AudioFocusManager::EnforceAudioFocusRequest(mojom::AudioFocusType type) {
void AudioFocusManager::EnforceAudioFocusRequest(
mojom::AudioFocusType type,
const base::UnguessableToken& group_id) {
DCHECK(IsAudioFocusEnforcementEnabled());
for (auto& old_session : audio_focus_stack_) {
......@@ -299,6 +331,11 @@ void AudioFocusManager::EnforceAudioFocusRequest(mojom::AudioFocusType type) {
switch (type) {
case mojom::AudioFocusType::kGain:
case mojom::AudioFocusType::kGainTransient:
// If the session has the same group id as the new session then we
// should not suspend that session.
if (old_session->group_id() == group_id)
break;
old_session->session()->Suspend(
mojom::MediaSession::SuspendType::kSystem);
break;
......@@ -338,7 +375,14 @@ void AudioFocusManager::EnforceAudioFocusAbandon(mojom::AudioFocusType type) {
case mojom::AudioFocusType::kGainTransient:
// The abandoned session suspended all the media sessions but we should
// start playing the top one again as the abandoned media was transient.
top->session()->Resume(mojom::MediaSession::SuspendType::kSystem);
// This will also apply to any sessions that have the same group_id as the
// new top most session.
for (auto& session : audio_focus_stack_) {
if (session->group_id() != top->group_id())
continue;
session->session()->Resume(mojom::MediaSession::SuspendType::kSystem);
}
break;
case mojom::AudioFocusType::kGainTransientMayDuck:
// The abandoned session ducked all the media sessions so we should unduck
......
......@@ -45,6 +45,12 @@ class AudioFocusManager : public mojom::AudioFocusManager,
mojom::MediaSessionInfoPtr session_info,
mojom::AudioFocusType type,
RequestAudioFocusCallback callback) override;
void RequestGroupedAudioFocus(mojom::AudioFocusRequestClientRequest request,
mojom::MediaSessionPtr media_session,
mojom::MediaSessionInfoPtr session_info,
mojom::AudioFocusType type,
const base::UnguessableToken& group_id,
RequestAudioFocusCallback callback) override;
void GetFocusRequests(GetFocusRequestsCallback callback) override;
void AddObserver(mojom::AudioFocusObserverPtr observer) override;
void SetSourceName(const std::string& name) override;
......@@ -83,7 +89,8 @@ class AudioFocusManager : public mojom::AudioFocusManager,
void RequestAudioFocusInternal(std::unique_ptr<StackRow>,
mojom::AudioFocusType,
base::OnceCallback<void()>);
void EnforceAudioFocusRequest(mojom::AudioFocusType);
void EnforceAudioFocusRequest(mojom::AudioFocusType type,
const base::UnguessableToken& group_id);
void AbandonAudioFocusInternal(RequestId);
void EnforceAudioFocusAbandon(mojom::AudioFocusType);
......
......@@ -99,6 +99,14 @@ class AudioFocusManagerTest : public testing::TestWithParam<bool> {
audio_focus_type);
}
AudioFocusManager::RequestId RequestGroupedAudioFocus(
test::MockMediaSession* session,
mojom::AudioFocusType audio_focus_type,
const base::UnguessableToken& group_id) {
return session->RequestGroupedAudioFocusFromService(
audio_focus_ptr_, audio_focus_type, group_id);
}
mojom::MediaSessionDebugInfoPtr GetDebugInfo(
AudioFocusManager::RequestId request_id) {
mojom::MediaSessionDebugInfoPtr result;
......@@ -1034,4 +1042,128 @@ TEST_P(AudioFocusManagerTest, ObserverActiveSessionChanged) {
}
}
TEST_P(AudioFocusManagerTest, AudioFocusGrouping_AllowDucking) {
test::MockMediaSession media_session_1;
test::MockMediaSession media_session_2;
test::MockMediaSession media_session_3;
base::UnguessableToken group_id = base::UnguessableToken::Create();
RequestGroupedAudioFocus(&media_session_1, mojom::AudioFocusType::kGain,
group_id);
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_1));
RequestAudioFocus(&media_session_2,
mojom::AudioFocusType::kGainTransientMayDuck);
EXPECT_EQ(GetStateFromParam(mojom::MediaSessionInfo::SessionState::kDucking),
GetState(&media_session_1));
RequestGroupedAudioFocus(&media_session_3, mojom::AudioFocusType::kGain,
group_id);
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_3));
EXPECT_EQ(GetStateFromParam(mojom::MediaSessionInfo::SessionState::kDucking),
GetState(&media_session_1));
}
TEST_P(AudioFocusManagerTest, AudioFocusGrouping_TransientResume) {
test::MockMediaSession media_session_1;
test::MockMediaSession media_session_2;
test::MockMediaSession media_session_3;
test::MockMediaSession media_session_4;
base::UnguessableToken group_id = base::UnguessableToken::Create();
RequestGroupedAudioFocus(&media_session_1, mojom::AudioFocusType::kGain,
group_id);
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_1));
RequestAudioFocus(&media_session_2, mojom::AudioFocusType::kGain);
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_2));
RequestGroupedAudioFocus(&media_session_3, mojom::AudioFocusType::kGain,
group_id);
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_3));
RequestAudioFocus(&media_session_4, mojom::AudioFocusType::kGainTransient);
EXPECT_EQ(
GetStateFromParam(mojom::MediaSessionInfo::SessionState::kSuspended),
GetState(&media_session_1));
EXPECT_EQ(
GetStateFromParam(mojom::MediaSessionInfo::SessionState::kSuspended),
GetState(&media_session_2));
EXPECT_EQ(
GetStateFromParam(mojom::MediaSessionInfo::SessionState::kSuspended),
GetState(&media_session_3));
media_session_4.AbandonAudioFocusFromClient();
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_1));
EXPECT_EQ(
GetStateFromParam(mojom::MediaSessionInfo::SessionState::kSuspended),
GetState(&media_session_2));
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_3));
}
TEST_P(AudioFocusManagerTest, AudioFocusGrouping_DoNotSuspendSameGroup) {
test::MockMediaSession media_session_1;
test::MockMediaSession media_session_2;
base::UnguessableToken group_id = base::UnguessableToken::Create();
RequestGroupedAudioFocus(&media_session_1, mojom::AudioFocusType::kGain,
group_id);
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_1));
RequestGroupedAudioFocus(&media_session_2, mojom::AudioFocusType::kGain,
group_id);
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_1));
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_2));
}
TEST_P(AudioFocusManagerTest, AudioFocusGrouping_DuckSameGroup) {
test::MockMediaSession media_session_1;
test::MockMediaSession media_session_2;
base::UnguessableToken group_id = base::UnguessableToken::Create();
RequestGroupedAudioFocus(&media_session_1, mojom::AudioFocusType::kGain,
group_id);
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_1));
RequestGroupedAudioFocus(
&media_session_2, mojom::AudioFocusType::kGainTransientMayDuck, group_id);
EXPECT_EQ(GetStateFromParam(mojom::MediaSessionInfo::SessionState::kDucking),
GetState(&media_session_1));
}
TEST_P(AudioFocusManagerTest, AudioFocusGrouping_TransientSameGroup) {
test::MockMediaSession media_session_1;
test::MockMediaSession media_session_2;
base::UnguessableToken group_id = base::UnguessableToken::Create();
RequestGroupedAudioFocus(&media_session_1, mojom::AudioFocusType::kGain,
group_id);
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_1));
RequestGroupedAudioFocus(&media_session_2,
mojom::AudioFocusType::kGainTransient, group_id);
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_1));
EXPECT_EQ(mojom::MediaSessionInfo::SessionState::kActive,
GetState(&media_session_2));
}
} // namespace media_session
......@@ -174,6 +174,41 @@ base::UnguessableToken MockMediaSession::RequestAudioFocusFromService(
return GetRequestIdFromClient();
}
base::UnguessableToken MockMediaSession::RequestGroupedAudioFocusFromService(
mojom::AudioFocusManagerPtr& service,
mojom::AudioFocusType audio_focus_type,
const base::UnguessableToken& group_id) {
bool result;
base::OnceClosure callback =
base::BindOnce([](bool* out_result) { *out_result = true; }, &result);
if (afr_client_.is_bound()) {
// Request audio focus through the existing request.
afr_client_->RequestAudioFocus(GetMediaSessionInfoSync(), audio_focus_type,
std::move(callback));
afr_client_.FlushForTesting();
} else {
// Build a new audio focus request.
mojom::MediaSessionPtr media_session;
bindings_.AddBinding(this, mojo::MakeRequest(&media_session));
service->RequestGroupedAudioFocus(
mojo::MakeRequest(&afr_client_), std::move(media_session),
GetMediaSessionInfoSync(), audio_focus_type, group_id,
std::move(callback));
service.FlushForTesting();
}
// If the audio focus was granted then we should set the session state to
// active.
if (result)
SetState(mojom::MediaSessionInfo::SessionState::kActive);
return GetRequestIdFromClient();
}
mojom::MediaSessionInfo::SessionState MockMediaSession::GetState() const {
return GetMediaSessionInfoSync()->state;
}
......
......@@ -83,6 +83,11 @@ class COMPONENT_EXPORT(MEDIA_SESSION_TEST_SUPPORT_CPP) MockMediaSession
mojom::AudioFocusManagerPtr&,
mojom::AudioFocusType);
base::UnguessableToken RequestGroupedAudioFocusFromService(
mojom::AudioFocusManagerPtr& service,
mojom::AudioFocusType audio_focus_type,
const base::UnguessableToken& group_id);
mojom::MediaSessionInfo::SessionState GetState() const;
mojom::AudioFocusRequestClient* audio_focus_request() const {
......
......@@ -7,7 +7,7 @@ module media_session.mojom;
import "mojo/public/mojom/base/unguessable_token.mojom";
import "services/media_session/public/mojom/media_session.mojom";
// Next MinVersion: 4
// Next MinVersion: 5
// These are the different types of audio focus that can be requested.
[Extensible]
......@@ -72,16 +72,26 @@ interface AudioFocusRequestClient {
};
// Controls audio focus across the entire system.
// Next Method ID: 4
// Next Method ID: 5
interface AudioFocusManager {
// Requests audio focus with |type| for the |media_session| with
// |session_info|. Media sessions should provide a |request| that will
// provide an AudioFocusRequestClient that can be used to control this
// request. The callback will resolve when audio focus has been granted.
RequestAudioFocus@0(AudioFocusRequestClient& client,
MediaSession media_session,
MediaSessionInfo session_info,
AudioFocusType type) => ();
MediaSession media_session,
MediaSessionInfo session_info,
AudioFocusType type) => ();
// Requests audio focus as above but with a |group_id| that is used for
// grouping sessions together. This is when a group of media sessions
// will share audio focus.
[MinVersion=4] RequestGroupedAudioFocus@4(
AudioFocusRequestClient& client,
MediaSession media_session,
MediaSessionInfo session_info,
AudioFocusType type,
mojo_base.mojom.UnguessableToken group_id) => ();
// Gets all the information about all |MediaSessions| that have requested
// audio focus and their current requested type.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment