1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/media/router/providers/cast/cast_media_controller.h"
6
7 #include "base/json/json_reader.h"
8 #include "chrome/browser/media/router/providers/cast/app_activity.h"
9 #include "chrome/browser/media/router/providers/cast/mock_app_activity.h"
10 #include "chrome/browser/media/router/test/media_router_mojo_test.h"
11 #include "components/media_router/common/media_route.h"
12 #include "content/public/test/browser_task_environment.h"
13 #include "mojo/public/cpp/bindings/remote.h"
14 #include "testing/gmock/include/gmock/gmock.h"
15 #include "testing/gtest/include/gtest/gtest.h"
16
17 using base::Value;
18 using testing::_;
19 using testing::Invoke;
20 using testing::WithArg;
21
22 namespace media_router {
23
24 namespace {
25
26 constexpr char kSessionId[] = "sessionId123";
27 constexpr int kMediaSessionId = 12345678;
28
29 // Verifies that the session ID is |kSessionId|.
VerifySessionId(const Value & v2_message_body)30 void VerifySessionId(const Value& v2_message_body) {
31 const Value* sessionId = v2_message_body.FindKey("sessionId");
32 ASSERT_TRUE(sessionId);
33 ASSERT_TRUE(sessionId->is_string());
34 EXPECT_EQ(kSessionId, sessionId->GetString());
35 }
36
37 // Verifies that the media session ID is |kMediaSessionId|.
VerifySessionAndMediaSessionIds(const Value & v2_message_body)38 void VerifySessionAndMediaSessionIds(const Value& v2_message_body) {
39 VerifySessionId(v2_message_body);
40 const Value* mediaSessionId = v2_message_body.FindKey("mediaSessionId");
41 ASSERT_TRUE(mediaSessionId);
42 ASSERT_TRUE(mediaSessionId->is_int());
43 EXPECT_EQ(kMediaSessionId, mediaSessionId->GetInt());
44 }
45
GetPlayerStateValue(const mojom::MediaStatus & status)46 Value GetPlayerStateValue(const mojom::MediaStatus& status) {
47 switch (status.play_state) {
48 case mojom::MediaStatus::PlayState::PLAYING:
49 return Value("PLAYING");
50 case mojom::MediaStatus::PlayState::PAUSED:
51 return Value("PAUSED");
52 case mojom::MediaStatus::PlayState::BUFFERING:
53 return Value("BUFFERING");
54 }
55 }
56
GetSupportedMediaCommandsValue(const mojom::MediaStatus & status)57 Value GetSupportedMediaCommandsValue(const mojom::MediaStatus& status) {
58 base::ListValue commands;
59 // |can_set_volume| and |can_mute| are not used, because the receiver volume
60 // is used instead.
61 if (status.can_play_pause)
62 commands.AppendString("pause");
63 if (status.can_seek)
64 commands.AppendString("seek");
65 if (status.can_skip_to_next_track)
66 commands.AppendString("queue_next");
67 if (status.can_skip_to_previous_track)
68 commands.AppendString("queue_next");
69 return std::move(commands);
70 }
71
CreateImagesValue(const std::vector<mojom::MediaImagePtr> & images)72 Value CreateImagesValue(const std::vector<mojom::MediaImagePtr>& images) {
73 Value image_list(Value::Type::LIST);
74 for (const mojom::MediaImagePtr& image : images) {
75 Value image_value(Value::Type::DICTIONARY);
76 image_value.SetStringKey("url", image->url.spec());
77 // CastMediaController should be able to handle images that are missing the
78 // width or the height.
79 if (image->size) {
80 image_value.SetIntKey("width", image->size->width());
81 image_value.SetIntKey("height", image->size->height());
82 }
83 image_list.Append(std::move(image_value));
84 }
85 return image_list;
86 }
87
CreateMediaStatus(const mojom::MediaStatus & status)88 Value CreateMediaStatus(const mojom::MediaStatus& status) {
89 Value status_value(Value::Type::DICTIONARY);
90 status_value.SetKey("mediaSessionId", Value(kMediaSessionId));
91 status_value.SetKey("media", Value(Value::Type::DICTIONARY));
92 status_value.SetPath("media.metadata", Value(Value::Type::DICTIONARY));
93 status_value.SetPath("media.metadata.title", Value(status.title));
94 status_value.SetPath("media.metadata.images",
95 CreateImagesValue(status.images));
96 status_value.SetPath("media.duration", Value(status.duration.InSecondsF()));
97 status_value.SetPath("currentTime", Value(status.current_time.InSecondsF()));
98 status_value.SetPath("playerState", GetPlayerStateValue(status));
99 status_value.SetPath("supportedMediaCommands",
100 GetSupportedMediaCommandsValue(status));
101 status_value.SetPath("volume", Value(Value::Type::DICTIONARY));
102 status_value.SetPath("volume.level", Value(status.volume));
103 status_value.SetPath("volume.muted", Value(status.is_muted));
104
105 return status_value;
106 }
107
CreateSampleMediaStatus()108 mojom::MediaStatusPtr CreateSampleMediaStatus() {
109 mojom::MediaStatusPtr status = mojom::MediaStatus::New();
110 status->title = "media title";
111 status->can_play_pause = true;
112 status->can_mute = true;
113 status->can_set_volume = false;
114 status->can_seek = false;
115 status->can_skip_to_next_track = true;
116 status->can_skip_to_previous_track = false;
117 status->is_muted = false;
118 status->volume = 0.7;
119 status->play_state = mojom::MediaStatus::PlayState::BUFFERING;
120 status->duration = base::TimeDelta::FromSeconds(30);
121 status->current_time = base::TimeDelta::FromSeconds(12);
122 return status;
123 }
124
CreateSampleSession()125 std::unique_ptr<CastSession> CreateSampleSession() {
126 MediaSinkInternal sink(MediaSink("sinkId123", "name", SinkIconType::CAST),
127 CastSinkExtraData());
128 base::Optional<Value> receiver_status = base::JSONReader::Read(R"({
129 "applications": [{
130 "appId": "ABCD1234",
131 "displayName": "My App",
132 "sessionId": "sessionId123",
133 "transportId": "transportId123",
134 "namespaces": [{"name": "urn:x-cast:com.example"}]
135 }],
136 "volume": {
137 "controlType": "attenuation",
138 "level": 0.8,
139 "muted": false,
140 "stepInterval": 0.1
141 }
142 })");
143 return CastSession::From(sink, receiver_status.value());
144 }
145
146 } // namespace
147
148 class CastMediaControllerTest : public testing::Test {
149 public:
CastMediaControllerTest()150 CastMediaControllerTest() : activity_(MediaRoute(), "appId123") {}
151 ~CastMediaControllerTest() override = default;
152
SetUp()153 void SetUp() override {
154 testing::Test::SetUp();
155
156 mojo::PendingRemote<mojom::MediaStatusObserver> mojo_status_observer;
157 status_observer_ = std::make_unique<MockMediaStatusObserver>(
158 mojo_status_observer.InitWithNewPipeAndPassReceiver());
159 controller_ = std::make_unique<CastMediaController>(
160 &activity_, mojo_controller_.BindNewPipeAndPassReceiver(),
161 std::move(mojo_status_observer));
162 }
163
TearDown()164 void TearDown() override {
165 VerifyAndClearExpectations();
166 testing::Test::TearDown();
167 }
168
VerifyAndClearExpectations()169 void VerifyAndClearExpectations() {
170 base::RunLoop().RunUntilIdle();
171 testing::Mock::VerifyAndClearExpectations(&activity_);
172 testing::Mock::VerifyAndClearExpectations(status_observer_.get());
173 }
174
SetSessionAndMediaStatus()175 void SetSessionAndMediaStatus() {
176 controller_->SetSession(*CreateSampleSession());
177 SetMediaStatus(*CreateSampleMediaStatus());
178 }
179
SetMediaStatus(const mojom::MediaStatus & status)180 void SetMediaStatus(const mojom::MediaStatus& status) {
181 SetMediaStatus(CreateMediaStatus(status));
182 }
183
SetMediaStatus(Value status_value)184 void SetMediaStatus(Value status_value) {
185 Value status_list(Value::Type::DICTIONARY);
186 status_list.SetKey("status", Value(Value::Type::LIST));
187 status_list.FindKey("status")->Append(std::move(status_value));
188
189 controller_->SetMediaStatus(status_list);
190 }
191
192 protected:
193 content::BrowserTaskEnvironment task_environment_;
194 MockAppActivity activity_;
195 std::unique_ptr<CastMediaController> controller_;
196 mojo::Remote<mojom::MediaController> mojo_controller_;
197 std::unique_ptr<MockMediaStatusObserver> status_observer_;
198 };
199
TEST_F(CastMediaControllerTest,SendPlayRequest)200 TEST_F(CastMediaControllerTest, SendPlayRequest) {
201 SetSessionAndMediaStatus();
202 EXPECT_CALL(activity_, SendMediaRequestToReceiver(_))
203 .WillOnce([](const CastInternalMessage& cast_message) {
204 EXPECT_EQ("PLAY", cast_message.v2_message_type());
205 VerifySessionAndMediaSessionIds(cast_message.v2_message_body());
206 return 0;
207 });
208 mojo_controller_->Play();
209 }
210
TEST_F(CastMediaControllerTest,SendPauseRequest)211 TEST_F(CastMediaControllerTest, SendPauseRequest) {
212 SetSessionAndMediaStatus();
213 EXPECT_CALL(activity_, SendMediaRequestToReceiver(_))
214 .WillOnce([](const CastInternalMessage& cast_message) {
215 EXPECT_EQ("PAUSE", cast_message.v2_message_type());
216 VerifySessionAndMediaSessionIds(cast_message.v2_message_body());
217 return 0;
218 });
219 mojo_controller_->Pause();
220 }
221
TEST_F(CastMediaControllerTest,SendMuteRequests)222 TEST_F(CastMediaControllerTest, SendMuteRequests) {
223 SetSessionAndMediaStatus();
224 EXPECT_CALL(activity_, SendSetVolumeRequestToReceiver(_, _))
225 .WillOnce(WithArg<0>([](const CastInternalMessage& cast_message) {
226 EXPECT_EQ("SET_VOLUME", cast_message.v2_message_type());
227 EXPECT_TRUE(
228 cast_message.v2_message_body().FindPath("volume.muted")->GetBool());
229 VerifySessionId(cast_message.v2_message_body());
230 return 0;
231 }));
232 mojo_controller_->SetMute(true);
233 VerifyAndClearExpectations();
234
235 EXPECT_CALL(activity_, SendSetVolumeRequestToReceiver(_, _))
236 .WillOnce(WithArg<0>([](const CastInternalMessage& cast_message) {
237 EXPECT_EQ("SET_VOLUME", cast_message.v2_message_type());
238 EXPECT_FALSE(
239 cast_message.v2_message_body().FindPath("volume.muted")->GetBool());
240 VerifySessionId(cast_message.v2_message_body());
241 return 0;
242 }));
243 mojo_controller_->SetMute(false);
244 }
245
TEST_F(CastMediaControllerTest,SendVolumeRequest)246 TEST_F(CastMediaControllerTest, SendVolumeRequest) {
247 SetSessionAndMediaStatus();
248 EXPECT_CALL(activity_, SendSetVolumeRequestToReceiver(_, _))
249 .WillOnce(WithArg<0>([&](const CastInternalMessage& cast_message) {
250 EXPECT_EQ("SET_VOLUME", cast_message.v2_message_type());
251 EXPECT_FLOAT_EQ(0.314, cast_message.v2_message_body()
252 .FindPath("volume.level")
253 ->GetDouble());
254 VerifySessionId(cast_message.v2_message_body());
255 return 0;
256 }));
257 mojo_controller_->SetVolume(0.314);
258 }
259
TEST_F(CastMediaControllerTest,SendSeekRequest)260 TEST_F(CastMediaControllerTest, SendSeekRequest) {
261 SetSessionAndMediaStatus();
262 EXPECT_CALL(activity_, SendMediaRequestToReceiver(_))
263 .WillOnce([&](const CastInternalMessage& cast_message) {
264 EXPECT_EQ("SEEK", cast_message.v2_message_type());
265 EXPECT_DOUBLE_EQ(
266 12.34,
267 cast_message.v2_message_body().FindKey("currentTime")->GetDouble());
268 VerifySessionId(cast_message.v2_message_body());
269 return 0;
270 });
271 mojo_controller_->Seek(base::TimeDelta::FromSecondsD(12.34));
272 }
273
TEST_F(CastMediaControllerTest,SendNextTrackRequest)274 TEST_F(CastMediaControllerTest, SendNextTrackRequest) {
275 SetSessionAndMediaStatus();
276 EXPECT_CALL(activity_, SendMediaRequestToReceiver(_))
277 .WillOnce([](const CastInternalMessage& cast_message) {
278 EXPECT_EQ("QUEUE_UPDATE", cast_message.v2_message_type());
279 EXPECT_EQ(1, cast_message.v2_message_body().FindKey("jump")->GetInt());
280 VerifySessionAndMediaSessionIds(cast_message.v2_message_body());
281 return 0;
282 });
283 mojo_controller_->NextTrack();
284 }
285
TEST_F(CastMediaControllerTest,SendPreviousTrackRequest)286 TEST_F(CastMediaControllerTest, SendPreviousTrackRequest) {
287 SetSessionAndMediaStatus();
288 EXPECT_CALL(activity_, SendMediaRequestToReceiver(_))
289 .WillOnce([](const CastInternalMessage& cast_message) {
290 EXPECT_EQ("QUEUE_UPDATE", cast_message.v2_message_type());
291 EXPECT_EQ(-1, cast_message.v2_message_body().FindKey("jump")->GetInt());
292 VerifySessionAndMediaSessionIds(cast_message.v2_message_body());
293 return 0;
294 });
295 mojo_controller_->PreviousTrack();
296 }
297
TEST_F(CastMediaControllerTest,UpdateMediaStatus)298 TEST_F(CastMediaControllerTest, UpdateMediaStatus) {
299 mojom::MediaStatusPtr expected_status = CreateSampleMediaStatus();
300
301 EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
302 .WillOnce([&](mojom::MediaStatusPtr status) {
303 EXPECT_EQ(expected_status->title, status->title);
304 EXPECT_EQ(expected_status->can_play_pause, status->can_play_pause);
305 EXPECT_EQ(expected_status->can_seek, status->can_seek);
306 EXPECT_EQ(expected_status->can_skip_to_next_track,
307 status->can_skip_to_next_track);
308 EXPECT_EQ(expected_status->can_skip_to_previous_track,
309 status->can_skip_to_previous_track);
310 EXPECT_EQ(expected_status->play_state, status->play_state);
311 EXPECT_EQ(expected_status->duration, status->duration);
312 EXPECT_EQ(expected_status->current_time, status->current_time);
313 });
314 SetMediaStatus(*expected_status);
315 VerifyAndClearExpectations();
316 }
317
TEST_F(CastMediaControllerTest,UpdateMediaStatusWithDoubleDurations)318 TEST_F(CastMediaControllerTest, UpdateMediaStatusWithDoubleDurations) {
319 mojom::MediaStatusPtr expected_status = CreateSampleMediaStatus();
320 expected_status->duration = base::TimeDelta::FromSecondsD(30.5);
321 expected_status->current_time = base::TimeDelta::FromSecondsD(12.9);
322
323 EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
324 .WillOnce([&](mojom::MediaStatusPtr status) {
325 EXPECT_DOUBLE_EQ(expected_status->duration.InSecondsF(),
326 status->duration.InSecondsF());
327 EXPECT_DOUBLE_EQ(expected_status->current_time.InSecondsF(),
328 status->current_time.InSecondsF());
329 });
330 SetMediaStatus(*expected_status);
331 VerifyAndClearExpectations();
332 }
333
TEST_F(CastMediaControllerTest,UpdateMediaImages)334 TEST_F(CastMediaControllerTest, UpdateMediaImages) {
335 mojom::MediaStatusPtr expected_status = CreateSampleMediaStatus();
336 expected_status->images.emplace_back(
337 base::in_place, GURL("https://example.com/1.png"), gfx::Size(123, 456));
338 expected_status->images.emplace_back(
339 base::in_place, GURL("https://example.com/2.png"), gfx::Size(789, 0));
340 const mojom::MediaImage& image1 = *expected_status->images.at(0);
341 const mojom::MediaImage& image2 = *expected_status->images.at(1);
342
343 EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
344 .WillOnce([&](const mojom::MediaStatusPtr& status) {
345 ASSERT_EQ(2u, status->images.size());
346 EXPECT_EQ(image1.url.spec(), status->images.at(0)->url.spec());
347 EXPECT_EQ(image1.size->width(), status->images.at(0)->size->width());
348 EXPECT_EQ(image1.size->height(), status->images.at(0)->size->height());
349 EXPECT_EQ(image2.url.spec(), status->images.at(1)->url.spec());
350 EXPECT_EQ(base::nullopt, status->images.at(1)->size);
351 });
352 SetMediaStatus(*expected_status);
353 VerifyAndClearExpectations();
354 }
355
TEST_F(CastMediaControllerTest,IgnoreInvalidImage)356 TEST_F(CastMediaControllerTest, IgnoreInvalidImage) {
357 // Set one valid image and one invalid image.
358 mojom::MediaStatusPtr expected_status = CreateSampleMediaStatus();
359 expected_status->images.emplace_back(
360 base::in_place, GURL("https://example.com/1.png"), gfx::Size(123, 456));
361 const mojom::MediaImage& valid_image = *expected_status->images.at(0);
362 Value status_value = CreateMediaStatus(*expected_status);
363 status_value.FindListPath("media.metadata.images")->Append("invalid image");
364
365 EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
366 .WillOnce([&](const mojom::MediaStatusPtr& status) {
367 ASSERT_EQ(1u, status->images.size());
368 EXPECT_EQ(valid_image.url.spec(), status->images.at(0)->url.spec());
369 });
370 SetMediaStatus(std::move(status_value));
371 VerifyAndClearExpectations();
372 }
373
TEST_F(CastMediaControllerTest,UpdateVolumeStatus)374 TEST_F(CastMediaControllerTest, UpdateVolumeStatus) {
375 auto session = CreateSampleSession();
376 const float session_volume =
377 session->value().FindPath("receiver.volume.level")->GetDouble();
378 const bool session_muted =
379 session->value().FindPath("receiver.volume.muted")->GetBool();
380 EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
381 .WillOnce([&](mojom::MediaStatusPtr status) {
382 EXPECT_FLOAT_EQ(session_volume, status->volume);
383 EXPECT_EQ(session_muted, status->is_muted);
384 });
385 controller_->SetSession(*session);
386 VerifyAndClearExpectations();
387
388 // The volume info is set in SetSession() rather than SetMediaStatus(), so the
389 // volume info in the latter should be ignored.
390 EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
391 .WillOnce([&](mojom::MediaStatusPtr status) {
392 EXPECT_FLOAT_EQ(session_volume, status->volume);
393 EXPECT_EQ(session_muted, status->is_muted);
394 });
395 mojom::MediaStatusPtr updated_status = CreateSampleMediaStatus();
396 updated_status->volume = 0.3;
397 updated_status->is_muted = true;
398 SetMediaStatus(*updated_status);
399 VerifyAndClearExpectations();
400 }
401
402 } // namespace media_router
403