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