blob: 1e2eb8c86e7fca95c504b78bea99575d18d0e8ed [file] [log] [blame]
// 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 "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::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_(new StrictMock<MockMediaCodecLoopClient>()) {}
~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<MediaCodecBridge> codec(new MockMediaCodecBridge());
// Since we're providing a codec, we do not expect an error.
EXPECT_CALL(*client_, OnCodecLoopError()).Times(0);
codec_loop_.reset(new 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::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<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_;
DISALLOW_COPY_AND_ASSIGN(MediaCodecLoopTest);
};
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_.reset(
new 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::TimeDelta::FromSeconds(i + 1);
ExpectDequeueOutputBuffer(buffer);
ExpectOnDecodedFrame(buffer);
}
ExpectEmptyIOLoop();
codec_loop_->ExpectWork();
}
TEST_F(MediaCodecLoopTest, TestTryFlushOnJellyBeanMR2) {
// On JB MR2+ MCL should be willing to use MediaCodecBridge::Flush.
ConstructCodecLoop(base::android::SDK_VERSION_JELLY_BEAN_MR2);
EXPECT_CALL(Codec(), Flush()).Times(1).WillOnce(Return(MEDIA_CODEC_OK));
ASSERT_TRUE(codec_loop_->TryFlush());
}
TEST_F(MediaCodecLoopTest, TestTryFlushAfterJellyBeanMR2Fails) {
// On JB MR2+, MCL should be willing to use MediaCodecBridge::Flush. Try
// that, but make Flush fail.
ConstructCodecLoop(base::android::SDK_VERSION_JELLY_BEAN_MR2);
EXPECT_CALL(Codec(), Flush()).Times(1).WillOnce(Return(MEDIA_CODEC_ERROR));
EXPECT_CALL(*client_, OnCodecLoopError()).Times(1);
ASSERT_FALSE(codec_loop_->TryFlush());
}
TEST_F(MediaCodecLoopTest, TestTryFlushOnJellyBeanMR1) {
// In JB MR1, MCL should not be willing to use MediaCodecBridge::Flush.
ConstructCodecLoop(base::android::SDK_VERSION_JELLY_BEAN_MR1);
ASSERT_FALSE(codec_loop_->TryFlush());
}
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