| // Copyright 2016 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 "media/base/android/media_codec_loop.h" |
| |
| #include <memory> |
| |
| #include "base/android/build_info.h" |
| #include "base/macros.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/test/test_mock_time_task_runner.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "media/base/android/media_codec_bridge.h" |
| #include "media/base/android/mock_media_codec_bridge.h" |
| #include "media/base/waiting.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using ::testing::_; |
| using ::testing::AtLeast; |
| using ::testing::DoAll; |
| using ::testing::Eq; |
| using ::testing::Field; |
| using ::testing::InSequence; |
| using ::testing::Mock; |
| using ::testing::Return; |
| using ::testing::SetArgPointee; |
| using ::testing::StrictMock; |
| |
| namespace media { |
| |
| // The client is a strict mock, since we don't want random calls into it. We |
| // want to be sure about the call sequence. |
| class MockMediaCodecLoopClient : public StrictMock<MediaCodecLoop::Client> { |
| public: |
| MOCK_CONST_METHOD0(IsAnyInputPending, bool()); |
| MOCK_METHOD0(ProvideInputData, MediaCodecLoop::InputData()); |
| MOCK_METHOD1(OnInputDataQueued, void(bool)); |
| MOCK_METHOD1(OnDecodedEos, bool(const MediaCodecLoop::OutputBuffer&)); |
| MOCK_METHOD1(OnDecodedFrame, bool(const MediaCodecLoop::OutputBuffer&)); |
| MOCK_METHOD1(OnWaiting, void(WaitingReason reason)); |
| MOCK_METHOD0(OnOutputFormatChanged, bool()); |
| MOCK_METHOD0(OnCodecLoopError, void()); |
| }; |
| |
| class MediaCodecLoopTest : public testing::Test { |
| public: |
| MediaCodecLoopTest() |
| : task_runner_handle_(mock_task_runner_), |
| client_(std::make_unique<MockMediaCodecLoopClient>()) {} |
| |
| MediaCodecLoopTest(const MediaCodecLoopTest&) = delete; |
| MediaCodecLoopTest& operator=(const MediaCodecLoopTest&) = delete; |
| |
| ~MediaCodecLoopTest() override {} |
| |
| protected: |
| enum IdleExpectation { |
| ShouldBeIdle, |
| ShouldNotBeIdle, |
| }; |
| |
| // Wait until |codec_loop_| is idle. |
| // Do not call this in a sequence. |
| void WaitUntilIdle(IdleExpectation idleExpectation = ShouldBeIdle) { |
| switch (idleExpectation) { |
| case ShouldBeIdle: |
| EXPECT_CALL(*client_, IsAnyInputPending()).Times(0); |
| EXPECT_CALL(Codec(), DequeueOutputBuffer(_, _, _, _, _, _, _)).Times(0); |
| break; |
| case ShouldNotBeIdle: |
| // Expect at least one call to see if more work is ready. We will |
| // return 'no'. |
| EXPECT_CALL(*client_, IsAnyInputPending()) |
| .Times(AtLeast(1)) |
| .WillRepeatedly(Return(false)); |
| EXPECT_CALL(Codec(), DequeueOutputBuffer(_, _, _, _, _, _, _)) |
| .Times(AtLeast(1)) |
| .WillRepeatedly(Return(MEDIA_CODEC_TRY_AGAIN_LATER)); |
| break; |
| } |
| |
| // Either way, we expect that MCL should not attempt to dequeue input |
| // buffers, either because it's idle or because we said that no input |
| // is pending. |
| EXPECT_CALL(Codec(), DequeueInputBuffer(_, _)).Times(0); |
| |
| // TODO(liberato): assume that MCL doesn't retry for 30 seconds. Note |
| // that this doesn't actually wall-clock wait. |
| mock_task_runner_->FastForwardBy(base::Seconds(30)); |
| } |
| |
| void ConstructCodecLoop(int sdk_int = base::android::SDK_VERSION_LOLLIPOP) { |
| std::unique_ptr<MediaCodecBridge> codec(new MockMediaCodecBridge()); |
| // Since we're providing a codec, we do not expect an error. |
| EXPECT_CALL(*client_, OnCodecLoopError()).Times(0); |
| codec_loop_ = std::make_unique<MediaCodecLoop>( |
| sdk_int, client_.get(), std::move(codec), mock_task_runner_); |
| codec_loop_->SetTestTickClock(mock_task_runner_->GetMockTickClock()); |
| Mock::VerifyAndClearExpectations(client_.get()); |
| } |
| |
| // Set an expectation that MCL will try to get another input / output buffer, |
| // and not get one in ExpectWork. |
| void ExpectEmptyIOLoop() { |
| ExpectIsAnyInputPending(false); |
| EXPECT_CALL(Codec(), DequeueOutputBuffer(_, _, _, _, _, _, _)) |
| .Times(1) |
| .WillOnce(Return(MEDIA_CODEC_TRY_AGAIN_LATER)); |
| } |
| |
| void ExpectIsAnyInputPending(bool pending) { |
| EXPECT_CALL(*client_, IsAnyInputPending()).WillOnce(Return(pending)); |
| } |
| |
| void ExpectDequeueInputBuffer(int input_buffer_index, |
| MediaCodecStatus status = MEDIA_CODEC_OK) { |
| EXPECT_CALL(Codec(), DequeueInputBuffer(_, _)) |
| .WillOnce(DoAll(SetArgPointee<1>(input_buffer_index), Return(status))); |
| } |
| |
| void ExpectInputDataQueued(bool success) { |
| EXPECT_CALL(*client_, OnInputDataQueued(success)).Times(1); |
| } |
| |
| // Expect a call to queue |data| into MC buffer |input_buffer_index|. |
| void ExpectQueueInputBuffer(int input_buffer_index, |
| const MediaCodecLoop::InputData& data, |
| MediaCodecStatus status = MEDIA_CODEC_OK) { |
| EXPECT_CALL(Codec(), QueueInputBuffer(input_buffer_index, data.memory, |
| data.length, data.presentation_time)) |
| .Times(1) |
| .WillOnce(Return(status)); |
| } |
| |
| void ExpectProvideInputData(const MediaCodecLoop::InputData& data) { |
| EXPECT_CALL(*client_, ProvideInputData()).WillOnce(Return(data)); |
| } |
| |
| MediaCodecLoop::InputData BigBuckBunny() { |
| MediaCodecLoop::InputData data; |
| data.memory = reinterpret_cast<const uint8_t*>("big buck bunny"); |
| data.length = 14; |
| data.presentation_time = base::Seconds(1); |
| return data; |
| } |
| |
| struct OutputBuffer { |
| int index = 1; |
| size_t offset = 0; |
| size_t size = 1024; |
| base::TimeDelta pts = base::Seconds(1); |
| bool eos = false; |
| bool key_frame = true; |
| }; |
| |
| struct EosOutputBuffer : public OutputBuffer { |
| EosOutputBuffer() { eos = true; } |
| }; |
| |
| void ExpectDequeueOutputBuffer(MediaCodecStatus status) { |
| EXPECT_CALL(Codec(), DequeueOutputBuffer(_, _, _, _, _, _, _)) |
| .WillOnce(Return(status)); |
| } |
| |
| void ExpectDequeueOutputBuffer(const OutputBuffer& buffer) { |
| EXPECT_CALL(Codec(), DequeueOutputBuffer(_, _, _, _, _, _, _)) |
| .WillOnce(DoAll( |
| SetArgPointee<1>(buffer.index), SetArgPointee<2>(buffer.offset), |
| SetArgPointee<3>(buffer.size), SetArgPointee<4>(buffer.pts), |
| SetArgPointee<5>(buffer.eos), SetArgPointee<6>(buffer.key_frame), |
| Return(MEDIA_CODEC_OK))); |
| } |
| |
| void ExpectOnDecodedFrame(const OutputBuffer& buf) { |
| EXPECT_CALL(*client_, |
| OnDecodedFrame( |
| Field(&MediaCodecLoop::OutputBuffer::index, Eq(buf.index)))) |
| .Times(1) |
| .WillOnce(Return(true)); |
| } |
| |
| MockMediaCodecBridge& Codec() { |
| return *static_cast<MockMediaCodecBridge*>(codec_loop_->GetCodec()); |
| } |
| |
| public: |
| // Mocks the current thread's task runner which will also be used as the |
| // MediaCodecLoop's task runner. |
| scoped_refptr<base::TestMockTimeTaskRunner> mock_task_runner_ = |
| new base::TestMockTimeTaskRunner; |
| base::ThreadTaskRunnerHandle task_runner_handle_; |
| |
| std::unique_ptr<MediaCodecLoop> codec_loop_; |
| std::unique_ptr<MockMediaCodecLoopClient> client_; |
| }; |
| |
| TEST_F(MediaCodecLoopTest, TestConstructionWithNullCodec) { |
| std::unique_ptr<MediaCodecBridge> codec; |
| EXPECT_CALL(*client_, OnCodecLoopError()).Times(1); |
| const int sdk_int = base::android::SDK_VERSION_LOLLIPOP; |
| codec_loop_ = std::make_unique<MediaCodecLoop>( |
| sdk_int, client_.get(), std::move(codec), |
| scoped_refptr<base::SingleThreadTaskRunner>()); |
| // Do not WaitUntilIdle() here, since that assumes that we have a codec. |
| |
| ASSERT_FALSE(codec_loop_->GetCodec()); |
| } |
| |
| TEST_F(MediaCodecLoopTest, TestConstructionWithCodec) { |
| ConstructCodecLoop(); |
| ASSERT_EQ(codec_loop_->GetCodec(), &Codec()); |
| WaitUntilIdle(ShouldBeIdle); |
| } |
| |
| TEST_F(MediaCodecLoopTest, TestPendingWorkWithoutInput) { |
| ConstructCodecLoop(); |
| // MCL should try ask if there is pending input, and try to dequeue output. |
| ExpectIsAnyInputPending(false); |
| EXPECT_CALL(Codec(), DequeueOutputBuffer(_, _, _, _, _, _, _)) |
| .Times(1) |
| .WillOnce(Return(MEDIA_CODEC_TRY_AGAIN_LATER)); |
| codec_loop_->ExpectWork(); |
| WaitUntilIdle(ShouldNotBeIdle); |
| } |
| |
| TEST_F(MediaCodecLoopTest, TestPendingWorkWithInput) { |
| ConstructCodecLoop(); |
| // MCL should try ask if there is pending input, and try to dequeue both an |
| // output and input buffer. |
| ExpectIsAnyInputPending(true); |
| EXPECT_CALL(Codec(), DequeueOutputBuffer(_, _, _, _, _, _, _)).Times(1); |
| EXPECT_CALL(Codec(), DequeueInputBuffer(_, _)).Times(1); |
| codec_loop_->ExpectWork(); |
| WaitUntilIdle(ShouldNotBeIdle); |
| } |
| |
| TEST_F(MediaCodecLoopTest, TestPendingWorkWithOutputBuffer) { |
| ConstructCodecLoop(); |
| { |
| InSequence _s; |
| |
| // MCL will first request input, then try to dequeue output. |
| ExpectIsAnyInputPending(false); |
| OutputBuffer buf; |
| ExpectDequeueOutputBuffer(buf); |
| ExpectOnDecodedFrame(buf); |
| |
| // MCL will try again for another set of buffers before ExpectWork() |
| // returns. This is why we don't just leave them for WaitUntilIdle(). |
| ExpectEmptyIOLoop(); |
| } |
| codec_loop_->ExpectWork(); |
| WaitUntilIdle(ShouldNotBeIdle); |
| } |
| |
| TEST_F(MediaCodecLoopTest, TestQueueEos) { |
| // Test sending an EOS to MCL => MCB =dequeue EOS=> MCL . |
| ConstructCodecLoop(); |
| { |
| InSequence _s; |
| |
| ExpectIsAnyInputPending(true); |
| int input_buffer_index = 123; |
| ExpectDequeueInputBuffer(input_buffer_index); |
| |
| MediaCodecLoop::InputData data; |
| data.is_eos = true; |
| ExpectProvideInputData(data); |
| EXPECT_CALL(Codec(), QueueEOS(input_buffer_index)); |
| ExpectInputDataQueued(true); |
| |
| // Now send the EOS back on the output queue. |
| EosOutputBuffer eos; |
| ExpectDequeueOutputBuffer(eos); |
| EXPECT_CALL(Codec(), ReleaseOutputBuffer(eos.index, false)); |
| EXPECT_CALL(*client_, OnDecodedEos(_)).Times(1).WillOnce(Return(true)); |
| |
| // See TestUnqueuedEos. |
| EXPECT_CALL(Codec(), DequeueOutputBuffer(_, _, _, _, _, _, _)) |
| .Times(1) |
| .WillOnce(Return(MEDIA_CODEC_TRY_AGAIN_LATER)); |
| } |
| codec_loop_->ExpectWork(); |
| // Don't WaitUntilIdle() here. See TestUnqueuedEos. |
| } |
| |
| TEST_F(MediaCodecLoopTest, TestQueueEosFailure) { |
| // Test sending an EOS to MCL => MCB =dequeue EOS fails=> MCL error. |
| ConstructCodecLoop(); |
| { |
| InSequence _s; |
| |
| ExpectIsAnyInputPending(true); |
| int input_buffer_index = 123; |
| ExpectDequeueInputBuffer(input_buffer_index); |
| |
| MediaCodecLoop::InputData data; |
| data.is_eos = true; |
| ExpectProvideInputData(data); |
| EXPECT_CALL(Codec(), QueueEOS(input_buffer_index)); |
| ExpectInputDataQueued(true); |
| |
| // Now send the EOS back on the output queue. |
| EosOutputBuffer eos; |
| ExpectDequeueOutputBuffer(eos); |
| EXPECT_CALL(Codec(), ReleaseOutputBuffer(eos.index, false)); |
| EXPECT_CALL(*client_, OnDecodedEos(_)).Times(1).WillOnce(Return(false)); |
| EXPECT_CALL(*client_, OnCodecLoopError()).Times(1); |
| } |
| codec_loop_->ExpectWork(); |
| // Don't WaitUntilIdle() here. |
| } |
| |
| TEST_F(MediaCodecLoopTest, TestQueueInputData) { |
| // Send a buffer full of data into MCL and make sure that it gets queued with |
| // MediaCodecBridge correctly. |
| ConstructCodecLoop(); |
| { |
| InSequence _s; |
| |
| ExpectIsAnyInputPending(true); |
| int input_buffer_index = 123; |
| ExpectDequeueInputBuffer(input_buffer_index); |
| |
| MediaCodecLoop::InputData data = BigBuckBunny(); |
| ExpectProvideInputData(data); |
| |
| // MCL should send the buffer into MediaCodec and notify the client. |
| ExpectQueueInputBuffer(input_buffer_index, data); |
| ExpectInputDataQueued(true); |
| |
| // MCL will try to dequeue an output buffer too. |
| EXPECT_CALL(Codec(), DequeueOutputBuffer(_, _, _, _, _, _, _)) |
| .Times(1) |
| .WillOnce(Return(MEDIA_CODEC_TRY_AGAIN_LATER)); |
| |
| // ExpectWork will try again. |
| ExpectEmptyIOLoop(); |
| } |
| codec_loop_->ExpectWork(); |
| WaitUntilIdle(ShouldNotBeIdle); |
| } |
| |
| TEST_F(MediaCodecLoopTest, TestQueueInputDataFails) { |
| // Send a buffer full of data into MCL, but MediaCodecBridge fails to queue |
| // it successfully. |
| ConstructCodecLoop(); |
| { |
| InSequence _s; |
| |
| ExpectIsAnyInputPending(true); |
| int input_buffer_index = 123; |
| ExpectDequeueInputBuffer(input_buffer_index); |
| |
| MediaCodecLoop::InputData data = BigBuckBunny(); |
| ExpectProvideInputData(data); |
| |
| // MCL should send the buffer into MediaCodec and notify the client. |
| ExpectQueueInputBuffer(input_buffer_index, data, MEDIA_CODEC_ERROR); |
| ExpectInputDataQueued(false); |
| EXPECT_CALL(*client_, OnCodecLoopError()).Times(1); |
| } |
| codec_loop_->ExpectWork(); |
| // MCL is now in the error state. |
| } |
| |
| TEST_F(MediaCodecLoopTest, TestQueueInputDataTryAgain) { |
| // Signal that there is input pending, but don't provide an input buffer. |
| ConstructCodecLoop(); |
| { |
| InSequence _s; |
| |
| ExpectIsAnyInputPending(true); |
| ExpectDequeueInputBuffer(-1, MEDIA_CODEC_TRY_AGAIN_LATER); |
| // MCL will try for output too. |
| ExpectDequeueOutputBuffer(MEDIA_CODEC_TRY_AGAIN_LATER); |
| } |
| codec_loop_->ExpectWork(); |
| // Note that the client might not be allowed to change from "input pending" |
| // to "no input pending" without actually being asked for input. For now, |
| // MCL doesn't assume this. |
| WaitUntilIdle(ShouldNotBeIdle); |
| } |
| |
| TEST_F(MediaCodecLoopTest, TestSeveralPendingIOBuffers) { |
| // Provide several input and output buffers to MCL. |
| ConstructCodecLoop(); |
| int input_buffer_index = 123; |
| const int num_loops = 4; |
| |
| InSequence _s; |
| for (int i = 0; i < num_loops; i++, input_buffer_index++) { |
| ExpectIsAnyInputPending(true); |
| ExpectDequeueInputBuffer(input_buffer_index); |
| |
| MediaCodecLoop::InputData data = BigBuckBunny(); |
| ExpectProvideInputData(data); |
| |
| ExpectQueueInputBuffer(input_buffer_index, data); |
| ExpectInputDataQueued(true); |
| |
| OutputBuffer buffer; |
| buffer.index = i; |
| buffer.size += i; |
| buffer.pts = base::Seconds(i + 1); |
| ExpectDequeueOutputBuffer(buffer); |
| ExpectOnDecodedFrame(buffer); |
| } |
| |
| ExpectEmptyIOLoop(); |
| |
| codec_loop_->ExpectWork(); |
| } |
| |
| TEST_F(MediaCodecLoopTest, TestOnKeyAdded) { |
| ConstructCodecLoop(); |
| |
| int input_buffer_index = 123; |
| MediaCodecLoop::InputData data = BigBuckBunny(); |
| |
| // First provide input, but have MediaCodecBridge require a key. |
| { |
| InSequence _s; |
| |
| // First ExpectWork() |
| ExpectIsAnyInputPending(true); |
| ExpectDequeueInputBuffer(input_buffer_index); |
| |
| ExpectProvideInputData(data); |
| |
| // Notify MCL that it's missing the key. |
| ExpectQueueInputBuffer(input_buffer_index, data, MEDIA_CODEC_NO_KEY); |
| |
| EXPECT_CALL(*client_, OnWaiting(WaitingReason::kNoDecryptionKey)).Times(1); |
| |
| // MCL should now try for output buffers. |
| ExpectDequeueOutputBuffer(MEDIA_CODEC_TRY_AGAIN_LATER); |
| |
| // MCL will try again, since trying to queue the input buffer is considered |
| // doing work, for some reason. It would be nice to make this optional. |
| // Note that it should not ask us for more input, since it has not yet sent |
| // the buffer we just provided. |
| ExpectDequeueOutputBuffer(MEDIA_CODEC_TRY_AGAIN_LATER); |
| } |
| codec_loop_->ExpectWork(); |
| |
| // Try again, to be sure that MCL doesn't request more input. Note that this |
| // is also done in the above loop, but that one could be made optional. This |
| // forces MCL to try again as part of an entirely new ExpectWork cycle. |
| { |
| InSequence _s; |
| // MCL should only try for output buffers, since it's still waiting for a |
| // key to be added. |
| ExpectDequeueOutputBuffer(MEDIA_CODEC_TRY_AGAIN_LATER); |
| } |
| codec_loop_->ExpectWork(); |
| |
| // When we add the key, MCL will DoPending work again. This time, it should |
| // succeed since the key has been added. |
| { |
| InSequence _s; |
| // MCL should not retain the original pointer. |
| data.memory = nullptr; |
| ExpectQueueInputBuffer(input_buffer_index, data); |
| ExpectInputDataQueued(true); |
| ExpectDequeueOutputBuffer(MEDIA_CODEC_TRY_AGAIN_LATER); |
| |
| // MCL did work, so it will try again. |
| ExpectEmptyIOLoop(); |
| } |
| |
| codec_loop_->OnKeyAdded(); |
| WaitUntilIdle(ShouldNotBeIdle); |
| } |
| |
| } // namespace media |