blob: ce38f89e423a408e55166dab9d8577a8dbac8c3b [file] [log] [blame]
// Copyright 2019 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 "chrome/browser/media/router/providers/cast/cast_media_controller.h"
#include "base/json/json_reader.h"
#include "chrome/browser/media/router/providers/cast/cast_activity_record.h"
#include "chrome/browser/media/router/providers/cast/mock_cast_activity_record.h"
#include "chrome/browser/media/router/test/media_router_mojo_test.h"
#include "chrome/common/media_router/media_route.h"
#include "content/public/test/browser_task_environment.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using base::Value;
using testing::_;
using testing::Invoke;
using testing::WithArg;
namespace media_router {
namespace {
constexpr char kSessionId[] = "sessionId123";
constexpr int kMediaSessionId = 12345678;
// Verifies that the session ID is |kSessionId|.
void VerifySessionId(const Value& v2_message_body) {
const Value* sessionId = v2_message_body.FindKey("sessionId");
ASSERT_TRUE(sessionId);
ASSERT_TRUE(sessionId->is_string());
EXPECT_EQ(kSessionId, sessionId->GetString());
}
// Verifies that the media session ID is |kMediaSessionId|.
void VerifySessionAndMediaSessionIds(const Value& v2_message_body) {
VerifySessionId(v2_message_body);
const Value* mediaSessionId = v2_message_body.FindKey("mediaSessionId");
ASSERT_TRUE(mediaSessionId);
ASSERT_TRUE(mediaSessionId->is_int());
EXPECT_EQ(kMediaSessionId, mediaSessionId->GetInt());
}
Value GetPlayerStateValue(const mojom::MediaStatus& status) {
switch (status.play_state) {
case mojom::MediaStatus::PlayState::PLAYING:
return Value("PLAYING");
case mojom::MediaStatus::PlayState::PAUSED:
return Value("PAUSED");
case mojom::MediaStatus::PlayState::BUFFERING:
return Value("BUFFERING");
}
}
Value GetSupportedMediaCommandsValue(const mojom::MediaStatus& status) {
base::ListValue commands;
// |can_set_volume| and |can_mute| are not used, because the receiver volume
// is used instead.
if (status.can_play_pause)
commands.AppendString("pause");
if (status.can_seek)
commands.AppendString("seek");
if (status.can_skip_to_next_track)
commands.AppendString("queue_next");
if (status.can_skip_to_previous_track)
commands.AppendString("queue_next");
return std::move(commands);
}
Value CreateImagesValue(const std::vector<mojom::MediaImagePtr>& images) {
Value image_list(Value::Type::LIST);
for (const mojom::MediaImagePtr& image : images) {
Value image_value(Value::Type::DICTIONARY);
image_value.SetStringKey("url", image->url.spec());
// CastMediaController should be able to handle images that are missing the
// width or the height.
if (image->size) {
image_value.SetIntKey("width", image->size->width());
image_value.SetIntKey("height", image->size->height());
}
image_list.Append(std::move(image_value));
}
return image_list;
}
mojom::MediaStatusPtr CreateSampleMediaStatus() {
mojom::MediaStatusPtr status = mojom::MediaStatus::New();
status->title = "media title";
status->can_play_pause = true;
status->can_mute = true;
status->can_set_volume = false;
status->can_seek = false;
status->can_skip_to_next_track = true;
status->can_skip_to_previous_track = false;
status->is_muted = false;
status->volume = 0.7;
status->play_state = mojom::MediaStatus::PlayState::BUFFERING;
status->duration = base::TimeDelta::FromSeconds(30);
status->current_time = base::TimeDelta::FromSeconds(12);
return status;
}
std::unique_ptr<CastSession> CreateSampleSession() {
MediaSinkInternal sink(MediaSink("sinkId123", "name", SinkIconType::CAST),
CastSinkExtraData());
base::Optional<Value> receiver_status = base::JSONReader::Read(R"({
"applications": [{
"appId": "ABCD1234",
"displayName": "My App",
"sessionId": "sessionId123",
"transportId": "transportId123",
"namespaces": [{"name": "urn:x-cast:com.example"}]
}],
"volume": {
"controlType": "attenuation",
"level": 0.8,
"muted": false,
"stepInterval": 0.1
}
})");
return CastSession::From(sink, receiver_status.value());
}
} // namespace
class CastMediaControllerTest : public testing::Test {
public:
CastMediaControllerTest() : activity_(MediaRoute(), "appId123") {}
~CastMediaControllerTest() override = default;
void SetUp() override {
testing::Test::SetUp();
mojo::PendingRemote<mojom::MediaStatusObserver> mojo_status_observer;
status_observer_ = std::make_unique<MockMediaStatusObserver>(
mojo_status_observer.InitWithNewPipeAndPassReceiver());
controller_ = std::make_unique<CastMediaController>(
&activity_, mojo_controller_.BindNewPipeAndPassReceiver(),
std::move(mojo_status_observer));
}
void TearDown() override {
VerifyAndClearExpectations();
testing::Test::TearDown();
}
void VerifyAndClearExpectations() {
base::RunLoop().RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&activity_);
testing::Mock::VerifyAndClearExpectations(status_observer_.get());
}
void SetSessionAndMediaStatus() {
controller_->SetSession(*CreateSampleSession());
SetMediaStatus(*CreateSampleMediaStatus());
}
void SetMediaStatus(const mojom::MediaStatus& status) {
Value status_value(Value::Type::DICTIONARY);
status_value.SetKey("mediaSessionId", Value(kMediaSessionId));
status_value.SetKey("media", Value(Value::Type::DICTIONARY));
status_value.SetPath("media.metadata", Value(Value::Type::DICTIONARY));
status_value.SetPath("media.metadata.title", Value(status.title));
status_value.SetPath("media.metadata.images",
CreateImagesValue(status.images));
status_value.SetPath("media.duration", Value(status.duration.InSecondsF()));
status_value.SetPath("currentTime",
Value(status.current_time.InSecondsF()));
status_value.SetPath("playerState", GetPlayerStateValue(status));
status_value.SetPath("supportedMediaCommands",
GetSupportedMediaCommandsValue(status));
status_value.SetPath("volume", Value(Value::Type::DICTIONARY));
status_value.SetPath("volume.level", Value(status.volume));
status_value.SetPath("volume.muted", Value(status.is_muted));
Value status_list(Value::Type::DICTIONARY);
status_list.SetKey("status", Value(Value::Type::LIST));
status_list.FindKey("status")->Append(std::move(status_value));
controller_->SetMediaStatus(std::move(status_list));
}
protected:
content::BrowserTaskEnvironment task_environment_;
MockCastActivityRecord activity_;
std::unique_ptr<CastMediaController> controller_;
mojo::Remote<mojom::MediaController> mojo_controller_;
std::unique_ptr<MockMediaStatusObserver> status_observer_;
};
TEST_F(CastMediaControllerTest, SendPlayRequest) {
SetSessionAndMediaStatus();
EXPECT_CALL(activity_, SendMediaRequestToReceiver(_))
.WillOnce([](const CastInternalMessage& cast_message) {
EXPECT_EQ("PLAY", cast_message.v2_message_type());
VerifySessionAndMediaSessionIds(cast_message.v2_message_body());
return 0;
});
mojo_controller_->Play();
}
TEST_F(CastMediaControllerTest, SendPauseRequest) {
SetSessionAndMediaStatus();
EXPECT_CALL(activity_, SendMediaRequestToReceiver(_))
.WillOnce([](const CastInternalMessage& cast_message) {
EXPECT_EQ("PAUSE", cast_message.v2_message_type());
VerifySessionAndMediaSessionIds(cast_message.v2_message_body());
return 0;
});
mojo_controller_->Pause();
}
TEST_F(CastMediaControllerTest, SendMuteRequests) {
SetSessionAndMediaStatus();
EXPECT_CALL(activity_, SendSetVolumeRequestToReceiver(_, _))
.WillOnce(WithArg<0>([](const CastInternalMessage& cast_message) {
EXPECT_EQ("SET_VOLUME", cast_message.v2_message_type());
EXPECT_TRUE(
cast_message.v2_message_body().FindPath("volume.muted")->GetBool());
VerifySessionId(cast_message.v2_message_body());
return 0;
}));
mojo_controller_->SetMute(true);
VerifyAndClearExpectations();
EXPECT_CALL(activity_, SendSetVolumeRequestToReceiver(_, _))
.WillOnce(WithArg<0>([](const CastInternalMessage& cast_message) {
EXPECT_EQ("SET_VOLUME", cast_message.v2_message_type());
EXPECT_FALSE(
cast_message.v2_message_body().FindPath("volume.muted")->GetBool());
VerifySessionId(cast_message.v2_message_body());
return 0;
}));
mojo_controller_->SetMute(false);
}
TEST_F(CastMediaControllerTest, SendVolumeRequest) {
SetSessionAndMediaStatus();
EXPECT_CALL(activity_, SendSetVolumeRequestToReceiver(_, _))
.WillOnce(WithArg<0>([&](const CastInternalMessage& cast_message) {
EXPECT_EQ("SET_VOLUME", cast_message.v2_message_type());
EXPECT_FLOAT_EQ(0.314, cast_message.v2_message_body()
.FindPath("volume.level")
->GetDouble());
VerifySessionId(cast_message.v2_message_body());
return 0;
}));
mojo_controller_->SetVolume(0.314);
}
TEST_F(CastMediaControllerTest, SendSeekRequest) {
SetSessionAndMediaStatus();
EXPECT_CALL(activity_, SendMediaRequestToReceiver(_))
.WillOnce([&](const CastInternalMessage& cast_message) {
EXPECT_EQ("SEEK", cast_message.v2_message_type());
EXPECT_DOUBLE_EQ(
12.34,
cast_message.v2_message_body().FindKey("currentTime")->GetDouble());
VerifySessionId(cast_message.v2_message_body());
return 0;
});
mojo_controller_->Seek(base::TimeDelta::FromSecondsD(12.34));
}
TEST_F(CastMediaControllerTest, SendNextTrackRequest) {
SetSessionAndMediaStatus();
EXPECT_CALL(activity_, SendMediaRequestToReceiver(_))
.WillOnce([](const CastInternalMessage& cast_message) {
EXPECT_EQ("QUEUE_UPDATE", cast_message.v2_message_type());
EXPECT_EQ(1, cast_message.v2_message_body().FindKey("jump")->GetInt());
VerifySessionAndMediaSessionIds(cast_message.v2_message_body());
return 0;
});
mojo_controller_->NextTrack();
}
TEST_F(CastMediaControllerTest, SendPreviousTrackRequest) {
SetSessionAndMediaStatus();
EXPECT_CALL(activity_, SendMediaRequestToReceiver(_))
.WillOnce([](const CastInternalMessage& cast_message) {
EXPECT_EQ("QUEUE_UPDATE", cast_message.v2_message_type());
EXPECT_EQ(-1, cast_message.v2_message_body().FindKey("jump")->GetInt());
VerifySessionAndMediaSessionIds(cast_message.v2_message_body());
return 0;
});
mojo_controller_->PreviousTrack();
}
TEST_F(CastMediaControllerTest, UpdateMediaStatus) {
mojom::MediaStatusPtr expected_status = CreateSampleMediaStatus();
EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
.WillOnce([&](mojom::MediaStatusPtr status) {
EXPECT_EQ(expected_status->title, status->title);
EXPECT_EQ(expected_status->can_play_pause, status->can_play_pause);
EXPECT_EQ(expected_status->can_seek, status->can_seek);
EXPECT_EQ(expected_status->can_skip_to_next_track,
status->can_skip_to_next_track);
EXPECT_EQ(expected_status->can_skip_to_previous_track,
status->can_skip_to_previous_track);
EXPECT_EQ(expected_status->play_state, status->play_state);
EXPECT_EQ(expected_status->duration, status->duration);
EXPECT_EQ(expected_status->current_time, status->current_time);
});
SetMediaStatus(*expected_status);
VerifyAndClearExpectations();
}
TEST_F(CastMediaControllerTest, UpdateMediaStatusWithDoubleDurations) {
mojom::MediaStatusPtr expected_status = CreateSampleMediaStatus();
expected_status->duration = base::TimeDelta::FromSecondsD(30.5);
expected_status->current_time = base::TimeDelta::FromSecondsD(12.9);
EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
.WillOnce([&](mojom::MediaStatusPtr status) {
EXPECT_DOUBLE_EQ(expected_status->duration.InSecondsF(),
status->duration.InSecondsF());
EXPECT_DOUBLE_EQ(expected_status->current_time.InSecondsF(),
status->current_time.InSecondsF());
});
SetMediaStatus(*expected_status);
VerifyAndClearExpectations();
}
TEST_F(CastMediaControllerTest, UpdateMediaImages) {
mojom::MediaStatusPtr expected_status = CreateSampleMediaStatus();
expected_status->images.emplace_back(
base::in_place, GURL("https://example.com/1.png"), gfx::Size(123, 456));
expected_status->images.emplace_back(
base::in_place, GURL("https://example.com/2.png"), gfx::Size(789, 0));
const mojom::MediaImage& image1 = *expected_status->images.at(0);
const mojom::MediaImage& image2 = *expected_status->images.at(1);
EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
.WillOnce([&](const mojom::MediaStatusPtr& status) {
ASSERT_EQ(2u, status->images.size());
EXPECT_EQ(image1.url.spec(), status->images.at(0)->url.spec());
EXPECT_EQ(image1.size->width(), status->images.at(0)->size->width());
EXPECT_EQ(image1.size->height(), status->images.at(0)->size->height());
EXPECT_EQ(image2.url.spec(), status->images.at(1)->url.spec());
EXPECT_EQ(base::nullopt, status->images.at(1)->size);
});
SetMediaStatus(*expected_status);
VerifyAndClearExpectations();
}
TEST_F(CastMediaControllerTest, UpdateVolumeStatus) {
auto session = CreateSampleSession();
const float session_volume =
session->value().FindPath("receiver.volume.level")->GetDouble();
const bool session_muted =
session->value().FindPath("receiver.volume.muted")->GetBool();
EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
.WillOnce([&](mojom::MediaStatusPtr status) {
EXPECT_FLOAT_EQ(session_volume, status->volume);
EXPECT_EQ(session_muted, status->is_muted);
});
controller_->SetSession(*session);
VerifyAndClearExpectations();
// The volume info is set in SetSession() rather than SetMediaStatus(), so the
// volume info in the latter should be ignored.
EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
.WillOnce([&](mojom::MediaStatusPtr status) {
EXPECT_FLOAT_EQ(session_volume, status->volume);
EXPECT_EQ(session_muted, status->is_muted);
});
mojom::MediaStatusPtr updated_status = CreateSampleMediaStatus();
updated_status->volume = 0.3;
updated_status->is_muted = true;
SetMediaStatus(*updated_status);
VerifyAndClearExpectations();
}
} // namespace media_router