|  | // Copyright 2020 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "fuchsia_web/webengine/browser/media_player_impl.h" | 
|  |  | 
|  | #include <fidl/fuchsia.media.sessions2/cpp/fidl.h> | 
|  | #include <lib/async/default.h> | 
|  |  | 
|  | #include "base/fuchsia/fidl_event_handler.h" | 
|  | #include "base/fuchsia/fuchsia_logging.h" | 
|  | #include "base/functional/bind.h" | 
|  | #include "base/run_loop.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "base/test/bind.h" | 
|  | #include "base/test/task_environment.h" | 
|  | #include "base/time/time.h" | 
|  | #include "content/public/browser/media_session.h" | 
|  | #include "content/public/test/mock_media_session.h" | 
|  | #include "mojo/public/cpp/bindings/pending_remote.h" | 
|  | #include "mojo/public/cpp/bindings/remote.h" | 
|  | #include "testing/gmock/include/gmock/gmock.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | class FakeMediaSession : public content::MockMediaSession { | 
|  | public: | 
|  | // content::MediaSession APIs mocked to observe if/when they are called. | 
|  | void SetDuckingVolumeMultiplier(double multiplier) override { ADD_FAILURE(); } | 
|  | void SetAudioFocusGroupId(const base::UnguessableToken& group_id) override { | 
|  | ADD_FAILURE(); | 
|  | } | 
|  |  | 
|  | // content::MediaSession APIs faked to implement testing behaviour. | 
|  | void AddObserver( | 
|  | mojo::PendingRemote<media_session::mojom::MediaSessionObserver> observer) | 
|  | override { | 
|  | if (observer_.is_bound()) { | 
|  | ADD_FAILURE(); | 
|  | } else { | 
|  | observer_.Bind(std::move(observer)); | 
|  | } | 
|  | } | 
|  |  | 
|  | media_session::mojom::MediaSessionObserver* observer() const { | 
|  | return observer_.is_bound() ? observer_.get() : nullptr; | 
|  | } | 
|  |  | 
|  | protected: | 
|  | mojo::Remote<media_session::mojom::MediaSessionObserver> observer_; | 
|  | }; | 
|  |  | 
|  | bool HasFlag(const fuchsia_media_sessions2::PlayerCapabilityFlags bits, | 
|  | const fuchsia_media_sessions2::PlayerCapabilityFlags flag) { | 
|  | return (bits & flag) == flag; | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | class MediaPlayerImplTest : public testing::Test { | 
|  | public: | 
|  | MediaPlayerImplTest() | 
|  | : task_environment_(base::test::TaskEnvironment::MainThreadType::IO), | 
|  | player_error_handler_( | 
|  | base::BindRepeating(&MediaPlayerImplTest::OnPlayerFidlError, | 
|  | base::Unretained(this))) { | 
|  | auto player_endpoints = | 
|  | fidl::CreateEndpoints<fuchsia_media_sessions2::Player>(); | 
|  | ZX_CHECK(player_endpoints.is_ok(), player_endpoints.status_value()); | 
|  | player_.Bind(std::move(player_endpoints->client), | 
|  | async_get_default_dispatcher(), &player_error_handler_); | 
|  | player_server_end_ = std::move(player_endpoints->server); | 
|  | } | 
|  |  | 
|  | MediaPlayerImplTest(const MediaPlayerImplTest&) = delete; | 
|  | MediaPlayerImplTest& operator=(const MediaPlayerImplTest&) = delete; | 
|  |  | 
|  | ~MediaPlayerImplTest() override = default; | 
|  |  | 
|  | void SetPlayerFidlErrorCallback( | 
|  | base::RepeatingCallback<void(fidl::UnbindInfo)> | 
|  | player_fidl_error_callback) { | 
|  | player_fidl_error_callback_ = std::move(player_fidl_error_callback); | 
|  | } | 
|  |  | 
|  | protected: | 
|  | void OnPlayerFidlError(fidl::UnbindInfo error) { | 
|  | if (player_fidl_error_callback_) { | 
|  | player_fidl_error_callback_.Run(std::move(error)); | 
|  | } | 
|  | } | 
|  |  | 
|  | base::test::SingleThreadTaskEnvironment task_environment_; | 
|  |  | 
|  | testing::StrictMock<FakeMediaSession> fake_session_; | 
|  | fidl::Client<fuchsia_media_sessions2::Player> player_; | 
|  | base::FidlErrorEventHandler<fuchsia_media_sessions2::Player> | 
|  | player_error_handler_; | 
|  | base::RepeatingCallback<void(fidl::UnbindInfo)> player_fidl_error_callback_; | 
|  |  | 
|  | fidl::ServerEnd<fuchsia_media_sessions2::Player> player_server_end_; | 
|  |  | 
|  | std::unique_ptr<MediaPlayerImpl> player_impl_; | 
|  | }; | 
|  |  | 
|  | // Verify that the `on_disconnect` closure is invoked if the client disconnects. | 
|  | TEST_F(MediaPlayerImplTest, OnDisconnectCalledOnDisconnect) { | 
|  | base::RunLoop run_loop; | 
|  | player_impl_ = std::make_unique<MediaPlayerImpl>( | 
|  | &fake_session_, std::move(player_server_end_), run_loop.QuitClosure()); | 
|  | player_ = {}; | 
|  | run_loop.Run(); | 
|  | } | 
|  |  | 
|  | // Verify that the `on_disconnect` closure is invoked if the client calls the | 
|  | // WatchInfoChange() API incorrectly. | 
|  | TEST_F(MediaPlayerImplTest, ClientDisconnectedOnBadApiUsage) { | 
|  | base::RunLoop on_disconnected_loop; | 
|  | base::RunLoop player_error_loop; | 
|  |  | 
|  | player_impl_ = std::make_unique<MediaPlayerImpl>( | 
|  | &fake_session_, std::move(player_server_end_), | 
|  | on_disconnected_loop.QuitClosure()); | 
|  | SetPlayerFidlErrorCallback( | 
|  | base::BindLambdaForTesting([&player_error_loop](fidl::UnbindInfo error) { | 
|  | EXPECT_EQ(error.status(), ZX_ERR_BAD_STATE); | 
|  | player_error_loop.Quit(); | 
|  | })); | 
|  |  | 
|  | // Call WatchInfoChange() three times in succession. The first call may | 
|  | // immediately invoke the callback, with initial state, but since there will | 
|  | // be no state-change between that and the second, it will hold the callback, | 
|  | // and the third call will therefore be a protocol violation. | 
|  | player_->WatchInfoChange().Then([](auto& result) { | 
|  | ASSERT_TRUE(result.is_ok()) << result.error_value().status_string(); | 
|  | }); | 
|  | player_->WatchInfoChange().Then( | 
|  | [](auto& result) { ASSERT_TRUE(result.is_error()); }); | 
|  | player_->WatchInfoChange().Then( | 
|  | [](auto& result) { ASSERT_TRUE(result.is_error()); }); | 
|  |  | 
|  | // Wait for both on-disconnected and player error handler to be invoked. | 
|  | on_disconnected_loop.Run(); | 
|  | player_error_loop.Run(); | 
|  | } | 
|  |  | 
|  | // Verify that the completer is Closed on destruction of `MediaPlayerImpl`. | 
|  | // Otherwise it will trigger a CHECK for failing to reply. | 
|  | TEST_F(MediaPlayerImplTest, WatchInfoChangeAsyncCompleterClosedOnDestruction) { | 
|  | player_impl_ = std::make_unique<MediaPlayerImpl>( | 
|  | &fake_session_, std::move(player_server_end_), | 
|  | MakeExpectedNotRunClosure(FROM_HERE)); | 
|  | player_->WatchInfoChange().Then([](auto result) {}); | 
|  | // The first call always replies immediately, so call a second call to hold a | 
|  | // pending completer. | 
|  | player_->WatchInfoChange().Then([](auto result) {}); | 
|  |  | 
|  | // Pump the message loop to process the WatchInfoChange() call. | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | } | 
|  |  | 
|  | // Verify that the first WatchInfoChange() registers the observer. | 
|  | TEST_F(MediaPlayerImplTest, WatchInfoChangeRegistersObserver) { | 
|  | player_impl_ = std::make_unique<MediaPlayerImpl>( | 
|  | &fake_session_, std::move(player_server_end_), | 
|  | MakeExpectedNotRunClosure(FROM_HERE)); | 
|  | player_->WatchInfoChange().Then([](auto result) {}); | 
|  |  | 
|  | ASSERT_FALSE(fake_session_.observer()); | 
|  |  | 
|  | // Pump the message loop to process the WatchInfoChange() call. | 
|  | base::RunLoop().RunUntilIdle(); | 
|  |  | 
|  | EXPECT_TRUE(fake_session_.observer()); | 
|  | } | 
|  |  | 
|  | // Verify that the initial session state is returned via WatchInfoChange(), | 
|  | // potentially via several calls to it. | 
|  | TEST_F(MediaPlayerImplTest, WatchInfoChangeReturnsInitialState) { | 
|  | player_impl_ = std::make_unique<MediaPlayerImpl>( | 
|  | &fake_session_, std::move(player_server_end_), | 
|  | MakeExpectedNotRunClosure(FROM_HERE)); | 
|  |  | 
|  | base::RunLoop return_info_loop; | 
|  | fuchsia_media_sessions2::PlayerInfoDelta initial_info; | 
|  | std::function<void( | 
|  | fidl::Result<fuchsia_media_sessions2::Player::WatchInfoChange>&)> | 
|  | on_info_change = | 
|  | [this, &initial_info, &on_info_change, &return_info_loop]( | 
|  | fidl::Result<fuchsia_media_sessions2::Player::WatchInfoChange>& | 
|  | result) { | 
|  | ASSERT_TRUE(result.is_ok()) << result.error_value().status_string(); | 
|  | auto delta = result->player_info_delta(); | 
|  | if (delta.player_status().has_value()) { | 
|  | initial_info.player_status( | 
|  | std::move(delta.player_status().value())); | 
|  | } | 
|  | if (delta.metadata().has_value()) { | 
|  | initial_info.metadata(std::move(delta.metadata().value())); | 
|  | } | 
|  | if (delta.player_capabilities().has_value()) { | 
|  | initial_info.player_capabilities( | 
|  | std::move(delta.player_capabilities().value())); | 
|  | } | 
|  |  | 
|  | // Only quit the loop once all of the expected fields are present. | 
|  | if (initial_info.player_status().has_value() && | 
|  | initial_info.metadata().has_value() && | 
|  | initial_info.player_capabilities().has_value()) { | 
|  | return_info_loop.Quit(); | 
|  | } else { | 
|  | player_->WatchInfoChange().Then(on_info_change); | 
|  | } | 
|  | }; | 
|  | player_->WatchInfoChange().Then(on_info_change); | 
|  |  | 
|  | // Pump the message loop to process the WatchInfoChange() call. | 
|  | base::RunLoop().RunUntilIdle(); | 
|  |  | 
|  | ASSERT_TRUE(fake_session_.observer()); | 
|  |  | 
|  | media_session::mojom::MediaSessionInfoPtr info( | 
|  | media_session::mojom::MediaSessionInfo::New()); | 
|  | info->state = | 
|  | media_session::mojom::MediaSessionInfo::SessionState::kSuspended; | 
|  | fake_session_.observer()->MediaSessionInfoChanged(std::move(info)); | 
|  |  | 
|  | media_session::MediaMetadata metadata; | 
|  | constexpr char kExpectedTitle[] = "Love Like A Sunset, Pt.1"; | 
|  | constexpr char16_t kExpectedTitle16[] = u"Love Like A Sunset, Pt.1"; | 
|  | metadata.title = kExpectedTitle16; | 
|  | constexpr char kExpectedArtist[] = "Phoenix"; | 
|  | constexpr char16_t kExpectedArtist16[] = u"Phoenix"; | 
|  | metadata.artist = kExpectedArtist16; | 
|  | constexpr char kExpectedAlbum[] = "Wolfgang Amadeus Phoenix"; | 
|  | constexpr char16_t kExpectedAlbum16[] = u"Wolfgang Amadeus Phoenix"; | 
|  | metadata.album = kExpectedAlbum16; | 
|  | constexpr char kExpectedSourceTitle[] = "Unknown"; | 
|  | constexpr char16_t kExpectedSourceTitle16[] = u"Unknown"; | 
|  | metadata.source_title = kExpectedSourceTitle16; | 
|  | fake_session_.observer()->MediaSessionMetadataChanged(metadata); | 
|  |  | 
|  | std::vector<media_session::mojom::MediaSessionAction> actions = { | 
|  | media_session::mojom::MediaSessionAction::kPlay, | 
|  | media_session::mojom::MediaSessionAction::kNextTrack, | 
|  | media_session::mojom::MediaSessionAction::kScrubTo}; | 
|  | fake_session_.observer()->MediaSessionActionsChanged(actions); | 
|  |  | 
|  | // These are sent by MediaSessionImpl, but currently ignored. | 
|  | fake_session_.observer()->MediaSessionImagesChanged({}); | 
|  | fake_session_.observer()->MediaSessionPositionChanged({}); | 
|  |  | 
|  | return_info_loop.Run(); | 
|  |  | 
|  | // Verify that all of the expected fields are present, and correct. | 
|  | ASSERT_TRUE(initial_info.player_status().has_value()); | 
|  | EXPECT_EQ(initial_info.player_status()->player_state(), | 
|  | fuchsia_media_sessions2::PlayerState::kPaused); | 
|  | ASSERT_TRUE(initial_info.metadata().has_value()); | 
|  | std::map<std::string, std::string> received_metadata; | 
|  | for (auto& property : initial_info.metadata()->properties()) { | 
|  | received_metadata[property.label()] = property.value(); | 
|  | } | 
|  | EXPECT_EQ(received_metadata[fuchsia_media::kMetadataLabelTitle], | 
|  | kExpectedTitle); | 
|  | EXPECT_EQ(received_metadata[fuchsia_media::kMetadataLabelArtist], | 
|  | kExpectedArtist); | 
|  | EXPECT_EQ(received_metadata[fuchsia_media::kMetadataLabelAlbum], | 
|  | kExpectedAlbum); | 
|  | EXPECT_EQ(received_metadata[fuchsia_media::kMetadataSourceTitle], | 
|  | kExpectedSourceTitle); | 
|  | ASSERT_TRUE(initial_info.player_capabilities().has_value()); | 
|  | ASSERT_TRUE(initial_info.player_capabilities()->flags().has_value()); | 
|  | const fuchsia_media_sessions2::PlayerCapabilityFlags received_flags = | 
|  | initial_info.player_capabilities()->flags().value(); | 
|  | EXPECT_TRUE(HasFlag(received_flags, | 
|  | fuchsia_media_sessions2::PlayerCapabilityFlags::kPlay)); | 
|  | EXPECT_TRUE(HasFlag( | 
|  | received_flags, | 
|  | fuchsia_media_sessions2::PlayerCapabilityFlags::kChangeToNextItem)); | 
|  | EXPECT_FALSE(HasFlag(received_flags, | 
|  | fuchsia_media_sessions2::PlayerCapabilityFlags::kSeek)); | 
|  | EXPECT_FALSE(HasFlag(received_flags, | 
|  | fuchsia_media_sessions2::PlayerCapabilityFlags::kPause)); | 
|  | } | 
|  |  | 
|  | // Verify that WatchInfoChange() waits for the next change to the session state | 
|  | // before returning. | 
|  | TEST_F(MediaPlayerImplTest, WatchInfoChangeWaitsForNextChange) { | 
|  | player_impl_ = std::make_unique<MediaPlayerImpl>( | 
|  | &fake_session_, std::move(player_server_end_), | 
|  | MakeExpectedNotRunClosure(FROM_HERE)); | 
|  |  | 
|  | // Start watching, which will connect the observer, and send some initial | 
|  | // state so that WatchInfoChange() will return. | 
|  | base::RunLoop player_state_loop; | 
|  | std::function<void( | 
|  | fidl::Result<fuchsia_media_sessions2::Player::WatchInfoChange>&)> | 
|  | on_info_change = | 
|  | [this, &on_info_change, &player_state_loop]( | 
|  | fidl::Result<fuchsia_media_sessions2::Player::WatchInfoChange>& | 
|  | result) { | 
|  | ASSERT_TRUE(result.is_ok()) << result.error_value().status_string(); | 
|  | auto delta = result->player_info_delta(); | 
|  | if (!delta.player_status().has_value() || | 
|  | !delta.player_status()->player_state().has_value()) { | 
|  | player_->WatchInfoChange().Then(on_info_change); | 
|  | return; | 
|  | } | 
|  | EXPECT_EQ(delta.player_status()->player_state().value(), | 
|  | fuchsia_media_sessions2::PlayerState::kPaused); | 
|  | player_state_loop.Quit(); | 
|  | }; | 
|  | player_->WatchInfoChange().Then(on_info_change); | 
|  |  | 
|  | // Pump the message loop to process the first WatchInfoChange() call. | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | ASSERT_TRUE(fake_session_.observer()); | 
|  |  | 
|  | // Set an initial player state for the session, and wait for it. | 
|  | media_session::mojom::MediaSessionInfoPtr info( | 
|  | media_session::mojom::MediaSessionInfo::New()); | 
|  | info->state = | 
|  | media_session::mojom::MediaSessionInfo::SessionState::kSuspended; | 
|  | fake_session_.observer()->MediaSessionInfoChanged(std::move(info)); | 
|  | player_state_loop.Run(); | 
|  |  | 
|  | // Calling WatchInfoChange() now should succeed, but not immediately return | 
|  | // any new data. | 
|  | base::RunLoop change_loop; | 
|  | absl::optional<fuchsia_media_sessions2::PlayerState> state_after_change; | 
|  |  | 
|  | player_->WatchInfoChange().Then( | 
|  | [&change_loop, &state_after_change]( | 
|  | fidl::Result<fuchsia_media_sessions2::Player::WatchInfoChange>& | 
|  | result) { | 
|  | ASSERT_TRUE(result.is_ok()) << result.error_value().status_string(); | 
|  | auto delta = result->player_info_delta(); | 
|  | ASSERT_TRUE(delta.player_status().has_value()); | 
|  | ASSERT_TRUE(delta.player_status()->player_state().has_value()); | 
|  | state_after_change.emplace( | 
|  | delta.player_status()->player_state().value()); | 
|  | change_loop.Quit(); | 
|  | }); | 
|  |  | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_FALSE(state_after_change.has_value()); | 
|  |  | 
|  | // Generate a player status change, which should cause WatchInfoChange() to | 
|  | // return. | 
|  | info = media_session::mojom::MediaSessionInfo::New(); | 
|  | info->state = media_session::mojom::MediaSessionInfo::SessionState::kActive; | 
|  | fake_session_.observer()->MediaSessionInfoChanged(std::move(info)); | 
|  | change_loop.Run(); | 
|  | ASSERT_TRUE(state_after_change.has_value()); | 
|  | EXPECT_EQ(*state_after_change, | 
|  | fuchsia_media_sessions2::PlayerState::kPlaying); | 
|  | } | 
|  |  | 
|  | // Verify that each of the fire-and-forget playback controls are routed to the | 
|  | // expected MediaSession APIs. | 
|  | TEST_F(MediaPlayerImplTest, PlaybackControls) { | 
|  | testing::InSequence sequence; | 
|  | EXPECT_CALL(fake_session_, Resume(content::MediaSession::SuspendType::kUI)); | 
|  | EXPECT_CALL(fake_session_, Suspend(content::MediaSession::SuspendType::kUI)); | 
|  | EXPECT_CALL(fake_session_, Suspend(content::MediaSession::SuspendType::kUI)); | 
|  | EXPECT_CALL(fake_session_, SeekTo(base::TimeDelta())); | 
|  | base::TimeDelta skip_forward_delta_; | 
|  | EXPECT_CALL(fake_session_, Seek(testing::_)) | 
|  | .WillOnce(testing::SaveArg<0>(&skip_forward_delta_)); | 
|  | base::TimeDelta skip_reverse_delta_; | 
|  | EXPECT_CALL(fake_session_, Seek(testing::_)) | 
|  | .WillOnce(testing::SaveArg<0>(&skip_reverse_delta_)); | 
|  | EXPECT_CALL(fake_session_, NextTrack()); | 
|  | EXPECT_CALL(fake_session_, PreviousTrack()); | 
|  |  | 
|  | player_impl_ = std::make_unique<MediaPlayerImpl>( | 
|  | &fake_session_, std::move(player_server_end_), | 
|  | MakeExpectedNotRunClosure(FROM_HERE)); | 
|  |  | 
|  | EXPECT_TRUE(player_->Play().is_ok()); | 
|  | EXPECT_TRUE(player_->Pause().is_ok()); | 
|  | EXPECT_TRUE(player_->Stop().is_ok()); | 
|  | EXPECT_TRUE(player_->Seek(0).is_ok()); | 
|  | EXPECT_TRUE(player_->SkipForward().is_ok()); | 
|  | EXPECT_TRUE(player_->SkipReverse().is_ok()); | 
|  | EXPECT_TRUE(player_->NextItem().is_ok()); | 
|  | EXPECT_TRUE(player_->PrevItem().is_ok()); | 
|  |  | 
|  | // Pump the message loop to process each of the calls. | 
|  | base::RunLoop().RunUntilIdle(); | 
|  |  | 
|  | EXPECT_EQ(skip_forward_delta_, -skip_reverse_delta_); | 
|  | } |