| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/task_environment.h" |
| #include "base/threading/thread.h" |
| #include "base/time/time.h" |
| #include "media/base/audio_decoder_config.h" |
| #include "media/base/audio_timestamp_helper.h" |
| #include "media/base/decoder_buffer.h" |
| #include "media/base/media_util.h" |
| #include "media/base/mock_filters.h" |
| #include "media/base/test_helpers.h" |
| #include "media/base/waiting.h" |
| #include "media/mojo/clients/mojo_audio_decoder.h" |
| #include "media/mojo/mojom/audio_decoder.mojom.h" |
| #include "media/mojo/services/mojo_audio_decoder_service.h" |
| #include "media/mojo/services/mojo_cdm_service_context.h" |
| #include "media/mojo/services/mojo_media_client.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/self_owned_receiver.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using ::base::test::RunOnceCallback; |
| using ::testing::_; |
| using ::testing::DoAll; |
| using ::testing::InSequence; |
| using ::testing::InvokeWithoutArgs; |
| using ::testing::SaveArg; |
| using ::testing::StrictMock; |
| |
| namespace media { |
| |
| const SampleFormat kSampleFormat = kSampleFormatPlanarF32; |
| const int kChannels = 2; |
| const ChannelLayout kChannelLayout = CHANNEL_LAYOUT_STEREO; |
| const int kDefaultSampleRate = 44100; |
| const int kDefaultFrameSize = 100; |
| const int kOutputPerDecode = 3; |
| |
| namespace { |
| |
| // Proxies CreateAudioDecoder() to a callback. |
| class FakeMojoMediaClient : public MojoMediaClient { |
| public: |
| using CreateAudioDecoderCB = |
| base::RepeatingCallback<std::unique_ptr<AudioDecoder>()>; |
| |
| explicit FakeMojoMediaClient(CreateAudioDecoderCB create_audio_decoder_cb) |
| : create_audio_decoder_cb_(std::move(create_audio_decoder_cb)) {} |
| |
| FakeMojoMediaClient(const FakeMojoMediaClient&) = delete; |
| FakeMojoMediaClient& operator=(const FakeMojoMediaClient&) = delete; |
| |
| std::unique_ptr<AudioDecoder> CreateAudioDecoder( |
| scoped_refptr<base::SequencedTaskRunner> task_runner, |
| std::unique_ptr<media::MediaLog> media_log) override { |
| return create_audio_decoder_cb_.Run(); |
| } |
| |
| private: |
| const CreateAudioDecoderCB create_audio_decoder_cb_; |
| }; |
| |
| } // namespace |
| |
| // Tests MojoAudioDecoder (client) and MojoAudioDecoderService (service). |
| // To better simulate how they are used in production, the client and service |
| // are running on two different threads. |
| class MojoAudioDecoderTest : public ::testing::Test { |
| public: |
| MojoAudioDecoderTest() |
| : input_timestamp_helper_(kDefaultSampleRate), |
| service_thread_("Service Thread") { |
| input_timestamp_helper_.SetBaseTimestamp(base::TimeDelta()); |
| |
| service_thread_.Start(); |
| service_task_runner_ = service_thread_.task_runner(); |
| |
| // Setup the mojo connection. |
| mojo::PendingRemote<mojom::AudioDecoder> remote_audio_decoder; |
| service_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&MojoAudioDecoderTest::ConnectToService, |
| base::Unretained(this), |
| remote_audio_decoder.InitWithNewPipeAndPassReceiver())); |
| mojo_audio_decoder_ = std::make_unique<MojoAudioDecoder>( |
| task_environment_.GetMainThreadTaskRunner(), &media_log_, |
| std::move(remote_audio_decoder)); |
| } |
| |
| MojoAudioDecoderTest(const MojoAudioDecoderTest&) = delete; |
| MojoAudioDecoderTest& operator=(const MojoAudioDecoderTest&) = delete; |
| |
| ~MojoAudioDecoderTest() override { |
| // Destroy |mojo_audio_decoder_| first so that the service will be |
| // destructed. Then stop the service thread. Otherwise we'll leak memory. |
| mojo_audio_decoder_.reset(); |
| service_thread_.Stop(); |
| |
| RunLoopUntilIdle(); |
| } |
| |
| // Completion callbacks. |
| MOCK_METHOD1(OnInitialized, void(DecoderStatus)); |
| MOCK_METHOD1(OnOutput, void(scoped_refptr<AudioBuffer>)); |
| MOCK_METHOD1(OnWaiting, void(WaitingReason)); |
| MOCK_METHOD1(OnDecoded, void(DecoderStatus)); |
| MOCK_METHOD0(OnReset, void()); |
| |
| // Always create a new RunLoop (and destroy the old loop if it exists) before |
| // running the loop because we cannot run the same loop more than once. |
| |
| void RunLoop() { |
| DVLOG(1) << __func__; |
| run_loop_ = std::make_unique<base::RunLoop>(); |
| run_loop_->Run(); |
| } |
| |
| void RunLoopUntilIdle() { |
| DVLOG(1) << __func__; |
| run_loop_ = std::make_unique<base::RunLoop>(); |
| run_loop_->RunUntilIdle(); |
| } |
| |
| void QuitLoop() { |
| DVLOG(1) << __func__; |
| run_loop_->QuitWhenIdle(); |
| } |
| |
| std::unique_ptr<AudioDecoder> CreateAudioDecoder() { |
| CHECK_NE(owned_mock_audio_decoder_, nullptr); |
| return std::move(owned_mock_audio_decoder_); |
| } |
| |
| void ConnectToService(mojo::PendingReceiver<mojom::AudioDecoder> receiver) { |
| DCHECK(service_task_runner_->BelongsToCurrentThread()); |
| |
| EXPECT_CALL(*mock_audio_decoder_, Initialize_(_, _, _, _, _)) |
| .WillRepeatedly(DoAll(SaveArg<3>(&output_cb_), SaveArg<4>(&waiting_cb_), |
| RunOnceCallback<2>(DecoderStatus::Codes::kOk))); |
| EXPECT_CALL(*mock_audio_decoder_, Decode(_, _)) |
| .WillRepeatedly([&](scoped_refptr<DecoderBuffer> buffer, |
| AudioDecoder::DecodeCB decode_cb) { |
| ReturnOutput(); |
| std::move(decode_cb).Run(DecoderStatus::Codes::kOk); |
| }); |
| EXPECT_CALL(*mock_audio_decoder_, Reset_(_)) |
| .WillRepeatedly(RunOnceCallback<0>()); |
| |
| mojo::MakeSelfOwnedReceiver( |
| std::make_unique<MojoAudioDecoderService>(&mojo_media_client_, |
| &mojo_cdm_service_context_), |
| std::move(receiver)); |
| } |
| |
| void SetWriterCapacity(uint32_t capacity) { |
| mojo_audio_decoder_->set_writer_capacity_for_testing(capacity); |
| } |
| |
| void InitializeAndExpect(DecoderStatus status) { |
| DVLOG(1) << __func__ << ": success=" << static_cast<int>(status.code()); |
| EXPECT_CALL(*this, OnInitialized(SameStatusCode(status))) |
| .WillOnce(InvokeWithoutArgs(this, &MojoAudioDecoderTest::QuitLoop)); |
| |
| AudioDecoderConfig audio_config( |
| AudioCodec::kVorbis, kSampleFormat, kChannelLayout, kDefaultSampleRate, |
| EmptyExtraData(), EncryptionScheme::kUnencrypted); |
| |
| mojo_audio_decoder_->Initialize( |
| audio_config, nullptr, |
| base::BindOnce(&MojoAudioDecoderTest::OnInitialized, |
| base::Unretained(this)), |
| base::BindRepeating(&MojoAudioDecoderTest::OnOutput, |
| base::Unretained(this)), |
| base::BindRepeating(&MojoAudioDecoderTest::OnWaiting, |
| base::Unretained(this))); |
| |
| RunLoop(); |
| } |
| |
| void Initialize() { InitializeAndExpect(DecoderStatus::Codes::kOk); } |
| |
| void Decode() { |
| scoped_refptr<DecoderBuffer> buffer(new DecoderBuffer(100)); |
| mojo_audio_decoder_->Decode( |
| buffer, base::BindRepeating(&MojoAudioDecoderTest::OnDecoded, |
| base::Unretained(this))); |
| } |
| |
| void Reset() { |
| mojo_audio_decoder_->Reset( |
| base::BindOnce(&MojoAudioDecoderTest::OnReset, base::Unretained(this))); |
| } |
| |
| void ResetAndWaitUntilFinish() { |
| DVLOG(1) << __func__; |
| EXPECT_CALL(*this, OnReset()) |
| .WillOnce(InvokeWithoutArgs(this, &MojoAudioDecoderTest::QuitLoop)); |
| Reset(); |
| RunLoop(); |
| } |
| |
| void ReturnOutput() { |
| for (int i = 0; i < kOutputPerDecode; ++i) { |
| scoped_refptr<AudioBuffer> audio_buffer = MakeAudioBuffer<float>( |
| kSampleFormat, kChannelLayout, kChannels, kDefaultSampleRate, 1.0, |
| 0.0f, 100, input_timestamp_helper_.GetTimestamp()); |
| input_timestamp_helper_.AddFrames(kDefaultFrameSize); |
| output_cb_.Run(audio_buffer); |
| } |
| } |
| |
| void WaitForKey() { waiting_cb_.Run(WaitingReason::kNoDecryptionKey); } |
| |
| void DecodeMultipleTimes(int num_of_decodes) { |
| num_of_decodes_ = num_of_decodes; |
| KeepDecodingOrQuit(); |
| RunLoop(); |
| } |
| |
| void KeepDecodingOrQuit() { |
| if (decode_count_ >= num_of_decodes_) { |
| QuitLoop(); |
| return; |
| } |
| |
| decode_count_++; |
| |
| InSequence s; // Make sure OnOutput() and OnDecoded() are called in order. |
| EXPECT_CALL(*this, OnOutput(_)).Times(kOutputPerDecode); |
| EXPECT_CALL(*this, OnDecoded(IsOkStatus())) |
| .WillOnce( |
| InvokeWithoutArgs(this, &MojoAudioDecoderTest::KeepDecodingOrQuit)); |
| Decode(); |
| } |
| |
| void DecodeAndReset() { |
| InSequence s; // Make sure all callbacks are fired in order. |
| EXPECT_CALL(*this, OnOutput(_)).Times(kOutputPerDecode); |
| EXPECT_CALL(*this, OnDecoded(IsOkStatus())); |
| EXPECT_CALL(*this, OnReset()) |
| .WillOnce(InvokeWithoutArgs(this, &MojoAudioDecoderTest::QuitLoop)); |
| Decode(); |
| Reset(); |
| RunLoop(); |
| } |
| |
| base::test::SingleThreadTaskEnvironment task_environment_; |
| std::unique_ptr<base::RunLoop> run_loop_; |
| NullMediaLog media_log_; |
| |
| // The MojoAudioDecoder that we are testing. |
| std::unique_ptr<MojoAudioDecoder> mojo_audio_decoder_; |
| MojoCdmServiceContext mojo_cdm_service_context_; |
| AudioDecoder::OutputCB output_cb_; |
| WaitingCB waiting_cb_; |
| AudioTimestampHelper input_timestamp_helper_; |
| |
| // The thread where the service runs. This provides test coverage in an |
| // environment similar to what we use in production. Also, some race |
| // conditions can only be reproduced when we run the service and client on |
| // different threads. See http://crbug.com/646054 |
| base::Thread service_thread_; |
| scoped_refptr<base::SingleThreadTaskRunner> service_task_runner_; |
| |
| // Owned by the connection on the service thread. |
| raw_ptr<MojoAudioDecoderService> mojo_audio_decoder_service_ = nullptr; |
| |
| FakeMojoMediaClient mojo_media_client_{ |
| base::BindRepeating(&MojoAudioDecoderTest::CreateAudioDecoder, |
| base::Unretained(this))}; |
| |
| // Service side mock. |
| std::unique_ptr<MockAudioDecoder> owned_mock_audio_decoder_{ |
| std::make_unique<StrictMock<MockAudioDecoder>>()}; |
| raw_ptr<MockAudioDecoder> mock_audio_decoder_{ |
| owned_mock_audio_decoder_.get()}; |
| |
| int num_of_decodes_ = 0; |
| int decode_count_ = 0; |
| }; |
| |
| TEST_F(MojoAudioDecoderTest, Initialize_Success) { |
| Initialize(); |
| } |
| |
| TEST_F(MojoAudioDecoderTest, Reinitialize_Success) { |
| Initialize(); |
| DecodeMultipleTimes(10); |
| ResetAndWaitUntilFinish(); |
| |
| // Reinitialize MojoAudioDecoder. |
| Initialize(); |
| } |
| |
| // Makes sure all callbacks and client calls are called in order. See |
| // http://crbug.com/646054 |
| TEST_F(MojoAudioDecoderTest, Decode_MultipleTimes) { |
| Initialize(); |
| |
| // Choose a large number of decodes per test on purpose to expose potential |
| // out of order delivery of mojo messages. See http://crbug.com/646054 |
| DecodeMultipleTimes(100); |
| } |
| |
| TEST_F(MojoAudioDecoderTest, Reset_DuringDecode) { |
| Initialize(); |
| DecodeAndReset(); |
| } |
| |
| TEST_F(MojoAudioDecoderTest, Reset_DuringDecode_ChunkedWrite) { |
| // Use a small writer capacity to force chunked write. |
| SetWriterCapacity(10); |
| Initialize(); |
| DecodeAndReset(); |
| } |
| |
| TEST_F(MojoAudioDecoderTest, WaitingForKey) { |
| Initialize(); |
| EXPECT_CALL(*mock_audio_decoder_, Decode(_, _)) |
| .WillOnce([&](scoped_refptr<DecoderBuffer> buffer, |
| AudioDecoder::DecodeCB decode_cb) { |
| WaitForKey(); |
| std::move(decode_cb).Run(DecoderStatus::Codes::kOk); |
| }); |
| EXPECT_CALL(*this, OnWaiting(WaitingReason::kNoDecryptionKey)).Times(1); |
| EXPECT_CALL(*this, OnDecoded(IsOkStatus())) |
| .WillOnce(InvokeWithoutArgs(this, &MojoAudioDecoderTest::QuitLoop)); |
| Decode(); |
| RunLoop(); |
| } |
| |
| // TODO(xhwang): Add more tests. |
| |
| } // namespace media |