// 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 #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 { 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()) {} ~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::TimeDelta::FromSeconds(30)); } void ConstructCodecLoop(int sdk_int = base::android::SDK_VERSION_LOLLIPOP) { std::unique_ptr 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( 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("big buck bunny"); data.length = 14; data.presentation_time = base::TimeDelta::FromSeconds(1); return data; } struct OutputBuffer { int index = 1; size_t offset = 0; size_t size = 1024; base::TimeDelta pts = base::TimeDelta::FromSeconds(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(codec_loop_->GetCodec()); } public: // Mocks the current thread's task runner which will also be used as the // MediaCodecLoop's task runner. scoped_refptr mock_task_runner_ = new base::TestMockTimeTaskRunner; base::ThreadTaskRunnerHandle task_runner_handle_; std::unique_ptr codec_loop_; std::unique_ptr client_; DISALLOW_COPY_AND_ASSIGN(MediaCodecLoopTest); }; TEST_F(MediaCodecLoopTest, TestConstructionWithNullCodec) { std::unique_ptr codec; EXPECT_CALL(*client_, OnCodecLoopError()).Times(1); const int sdk_int = base::android::SDK_VERSION_LOLLIPOP; codec_loop_ = std::make_unique( sdk_int, client_.get(), std::move(codec), scoped_refptr()); // 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::TimeDelta::FromSeconds(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