| // 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; |
| std::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_); |
| } |