| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/media_session/audio_focus_manager.h" |
| |
| #include <iterator> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/containers/adapters.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/unguessable_token.h" |
| #include "mojo/public/cpp/bindings/interface_request.h" |
| #include "services/media_session/audio_focus_manager_metrics_helper.h" |
| #include "services/media_session/public/cpp/features.h" |
| #include "services/media_session/public/mojom/audio_focus.mojom.h" |
| |
| namespace media_session { |
| |
| namespace { |
| |
| mojom::EnforcementMode GetDefaultEnforcementMode() { |
| if (base::FeatureList::IsEnabled(features::kAudioFocusEnforcement)) { |
| if (base::FeatureList::IsEnabled(features::kAudioFocusSessionGrouping)) |
| return mojom::EnforcementMode::kSingleGroup; |
| return mojom::EnforcementMode::kSingleSession; |
| } |
| |
| return mojom::EnforcementMode::kNone; |
| } |
| |
| } // namespace |
| |
| class AudioFocusManager::StackRow : public mojom::AudioFocusRequestClient { |
| public: |
| StackRow(AudioFocusManager* owner, |
| mojom::AudioFocusRequestClientRequest request, |
| mojom::MediaSessionPtr session, |
| mojom::MediaSessionInfoPtr session_info, |
| mojom::AudioFocusType audio_focus_type, |
| RequestId id, |
| 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)), |
| audio_focus_type_(audio_focus_type), |
| binding_(this, std::move(request)), |
| owner_(owner) { |
| // Listen for mojo errors. |
| binding_.set_connection_error_handler( |
| base::BindOnce(&AudioFocusManager::StackRow::OnConnectionError, |
| base::Unretained(this))); |
| session_.set_connection_error_handler( |
| base::BindOnce(&AudioFocusManager::StackRow::OnConnectionError, |
| base::Unretained(this))); |
| |
| metrics_helper_.OnRequestAudioFocus( |
| AudioFocusManagerMetricsHelper::AudioFocusRequestSource::kInitial, |
| audio_focus_type); |
| } |
| |
| ~StackRow() override = default; |
| |
| // mojom::AudioFocusRequestClient. |
| void RequestAudioFocus(mojom::MediaSessionInfoPtr session_info, |
| mojom::AudioFocusType type, |
| RequestAudioFocusCallback callback) override { |
| SetSessionInfo(std::move(session_info)); |
| |
| if (IsActive() && owner_->IsSessionOnTopOfAudioFocusStack(id(), type)) { |
| // Early returning if |media_session| is already on top (has focus) and is |
| // active. |
| std::move(callback).Run(); |
| return; |
| } |
| |
| // Remove this StackRow for the audio focus stack. |
| std::unique_ptr<StackRow> row = owner_->RemoveFocusEntryIfPresent(id()); |
| DCHECK(row); |
| |
| owner_->RequestAudioFocusInternal(std::move(row), type, |
| std::move(callback)); |
| |
| metrics_helper_.OnRequestAudioFocus( |
| AudioFocusManagerMetricsHelper::AudioFocusRequestSource::kUpdate, |
| audio_focus_type_); |
| } |
| |
| void AbandonAudioFocus() override { |
| metrics_helper_.OnAbandonAudioFocus( |
| AudioFocusManagerMetricsHelper::AudioFocusAbandonSource::kAPI); |
| |
| owner_->AbandonAudioFocusInternal(id_); |
| } |
| |
| void MediaSessionInfoChanged(mojom::MediaSessionInfoPtr info) override { |
| SetSessionInfo(std::move(info)); |
| } |
| |
| void GetRequestId(GetRequestIdCallback callback) override { |
| std::move(callback).Run(id()); |
| } |
| |
| mojom::MediaSession* session() { return session_.get(); } |
| const mojom::MediaSessionInfoPtr& info() const { return session_info_; } |
| mojom::AudioFocusType audio_focus_type() const { return audio_focus_type_; } |
| |
| void SetAudioFocusType(mojom::AudioFocusType type) { |
| audio_focus_type_ = type; |
| } |
| |
| bool IsActive() const { |
| return session_info_->state == |
| mojom::MediaSessionInfo::SessionState::kActive; |
| } |
| |
| RequestId id() const { return id_; } |
| |
| 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(); |
| request->audio_focus_type = audio_focus_type_; |
| request->request_id = id_; |
| request->source_name = source_name_; |
| return request; |
| } |
| |
| void BindToController(mojom::MediaControllerRequest request) { |
| if (!controller_) { |
| controller_ = std::make_unique<MediaController>(); |
| controller_->SetMediaSession(session_.get()); |
| } |
| |
| controller_->BindToInterface(std::move(request)); |
| } |
| |
| void Suspend(const EnforcementState& state) { |
| DCHECK(!session_info_->force_duck); |
| |
| // In most cases if we stop or suspend we should call the ::Suspend method |
| // on the media session. The only exception is if the session has the |
| // |prefer_stop_for_gain_focus_loss| bit set in which case we should use |
| // ::Stop and ::Suspend respectively. |
| if (state.should_stop && session_info_->prefer_stop_for_gain_focus_loss) { |
| session_->Stop(mojom::MediaSession::SuspendType::kSystem); |
| } else { |
| was_suspended_ = was_suspended_ || state.should_suspend; |
| session_->Suspend(mojom::MediaSession::SuspendType::kSystem); |
| } |
| } |
| |
| void MaybeResume() { |
| DCHECK(!session_info_->force_duck); |
| |
| if (!was_suspended_) |
| return; |
| |
| was_suspended_ = false; |
| session_->Resume(mojom::MediaSession::SuspendType::kSystem); |
| } |
| |
| private: |
| void SetSessionInfo(mojom::MediaSessionInfoPtr session_info) { |
| bool is_controllable_changed = |
| session_info_->is_controllable != session_info->is_controllable; |
| |
| session_info_ = std::move(session_info); |
| |
| if (is_controllable_changed) |
| owner_->MaybeUpdateActiveSession(); |
| } |
| |
| void OnConnectionError() { |
| // Since we have multiple pathways that can call |OnConnectionError| we |
| // should use the |encountered_error_| bit to make sure we abandon focus |
| // just the first time. |
| if (encountered_error_) |
| return; |
| encountered_error_ = true; |
| |
| metrics_helper_.OnAbandonAudioFocus( |
| AudioFocusManagerMetricsHelper::AudioFocusAbandonSource:: |
| kConnectionError); |
| |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::BindOnce(&AudioFocusManager::AbandonAudioFocusInternal, |
| base::Unretained(owner_), id_)); |
| } |
| |
| const RequestId id_; |
| const std::string source_name_; |
| const base::UnguessableToken group_id_; |
| |
| AudioFocusManagerMetricsHelper metrics_helper_; |
| bool encountered_error_ = false; |
| bool was_suspended_ = false; |
| |
| std::unique_ptr<MediaController> controller_; |
| |
| mojom::MediaSessionPtr session_; |
| mojom::MediaSessionInfoPtr session_info_; |
| mojom::AudioFocusType audio_focus_type_; |
| mojo::Binding<mojom::AudioFocusRequestClient> binding_; |
| |
| // Weak pointer to the owning |AudioFocusManager| instance. |
| AudioFocusManager* owner_; |
| |
| DISALLOW_COPY_AND_ASSIGN(StackRow); |
| }; |
| |
| void AudioFocusManager::RequestAudioFocus( |
| mojom::AudioFocusRequestClientRequest request, |
| mojom::MediaSessionPtr media_session, |
| 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(), group_id), |
| type, std::move(callback)); |
| } |
| |
| void AudioFocusManager::GetFocusRequests(GetFocusRequestsCallback callback) { |
| std::vector<mojom::AudioFocusRequestStatePtr> requests; |
| |
| for (const auto& row : audio_focus_stack_) |
| requests.push_back(row->ToAudioFocusRequestState()); |
| |
| std::move(callback).Run(std::move(requests)); |
| } |
| |
| void AudioFocusManager::GetDebugInfoForRequest( |
| const RequestId& request_id, |
| GetDebugInfoForRequestCallback callback) { |
| for (auto& row : audio_focus_stack_) { |
| if (row->id() != request_id) |
| continue; |
| |
| 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; |
| } |
| |
| std::move(callback).Run(mojom::MediaSessionDebugInfo::New()); |
| } |
| |
| void AudioFocusManager::AbandonAudioFocusInternal(RequestId id) { |
| if (audio_focus_stack_.empty()) |
| return; |
| |
| if (audio_focus_stack_.back()->id() != id) { |
| RemoveFocusEntryIfPresent(id); |
| MaybeUpdateActiveSession(); |
| return; |
| } |
| |
| auto row = std::move(audio_focus_stack_.back()); |
| audio_focus_stack_.pop_back(); |
| |
| if (audio_focus_stack_.empty()) { |
| // Notify observers that we lost audio focus. |
| observers_.ForAllPtrs([&row](mojom::AudioFocusObserver* observer) { |
| observer->OnFocusLost(row->ToAudioFocusRequestState()); |
| }); |
| |
| MaybeUpdateActiveSession(); |
| return; |
| } |
| |
| EnforceAudioFocusAbandon(); |
| MaybeUpdateActiveSession(); |
| |
| // Notify observers that we lost audio focus. |
| observers_.ForAllPtrs([&row](mojom::AudioFocusObserver* observer) { |
| observer->OnFocusLost(row->ToAudioFocusRequestState()); |
| }); |
| |
| // Notify observers that the session on top gained focus. |
| StackRow* new_session = audio_focus_stack_.back().get(); |
| observers_.ForAllPtrs([&new_session](mojom::AudioFocusObserver* observer) { |
| observer->OnFocusGained(new_session->ToAudioFocusRequestState()); |
| }); |
| } |
| |
| void AudioFocusManager::AddObserver(mojom::AudioFocusObserverPtr observer) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| observers_.AddPtr(std::move(observer)); |
| } |
| |
| void AudioFocusManager::SetSourceName(const std::string& name) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| bindings_.dispatch_context()->source_name = name; |
| } |
| |
| void AudioFocusManager::SetEnforcementMode(mojom::EnforcementMode mode) { |
| if (mode == mojom::EnforcementMode::kDefault) |
| mode = GetDefaultEnforcementMode(); |
| |
| if (mode == enforcement_mode_) |
| return; |
| |
| enforcement_mode_ = mode; |
| |
| if (audio_focus_stack_.empty()) |
| return; |
| |
| EnforceAudioFocus(); |
| } |
| |
| void AudioFocusManager::CreateActiveMediaController( |
| mojom::MediaControllerRequest request) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| active_media_controller_.BindToInterface(std::move(request)); |
| } |
| |
| void AudioFocusManager::CreateMediaControllerForSession( |
| mojom::MediaControllerRequest request, |
| const base::UnguessableToken& request_id) { |
| for (auto& row : audio_focus_stack_) { |
| if (row->id() != request_id) |
| continue; |
| |
| row->BindToController(std::move(request)); |
| break; |
| } |
| } |
| |
| void AudioFocusManager::BindToInterface( |
| mojom::AudioFocusManagerRequest request) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| bindings_.AddBinding(this, std::move(request), |
| std::make_unique<BindingContext>()); |
| } |
| |
| void AudioFocusManager::BindToDebugInterface( |
| mojom::AudioFocusManagerDebugRequest request) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| debug_bindings_.AddBinding(this, std::move(request)); |
| } |
| |
| void AudioFocusManager::BindToControllerManagerInterface( |
| mojom::MediaControllerManagerRequest request) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_bindings_.AddBinding(this, std::move(request)); |
| } |
| |
| void AudioFocusManager::RequestAudioFocusInternal( |
| std::unique_ptr<StackRow> row, |
| mojom::AudioFocusType type, |
| base::OnceCallback<void()> callback) { |
| row->SetAudioFocusType(type); |
| audio_focus_stack_.push_back(std::move(row)); |
| |
| EnforceAudioFocus(); |
| MaybeUpdateActiveSession(); |
| |
| // Notify observers that we were gained audio focus. |
| mojom::AudioFocusRequestStatePtr session_state = |
| audio_focus_stack_.back()->ToAudioFocusRequestState(); |
| observers_.ForAllPtrs([&session_state](mojom::AudioFocusObserver* observer) { |
| observer->OnFocusGained(session_state.Clone()); |
| }); |
| |
| // We always grant the audio focus request but this may not always be the case |
| // in the future. |
| std::move(callback).Run(); |
| } |
| |
| void AudioFocusManager::EnforceAudioFocusAbandon() { |
| // Allow the top-most MediaSession having force duck to unduck even if |
| // it is not active. |
| if (enforcement_mode_ != mojom::EnforcementMode::kNone) { |
| for (auto iter = audio_focus_stack_.rbegin(); |
| iter != audio_focus_stack_.rend(); ++iter) { |
| if (!(*iter)->info()->force_duck) |
| continue; |
| |
| // TODO(beccahughes): Replace with std::rotate. |
| auto duck_row = std::move(*iter); |
| duck_row->session()->StopDucking(); |
| audio_focus_stack_.erase(std::next(iter).base()); |
| audio_focus_stack_.push_back(std::move(duck_row)); |
| return; |
| } |
| } |
| |
| EnforceAudioFocus(); |
| } |
| |
| void AudioFocusManager::EnforceAudioFocus() { |
| DCHECK_NE(mojom::EnforcementMode::kDefault, enforcement_mode_); |
| if (audio_focus_stack_.empty()) |
| return; |
| |
| EnforcementState state; |
| |
| for (auto& session : base::Reversed(audio_focus_stack_)) { |
| EnforceSingleSession(session.get(), state); |
| |
| // Update the flags based on the audio focus type of this session. |
| switch (session->audio_focus_type()) { |
| case mojom::AudioFocusType::kGain: |
| state.should_stop = true; |
| break; |
| case mojom::AudioFocusType::kGainTransient: |
| state.should_suspend = true; |
| break; |
| case mojom::AudioFocusType::kGainTransientMayDuck: |
| state.should_duck = true; |
| break; |
| } |
| } |
| } |
| |
| void AudioFocusManager::MaybeUpdateActiveSession() { |
| StackRow* active = nullptr; |
| |
| for (auto& row : base::Reversed(audio_focus_stack_)) { |
| if (!row->info()->is_controllable) |
| continue; |
| |
| active = row.get(); |
| break; |
| } |
| |
| if (!active_media_controller_.SetMediaSession(active ? active->session() |
| : nullptr)) { |
| return; |
| } |
| |
| mojom::AudioFocusRequestStatePtr state = |
| active ? active->ToAudioFocusRequestState() : nullptr; |
| |
| // Notify observers that the active media session changed. |
| observers_.ForAllPtrs([&state](mojom::AudioFocusObserver* observer) { |
| observer->OnActiveSessionChanged(state.Clone()); |
| }); |
| } |
| |
| AudioFocusManager::AudioFocusManager() |
| : enforcement_mode_(GetDefaultEnforcementMode()) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| } |
| |
| AudioFocusManager::~AudioFocusManager() = default; |
| |
| std::unique_ptr<AudioFocusManager::StackRow> |
| AudioFocusManager::RemoveFocusEntryIfPresent(RequestId id) { |
| std::unique_ptr<StackRow> row; |
| |
| for (auto iter = audio_focus_stack_.begin(); iter != audio_focus_stack_.end(); |
| ++iter) { |
| if ((*iter)->id() == id) { |
| row.swap((*iter)); |
| audio_focus_stack_.erase(iter); |
| break; |
| } |
| } |
| |
| return row; |
| } |
| |
| const std::string& AudioFocusManager::GetBindingSourceName() const { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| return bindings_.dispatch_context()->source_name; |
| } |
| |
| bool AudioFocusManager::IsSessionOnTopOfAudioFocusStack( |
| RequestId id, |
| mojom::AudioFocusType type) const { |
| return !audio_focus_stack_.empty() && audio_focus_stack_.back()->id() == id && |
| audio_focus_stack_.back()->audio_focus_type() == type; |
| } |
| |
| bool AudioFocusManager::ShouldSessionBeSuspended( |
| const StackRow* session, |
| const EnforcementState& state) const { |
| bool should_suspend_any = state.should_stop || state.should_suspend; |
| |
| switch (enforcement_mode_) { |
| case mojom::EnforcementMode::kSingleSession: |
| return should_suspend_any; |
| case mojom::EnforcementMode::kSingleGroup: |
| return should_suspend_any && |
| session->group_id() != audio_focus_stack_.back()->group_id(); |
| case mojom::EnforcementMode::kNone: |
| return false; |
| case mojom::EnforcementMode::kDefault: |
| NOTIMPLEMENTED(); |
| return false; |
| } |
| } |
| |
| bool AudioFocusManager::ShouldSessionBeDucked( |
| const StackRow* session, |
| const EnforcementState& state) const { |
| switch (enforcement_mode_) { |
| case mojom::EnforcementMode::kSingleSession: |
| case mojom::EnforcementMode::kSingleGroup: |
| if (session->info()->force_duck) |
| return state.should_duck || ShouldSessionBeSuspended(session, state); |
| return state.should_duck; |
| case mojom::EnforcementMode::kNone: |
| return false; |
| case mojom::EnforcementMode::kDefault: |
| NOTIMPLEMENTED(); |
| return false; |
| } |
| } |
| |
| void AudioFocusManager::EnforceSingleSession(StackRow* session, |
| const EnforcementState& state) { |
| if (ShouldSessionBeDucked(session, state)) { |
| session->session()->StartDucking(); |
| } else { |
| session->session()->StopDucking(); |
| } |
| |
| // If the session wants to be ducked instead of suspended we should stop now. |
| if (session->info()->force_duck) |
| return; |
| |
| if (ShouldSessionBeSuspended(session, state)) { |
| session->Suspend(state); |
| } else { |
| session->MaybeResume(); |
| } |
| } |
| |
| } // namespace media_session |