| // Copyright 2020 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 "fuchsia/engine/browser/media_player_impl.h" |
| |
| #include "base/run_loop.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/bind_test_util.h" |
| #include "base/test/task_environment.h" |
| #include "content/public/browser/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::MediaSession { |
| 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(); |
| } |
| MOCK_METHOD1(Suspend, void(SuspendType)); |
| MOCK_METHOD1(Resume, void(SuspendType)); |
| MOCK_METHOD0(StartDucking, void()); |
| MOCK_METHOD0(StopDucking, void()); |
| MOCK_METHOD0(PreviousTrack, void()); |
| MOCK_METHOD0(NextTrack, void()); |
| MOCK_METHOD0(SkipAd, void()); |
| MOCK_METHOD1(Seek, void(base::TimeDelta)); |
| MOCK_METHOD1(Stop, void(SuspendType)); |
| MOCK_METHOD1(SeekTo, void(base::TimeDelta)); |
| MOCK_METHOD1(ScrubTo, void(base::TimeDelta)); |
| MOCK_METHOD0(EnterPictureInPicture, void()); |
| MOCK_METHOD0(ExitPictureInPicture, void()); |
| MOCK_METHOD1(SetAudioSinkId, void(const base::Optional<std::string>& id)); |
| |
| // content::MediaSession APIs faked to implement testing behaviour. |
| MOCK_METHOD1(DidReceiveAction, |
| void(media_session::mojom::MediaSessionAction)); |
| MOCK_METHOD1(GetMediaSessionInfo, void(GetMediaSessionInfoCallback)); |
| MOCK_METHOD1(GetDebugInfo, void(GetDebugInfoCallback)); |
| MOCK_METHOD4(GetMediaImageBitmap, |
| void(const media_session::MediaImage& image, |
| int minimum_size_px, |
| int desired_size_px, |
| GetMediaImageBitmapCallback callback)); |
| |
| // 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) {} |
| ~MediaPlayerImplTest() override = default; |
| |
| void OnPlayerDisconnected() {} |
| |
| protected: |
| base::test::SingleThreadTaskEnvironment task_environment_; |
| |
| testing::StrictMock<FakeMediaSession> fake_session_; |
| fuchsia::media::sessions2::PlayerPtr player_; |
| |
| std::unique_ptr<MediaPlayerImpl> player_impl_; |
| |
| DISALLOW_COPY_AND_ASSIGN(MediaPlayerImplTest); |
| }; |
| |
| // 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_, player_.NewRequest(), run_loop.QuitClosure()); |
| player_.Unbind(); |
| 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_, player_.NewRequest(), on_disconnected_loop.QuitClosure()); |
| player_.set_error_handler([&player_error_loop](zx_status_t status) { |
| EXPECT_EQ(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([](fuchsia::media::sessions2::PlayerInfoDelta) {}); |
| player_->WatchInfoChange( |
| [](fuchsia::media::sessions2::PlayerInfoDelta) { ADD_FAILURE(); }); |
| player_->WatchInfoChange( |
| [](fuchsia::media::sessions2::PlayerInfoDelta) { ADD_FAILURE(); }); |
| |
| // Wait for both on-disconnected and player error handler to be invoked. |
| on_disconnected_loop.Run(); |
| player_error_loop.Run(); |
| } |
| |
| // Verify that the first WatchInfoChange() registers the observer. |
| TEST_F(MediaPlayerImplTest, WatchInfoChangeRegistersObserver) { |
| player_impl_ = |
| std::make_unique<MediaPlayerImpl>(&fake_session_, player_.NewRequest(), |
| MakeExpectedNotRunClosure(FROM_HERE)); |
| player_->WatchInfoChange([](fuchsia::media::sessions2::PlayerInfoDelta) {}); |
| |
| 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_, player_.NewRequest(), |
| MakeExpectedNotRunClosure(FROM_HERE)); |
| |
| base::RunLoop return_info_loop; |
| fuchsia::media::sessions2::PlayerInfoDelta initial_info; |
| std::function<void(fuchsia::media::sessions2::PlayerInfoDelta)> watch_info = |
| [this, &initial_info, &watch_info, |
| &return_info_loop](fuchsia::media::sessions2::PlayerInfoDelta delta) { |
| if (delta.has_player_status()) |
| initial_info.set_player_status( |
| std::move(*delta.mutable_player_status())); |
| if (delta.has_metadata()) |
| initial_info.set_metadata(delta.metadata()); |
| if (delta.has_player_capabilities()) |
| initial_info.set_player_capabilities( |
| std::move(*delta.mutable_player_capabilities())); |
| |
| // Only quit the loop once all of the expected fields are present. |
| if (initial_info.has_player_status() && initial_info.has_metadata() && |
| initial_info.has_player_capabilities()) { |
| return_info_loop.Quit(); |
| } else { |
| player_->WatchInfoChange(watch_info); |
| } |
| }; |
| player_->WatchInfoChange(watch_info); |
| |
| // 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"; |
| metadata.title = base::ASCIIToUTF16(kExpectedTitle); |
| constexpr char kExpectedArtist[] = "Phoenix"; |
| metadata.artist = base::ASCIIToUTF16(kExpectedArtist); |
| constexpr char kExpectedAlbum[] = "Wolfgang Amadeus Phoenix"; |
| metadata.album = base::ASCIIToUTF16(kExpectedAlbum); |
| constexpr char kExpectedSourceTitle[] = "Unknown"; |
| metadata.source_title = base::ASCIIToUTF16(kExpectedSourceTitle); |
| 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.has_player_status()); |
| ASSERT_TRUE(initial_info.player_status().has_player_state()); |
| EXPECT_EQ(initial_info.player_status().player_state(), |
| fuchsia::media::sessions2::PlayerState::PAUSED); |
| ASSERT_TRUE(initial_info.has_metadata()); |
| 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::METADATA_LABEL_TITLE], |
| kExpectedTitle); |
| EXPECT_EQ(received_metadata[fuchsia::media::METADATA_LABEL_ARTIST], |
| kExpectedArtist); |
| EXPECT_EQ(received_metadata[fuchsia::media::METADATA_LABEL_ALBUM], |
| kExpectedAlbum); |
| EXPECT_EQ(received_metadata[fuchsia::media::METADATA_SOURCE_TITLE], |
| kExpectedSourceTitle); |
| ASSERT_TRUE(initial_info.has_player_capabilities()); |
| ASSERT_TRUE(initial_info.player_capabilities().has_flags()); |
| const fuchsia::media::sessions2::PlayerCapabilityFlags received_flags = |
| initial_info.player_capabilities().flags(); |
| EXPECT_TRUE(HasFlag(received_flags, |
| fuchsia::media::sessions2::PlayerCapabilityFlags::PLAY)); |
| EXPECT_TRUE(HasFlag( |
| received_flags, |
| fuchsia::media::sessions2::PlayerCapabilityFlags::CHANGE_TO_NEXT_ITEM)); |
| EXPECT_FALSE(HasFlag(received_flags, |
| fuchsia::media::sessions2::PlayerCapabilityFlags::SEEK)); |
| EXPECT_FALSE(HasFlag( |
| received_flags, fuchsia::media::sessions2::PlayerCapabilityFlags::PAUSE)); |
| } |
| |
| // 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_, player_.NewRequest(), |
| 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(fuchsia::media::sessions2::PlayerInfoDelta)> |
| watch_for_player_state = |
| [this, &watch_for_player_state, &player_state_loop]( |
| fuchsia::media::sessions2::PlayerInfoDelta delta) { |
| if (!delta.has_player_status() || |
| !delta.player_status().has_player_state()) { |
| player_->WatchInfoChange(watch_for_player_state); |
| return; |
| } |
| EXPECT_EQ(delta.player_status().player_state(), |
| fuchsia::media::sessions2::PlayerState::PAUSED); |
| player_state_loop.Quit(); |
| }; |
| player_->WatchInfoChange(watch_for_player_state); |
| |
| // 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; |
| base::Optional<fuchsia::media::sessions2::PlayerState> state_after_change; |
| |
| player_->WatchInfoChange( |
| [&change_loop, |
| &state_after_change](fuchsia::media::sessions2::PlayerInfoDelta delta) { |
| ASSERT_TRUE(delta.has_player_status()); |
| ASSERT_TRUE(delta.player_status().has_player_state()); |
| state_after_change.emplace(delta.player_status().player_state()); |
| 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::PLAYING); |
| } |
| |
| // 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_, player_.NewRequest(), |
| MakeExpectedNotRunClosure(FROM_HERE)); |
| |
| player_->Play(); |
| player_->Pause(); |
| player_->Stop(); |
| player_->Seek(0); |
| player_->SkipForward(); |
| player_->SkipReverse(); |
| player_->NextItem(); |
| player_->PrevItem(); |
| |
| // Pump the message loop to process each of the calls. |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_EQ(skip_forward_delta_, -skip_reverse_delta_); |
| } |