#include "services/media_session/public/cpp/test/mock_media_session.h"
#include <algorithm>
#include <utility>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
namespace media_session {
namespace test {
namespace {
bool IsPositionEqual(const MediaPosition& p1, const MediaPosition& p2) {
if (p1.duration() != p2.duration() ||
p1.playback_rate() != p2.playback_rate()) {
return false;
}
base::TimeTicks now = base::TimeTicks::Now();
if (p1.GetPositionAtTime(now) == p2.GetPositionAtTime(now))
return true;
return p1.GetPositionAtTime(p1.last_updated_time()) ==
p2.GetPositionAtTime(p2.last_updated_time());
}
bool IsPositionGreaterOrEqual(const MediaPosition& p1,
const MediaPosition& p2) {
if (p1.duration() != p2.duration() ||
p1.playback_rate() != p2.playback_rate()) {
return false;
}
base::TimeTicks now = base::TimeTicks::Now();
if (p1.GetPositionAtTime(now) >= p2.GetPositionAtTime(now))
return true;
return p1.GetPositionAtTime(p1.last_updated_time()) >=
p2.GetPositionAtTime(p2.last_updated_time());
}
}
MockMediaSessionMojoObserver::MockMediaSessionMojoObserver(
mojom::MediaSession& media_session) {
media_session.AddObserver(receiver_.BindNewPipeAndPassRemote());
}
MockMediaSessionMojoObserver::~MockMediaSessionMojoObserver() = default;
void MockMediaSessionMojoObserver::MediaSessionInfoChanged(
mojom::MediaSessionInfoPtr session) {
session_info_ = std::move(session);
if (expected_controllable_.has_value() &&
expected_controllable_ == session_info_->is_controllable) {
QuitWaitingIfNeeded();
expected_controllable_.reset();
} else if (expected_hide_metadata_.has_value() &&
expected_hide_metadata_ == session_info_->hide_metadata) {
QuitWaitingIfNeeded();
expected_hide_metadata_.reset();
} else if (expected_meets_visibility_threshold_.has_value() &&
expected_meets_visibility_threshold_ ==
session_info_->meets_visibility_threshold) {
QuitWaitingIfNeeded();
expected_meets_visibility_threshold_.reset();
} else {
if (wanted_state_ == session_info_->state ||
session_info_->playback_state == wanted_playback_state_ ||
session_info_->microphone_state == wanted_microphone_state_ ||
session_info_->camera_state == wanted_camera_state_ ||
(wanted_audio_video_states_ &&
std::ranges::is_permutation(*session_info_->audio_video_states,
*wanted_audio_video_states_))) {
QuitWaitingIfNeeded();
}
}
}
void MockMediaSessionMojoObserver::MediaSessionMetadataChanged(
const std::optional<MediaMetadata>& metadata) {
session_metadata_ = metadata;
if (expected_metadata_.has_value() && expected_metadata_ == metadata) {
QuitWaitingIfNeeded();
expected_metadata_.reset();
} else if (waiting_for_empty_metadata_ &&
(!metadata.has_value() || metadata->IsEmpty())) {
QuitWaitingIfNeeded();
waiting_for_empty_metadata_ = false;
}
}
void MockMediaSessionMojoObserver::MediaSessionActionsChanged(
const std::vector<mojom::MediaSessionAction>& actions) {
session_actions_ =
std::set<mojom::MediaSessionAction>(actions.begin(), actions.end());
if (expected_actions_.has_value() && expected_actions_ == session_actions_) {
QuitWaitingIfNeeded();
expected_actions_.reset();
}
}
void MockMediaSessionMojoObserver::MediaSessionImagesChanged(
const base::flat_map<mojom::MediaSessionImageType, std::vector<MediaImage>>&
images) {
session_images_ = images;
if (expected_images_of_type_.has_value()) {
auto type = expected_images_of_type_->first;
auto expected_images = expected_images_of_type_->second;
auto it = session_images_->find(type);
if (it != session_images_->end() && it->second == expected_images) {
QuitWaitingIfNeeded();
expected_images_of_type_.reset();
}
}
}
void MockMediaSessionMojoObserver::MediaSessionPositionChanged(
const std::optional<media_session::MediaPosition>& position) {
session_position_ = position;
if (position.has_value() && expected_position_.has_value() &&
IsPositionEqual(*position, *expected_position_)) {
QuitWaitingIfNeeded();
expected_position_.reset();
} else if (position.has_value() && minimum_expected_position_.has_value() &&
IsPositionGreaterOrEqual(*position, *minimum_expected_position_)) {
QuitWaitingIfNeeded();
minimum_expected_position_.reset();
} else if (waiting_for_empty_position_ && !position.has_value()) {
QuitWaitingIfNeeded();
waiting_for_empty_position_ = false;
}
}
void MockMediaSessionMojoObserver::WaitForState(
mojom::MediaSessionInfo::SessionState wanted_state) {
if (session_info_ && session_info_->state == wanted_state)
return;
wanted_state_ = wanted_state;
StartWaiting();
}
void MockMediaSessionMojoObserver::WaitForPlaybackState(
mojom::MediaPlaybackState wanted_state) {
if (session_info_ && session_info_->playback_state == wanted_state)
return;
wanted_playback_state_ = wanted_state;
StartWaiting();
}
void MockMediaSessionMojoObserver::WaitForMicrophoneState(
mojom::MicrophoneState wanted_state) {
if (session_info_ && session_info_->microphone_state == wanted_state)
return;
wanted_microphone_state_ = wanted_state;
StartWaiting();
wanted_microphone_state_.reset();
}
void MockMediaSessionMojoObserver::WaitForCameraState(
mojom::CameraState wanted_state) {
if (session_info_ && session_info_->camera_state == wanted_state)
return;
wanted_camera_state_ = wanted_state;
StartWaiting();
wanted_camera_state_.reset();
}
void MockMediaSessionMojoObserver::WaitForAudioVideoStates(
const std::vector<mojom::MediaAudioVideoState>& wanted_states) {
if (session_info_ && std::ranges::is_permutation(
*session_info_->audio_video_states, wanted_states)) {
return;
}
wanted_audio_video_states_ = wanted_states;
StartWaiting();
}
void MockMediaSessionMojoObserver::WaitForControllable(bool is_controllable) {
if (session_info_ && session_info_->is_controllable == is_controllable)
return;
expected_controllable_ = is_controllable;
StartWaiting();
}
void MockMediaSessionMojoObserver::WaitForExpectedHideMetadata(
bool hide_metadata) {
if (session_info_ && session_info_->hide_metadata == hide_metadata) {
return;
}
expected_hide_metadata_ = hide_metadata;
StartWaiting();
}
void MockMediaSessionMojoObserver::WaitForEmptyMetadata() {
if (!session_metadata_.has_value() || !session_metadata_->has_value()) {
return;
}
waiting_for_empty_metadata_ = true;
StartWaiting();
}
void MockMediaSessionMojoObserver::WaitForExpectedMetadata(
const MediaMetadata& metadata) {
if (session_metadata_.has_value() && session_metadata_ == metadata)
return;
expected_metadata_ = metadata;
StartWaiting();
}
void MockMediaSessionMojoObserver::WaitForEmptyActions() {
WaitForExpectedActions(std::set<mojom::MediaSessionAction>());
}
void MockMediaSessionMojoObserver::WaitForExpectedActions(
const std::set<mojom::MediaSessionAction>& actions) {
if (session_actions_.has_value() && session_actions_ == actions)
return;
expected_actions_ = actions;
StartWaiting();
}
void MockMediaSessionMojoObserver::WaitForExpectedImagesOfType(
mojom::MediaSessionImageType type,
const std::vector<MediaImage>& images) {
if (session_images_.has_value()) {
auto it = session_images_->find(type);
if (it != session_images_->end() && it->second == images)
return;
}
expected_images_of_type_ = std::make_pair(type, images);
StartWaiting();
}
void MockMediaSessionMojoObserver::WaitForEmptyPosition() {
if (session_position_.has_value() && !session_position_->has_value())
return;
waiting_for_empty_position_ = true;
StartWaiting();
}
void MockMediaSessionMojoObserver::WaitForExpectedPosition(
const MediaPosition& position) {
if (session_position_.has_value() && session_position_->has_value() &&
IsPositionEqual(*session_position_.value(), position)) {
return;
}
expected_position_ = position;
StartWaiting();
}
base::TimeDelta MockMediaSessionMojoObserver::WaitForExpectedPositionAtLeast(
const MediaPosition& position) {
if (session_position_.has_value() && session_position_->has_value() &&
IsPositionGreaterOrEqual(*session_position_.value(), position)) {
return position.GetPositionAtTime(position.last_updated_time());
}
minimum_expected_position_ = position;
StartWaiting();
return (*session_position_)
->GetPositionAtTime((*session_position_)->last_updated_time());
}
bool MockMediaSessionMojoObserver::WaitForMeetsVisibilityThreshold(
bool meets_visibility_threshold) {
if (session_info_ &&
session_info_->meets_visibility_threshold == meets_visibility_threshold) {
return meets_visibility_threshold;
}
expected_meets_visibility_threshold_ = meets_visibility_threshold;
StartWaiting();
return meets_visibility_threshold;
}
void MockMediaSessionMojoObserver::StartWaiting() {
CHECK(!run_loop_);
run_loop_ = std::make_unique<base::RunLoop>();
run_loop_->Run();
run_loop_.reset();
}
void MockMediaSessionMojoObserver::QuitWaitingIfNeeded() {
if (run_loop_) {
run_loop_->Quit();
}
}
MockMediaSession::MockMediaSession() = default;
MockMediaSession::MockMediaSession(bool force_duck) : force_duck_(force_duck) {}
MockMediaSession::~MockMediaSession() {}
void MockMediaSession::Suspend(SuspendType suspend_type) {
SetState(mojom::MediaSessionInfo::SessionState::kSuspended);
}
void MockMediaSession::Resume(SuspendType suspend_type) {
SetState(mojom::MediaSessionInfo::SessionState::kActive);
}
void MockMediaSession::StartDucking() {
is_ducking_ = true;
NotifyObservers();
}
void MockMediaSession::StopDucking() {
is_ducking_ = false;
NotifyObservers();
}
void MockMediaSession::GetMediaSessionInfo(
GetMediaSessionInfoCallback callback) {
std::move(callback).Run(GetMediaSessionInfoSync());
}
void MockMediaSession::AddObserver(
mojo::PendingRemote<mojom::MediaSessionObserver> observer) {
++add_observer_count_;
mojo::Remote<mojom::MediaSessionObserver> media_session_observer(
std::move(observer));
media_session_observer->MediaSessionInfoChanged(GetMediaSessionInfoSync());
std::vector<mojom::MediaSessionAction> actions(actions_.begin(),
actions_.end());
media_session_observer->MediaSessionActionsChanged(actions);
media_session_observer->MediaSessionImagesChanged(images_);
observers_.Add(std::move(media_session_observer));
}
void MockMediaSession::GetDebugInfo(GetDebugInfoCallback callback) {
mojom::MediaSessionDebugInfoPtr debug_info(
mojom::MediaSessionDebugInfo::New());
debug_info->name = "name";
debug_info->owner = "owner";
debug_info->state = "state";
std::move(callback).Run(std::move(debug_info));
}
void MockMediaSession::PreviousTrack() {
prev_track_count_++;
}
void MockMediaSession::NextTrack() {
next_track_count_++;
}
void MockMediaSession::SkipAd() {
skip_ad_count_++;
}
void MockMediaSession::Seek(base::TimeDelta seek_time) {
seek_count_++;
}
void MockMediaSession::Stop(SuspendType type) {
SetState(mojom::MediaSessionInfo::SessionState::kInactive);
}
void MockMediaSession::GetMediaImageBitmap(
const MediaImage& image,
int minimum_size_px,
int desired_size_px,
GetMediaImageBitmapCallback callback) {
last_image_src_ = image.src;
SkBitmap bitmap;
bitmap.allocPixels(
SkImageInfo::Make(10, 10, kRGBA_8888_SkColorType, kOpaque_SkAlphaType));
std::move(callback).Run(bitmap);
}
void MockMediaSession::SeekTo(base::TimeDelta seek_time) {
seek_to_count_++;
is_scrubbing_ = false;
}
void MockMediaSession::ScrubTo(base::TimeDelta seek_time) {
is_scrubbing_ = true;
}
void MockMediaSession::EnterPictureInPicture() {
is_in_picture_in_picture_ = true;
NotifyObservers();
}
void MockMediaSession::ExitPictureInPicture() {
is_in_picture_in_picture_ = false;
NotifyObservers();
}
void MockMediaSession::GetVisibility(GetVisibilityCallback callback) {
std::move(callback).Run(false);
}
void MockMediaSession::SetIsControllable(bool value) {
is_controllable_ = value;
NotifyObservers();
}
void MockMediaSession::AbandonAudioFocusFromClient() {
DCHECK(afr_client_.is_bound());
afr_client_->AbandonAudioFocus();
afr_client_.FlushForTesting();
afr_client_.reset();
request_id_ = base::UnguessableToken::Null();
}
base::UnguessableToken MockMediaSession::RequestAudioFocusFromService(
mojo::Remote<mojom::AudioFocusManager>& service,
mojom::AudioFocusType audio_focus_type) {
if (afr_client_.is_bound()) {
RequestAudioFocusFromClient(audio_focus_type);
} else {
DCHECK(request_id_.is_empty());
mojo::PendingRemote<mojom::MediaSession> media_session;
receivers_.Add(this, media_session.InitWithNewPipeAndPassReceiver());
service->RequestAudioFocus(
afr_client_.BindNewPipeAndPassReceiver(), std::move(media_session),
GetMediaSessionInfoSync(), audio_focus_type,
base::BindOnce(
[](base::UnguessableToken* id,
const base::UnguessableToken& received_id) {
*id = received_id;
},
&request_id_));
service.FlushForTesting();
afr_client_.FlushForTesting();
}
DCHECK(!request_id_.is_empty());
SetState(mojom::MediaSessionInfo::SessionState::kActive);
return request_id_;
}
bool MockMediaSession::RequestGroupedAudioFocusFromService(
const base::UnguessableToken& request_id,
mojo::Remote<mojom::AudioFocusManager>& service,
mojom::AudioFocusType audio_focus_type,
const base::UnguessableToken& group_id) {
if (afr_client_.is_bound()) {
RequestAudioFocusFromClient(audio_focus_type);
SetState(mojom::MediaSessionInfo::SessionState::kActive);
return true;
}
DCHECK(request_id_.is_empty());
mojo::PendingRemote<mojom::MediaSession> media_session;
receivers_.Add(this, media_session.InitWithNewPipeAndPassReceiver());
bool success;
service->RequestGroupedAudioFocus(
request_id, afr_client_.BindNewPipeAndPassReceiver(),
std::move(media_session), GetMediaSessionInfoSync(), audio_focus_type,
group_id,
base::BindOnce([](bool* success, bool result) { *success = result; },
&success));
service.FlushForTesting();
afr_client_.FlushForTesting();
if (success) {
request_id_ = request_id;
SetState(mojom::MediaSessionInfo::SessionState::kActive);
} else {
afr_client_.reset();
}
return success;
}
mojom::MediaSessionInfo::SessionState MockMediaSession::GetState() const {
return GetMediaSessionInfoSync()->state;
}
void MockMediaSession::FlushForTesting() {
afr_client_.FlushForTesting();
}
void MockMediaSession::SimulateMetadataChanged(
const std::optional<MediaMetadata>& metadata) {
for (auto& observer : observers_) {
observer->MediaSessionMetadataChanged(metadata);
}
}
void MockMediaSession::SimulatePositionChanged(
const std::optional<MediaPosition>& position) {
for (auto& observer : observers_) {
observer->MediaSessionPositionChanged(position);
}
}
void MockMediaSession::ClearAllImages() {
images_.clear();
for (auto& observer : observers_) {
observer->MediaSessionImagesChanged(this->images_);
}
}
void MockMediaSession::SetImagesOfType(mojom::MediaSessionImageType type,
const std::vector<MediaImage>& images) {
images_.insert_or_assign(type, images);
for (auto& observer : observers_) {
observer->MediaSessionImagesChanged(this->images_);
}
}
void MockMediaSession::EnableAction(mojom::MediaSessionAction action) {
if (base::Contains(actions_, action))
return;
actions_.insert(action);
NotifyActionObservers();
}
void MockMediaSession::DisableAction(mojom::MediaSessionAction action) {
if (!base::Contains(actions_, action))
return;
actions_.erase(action);
NotifyActionObservers();
}
void MockMediaSession::SetState(mojom::MediaSessionInfo::SessionState state) {
state_ = state;
NotifyObservers();
}
void MockMediaSession::NotifyObservers() {
mojom::MediaSessionInfoPtr session_info = GetMediaSessionInfoSync();
if (afr_client_.is_bound()) {
afr_client_->MediaSessionInfoChanged(session_info.Clone());
}
for (auto& observer : observers_) {
observer->MediaSessionInfoChanged(session_info.Clone());
}
}
mojom::MediaSessionInfoPtr MockMediaSession::GetMediaSessionInfoSync() const {
mojom::MediaSessionInfoPtr info(mojom::MediaSessionInfo::New());
info->force_duck = force_duck_;
info->state = state_;
if (is_ducking_)
info->state = mojom::MediaSessionInfo::SessionState::kDucking;
info->playback_state = mojom::MediaPlaybackState::kPaused;
if (state_ == mojom::MediaSessionInfo::SessionState::kActive)
info->playback_state = mojom::MediaPlaybackState::kPlaying;
info->is_controllable = is_controllable_;
info->prefer_stop_for_gain_focus_loss = prefer_stop_;
info->picture_in_picture_state =
(is_in_picture_in_picture_
? mojom::MediaPictureInPictureState::kInPictureInPicture
: mojom::MediaPictureInPictureState::kNotInPictureInPicture);
return info;
}
void MockMediaSession::NotifyActionObservers() {
std::vector<mojom::MediaSessionAction> actions(actions_.begin(),
actions_.end());
for (auto& observer : observers_) {
observer->MediaSessionActionsChanged(actions);
}
}
void MockMediaSession::RequestAudioFocusFromClient(
mojom::AudioFocusType audio_focus_type) {
DCHECK(afr_client_.is_bound());
DCHECK(!request_id_.is_empty());
bool result = false;
afr_client_->RequestAudioFocus(
GetMediaSessionInfoSync(), audio_focus_type,
base::BindOnce([](bool* out_result) { *out_result = true; }, &result));
afr_client_.FlushForTesting();
DCHECK(result);
}
}
}