blob: 1362f9202cca111d4e86f901d63949e8e41dcc07 [file] [log] [blame]
// 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 "media/gpu/chromeos/video_decoder_pipeline.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/check_op.h"
#include "base/task/thread_pool.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "base/threading/thread_task_runner_handle.h"
#include "build/build_config.h"
#include "media/base/cdm_context.h"
#include "media/base/media_util.h"
#include "media/base/mock_filters.h"
#include "media/base/mock_media_log.h"
#include "media/base/status.h"
#include "media/base/video_decoder_config.h"
#include "media/gpu/chromeos/dmabuf_video_frame_pool.h"
#include "media/gpu/chromeos/mailbox_video_frame_converter.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/libdrm/src/include/drm/drm_fourcc.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
// gn check does not account for BUILDFLAG(), so including this header will
// make gn check fail for builds other than ash-chrome. See gn help nogncheck
// for more information.
#include "chromeos/components/cdm_factory_daemon/chromeos_cdm_context.h" // nogncheck
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
using base::test::RunClosure;
using ::testing::_;
using ::testing::ByMove;
using ::testing::InSequence;
using ::testing::Return;
using ::testing::StrictMock;
using ::testing::TestWithParam;
namespace media {
using PixelLayoutCandidate = ImageProcessor::PixelLayoutCandidate;
MATCHER_P(MatchesStatusCode, status_code, "") {
// media::Status doesn't provide an operator==(...), we add here a simple one.
return arg.code() == status_code;
}
class MockVideoFramePool : public DmabufVideoFramePool {
public:
MockVideoFramePool() = default;
~MockVideoFramePool() override = default;
// DmabufVideoFramePool implementation.
MOCK_METHOD6(Initialize,
CroStatus::Or<GpuBufferLayout>(const Fourcc&,
const gfx::Size&,
const gfx::Rect&,
const gfx::Size&,
size_t,
bool));
MOCK_METHOD0(GetFrame, scoped_refptr<VideoFrame>());
MOCK_METHOD0(IsExhausted, bool());
MOCK_METHOD1(NotifyWhenFrameAvailable, void(base::OnceClosure));
MOCK_METHOD0(ReleaseAllFrames, void());
};
constexpr gfx::Size kCodedSize(48, 36);
class MockDecoder : public VideoDecoderMixin {
public:
MockDecoder()
: VideoDecoderMixin(std::make_unique<MockMediaLog>(),
base::ThreadTaskRunnerHandle::Get(),
base::WeakPtr<VideoDecoderMixin::Client>(nullptr)) {}
~MockDecoder() override = default;
MOCK_METHOD6(Initialize,
void(const VideoDecoderConfig&,
bool,
CdmContext*,
InitCB,
const OutputCB&,
const WaitingCB&));
MOCK_METHOD2(Decode, void(scoped_refptr<DecoderBuffer>, DecodeCB));
MOCK_METHOD1(Reset, void(base::OnceClosure));
MOCK_METHOD0(ApplyResolutionChange, void());
MOCK_METHOD0(NeedsTranscryption, bool());
MOCK_CONST_METHOD0(GetDecoderType, VideoDecoderType());
};
#if BUILDFLAG(IS_CHROMEOS_ASH)
constexpr uint8_t kEncryptedData[] = {1, 8, 9};
constexpr uint8_t kTranscryptedData[] = {9, 2, 4};
class MockChromeOsCdmContext : public chromeos::ChromeOsCdmContext {
public:
MockChromeOsCdmContext() : chromeos::ChromeOsCdmContext() {}
~MockChromeOsCdmContext() override = default;
MOCK_METHOD3(GetHwKeyData,
void(const DecryptConfig*,
const std::vector<uint8_t>&,
chromeos::ChromeOsCdmContext::GetHwKeyDataCB));
MOCK_METHOD0(GetCdmContextRef, std::unique_ptr<CdmContextRef>());
MOCK_CONST_METHOD0(UsingArcCdm, bool());
};
// A real implementation of this class would actually hold onto a reference of
// the owner of the CdmContext to ensure it is not destructed before the
// CdmContextRef is destructed. For the tests here, we don't need to bother with
// that because the CdmContext is a class member declared before the
// VideoDecoderPipeline so the CdmContext will get destructed after what uses
// it.
class FakeCdmContextRef : public CdmContextRef {
public:
FakeCdmContextRef(CdmContext* cdm_context) : cdm_context_(cdm_context) {}
~FakeCdmContextRef() override = default;
CdmContext* GetCdmContext() override { return cdm_context_; }
private:
CdmContext* cdm_context_;
};
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
class MockImageProcessor : public ImageProcessor {
public:
explicit MockImageProcessor(
scoped_refptr<base::SequencedTaskRunner> client_task_runner)
: ImageProcessor(nullptr,
client_task_runner,
/*backend_task_runner=*/
base::ThreadPool::CreateSequencedTaskRunner({})) {}
MOCK_CONST_METHOD0(input_config, const ImageProcessorBackend::PortConfig&());
MOCK_CONST_METHOD0(output_config, const ImageProcessorBackend::PortConfig&());
};
struct DecoderPipelineTestParams {
// GTest params need to be copyable; hence we need here a RepeatingCallback
// version of VideoDecoderPipeline::CreateDecoderFunctionCB.
using RepeatingCreateDecoderFunctionCB = base::RepeatingCallback<
VideoDecoderPipeline::CreateDecoderFunctionCB::RunType>;
RepeatingCreateDecoderFunctionCB create_decoder_function_cb;
DecoderStatus::Codes status_code;
};
class VideoDecoderPipelineTest
: public testing::TestWithParam<DecoderPipelineTestParams> {
public:
VideoDecoderPipelineTest()
: config_(VideoCodec::kVP8,
VP8PROFILE_ANY,
VideoDecoderConfig::AlphaMode::kIsOpaque,
VideoColorSpace(),
kNoTransformation,
kCodedSize,
gfx::Rect(kCodedSize),
kCodedSize,
EmptyExtraData(),
EncryptionScheme::kUnencrypted),
converter_(new VideoFrameConverter) {
auto pool = std::make_unique<MockVideoFramePool>();
pool_ = pool.get();
decoder_ = base::WrapUnique(new VideoDecoderPipeline(
base::ThreadTaskRunnerHandle::Get(), std::move(pool),
std::move(converter_), std::make_unique<MockMediaLog>(),
// This callback needs to be configured in the individual tests.
base::BindOnce(&VideoDecoderPipelineTest::CreateNullMockDecoder)));
}
~VideoDecoderPipelineTest() override = default;
void TearDown() override {
VideoDecoderPipeline::DestroyAsync(std::move(decoder_));
task_environment_.RunUntilIdle();
}
MOCK_METHOD1(OnInit, void(DecoderStatus));
MOCK_METHOD1(OnOutput, void(scoped_refptr<VideoFrame>));
MOCK_METHOD0(OnResetDone, void());
MOCK_METHOD1(OnDecodeDone, void(DecoderStatus));
MOCK_METHOD1(OnWaiting, void(WaitingReason));
void SetCreateDecoderFunctionCB(VideoDecoderPipeline::CreateDecoderFunctionCB
function) NO_THREAD_SAFETY_ANALYSIS {
decoder_->create_decoder_function_cb_ = std::move(function);
}
void SetCreateImageProcessorCBForTesting(
VideoDecoderPipeline::CreateImageProcessorCBForTesting function)
NO_THREAD_SAFETY_ANALYSIS {
decoder_->create_image_processor_cb_for_testing_ = std::move(function);
}
// Constructs |decoder_| with a given |create_decoder_function_cb| and
// verifying |status_code| is received back in OnInit().
void InitializeDecoder(
VideoDecoderPipeline::CreateDecoderFunctionCB create_decoder_function_cb,
DecoderStatus::Codes status_code,
CdmContext* cdm_context = nullptr) {
SetCreateDecoderFunctionCB(std::move(create_decoder_function_cb));
base::RunLoop run_loop;
EXPECT_CALL(*this, OnInit(MatchesStatusCode(status_code)))
.WillOnce(RunClosure(run_loop.QuitClosure()));
decoder_->Initialize(
config_, false /* low_delay */, cdm_context,
base::BindOnce(&VideoDecoderPipelineTest::OnInit,
base::Unretained(this)),
base::BindRepeating(&VideoDecoderPipelineTest::OnOutput,
base::Unretained(this)),
base::BindRepeating(&VideoDecoderPipelineTest::OnWaiting,
base::Unretained(this)));
run_loop.Run();
testing::Mock::VerifyAndClearExpectations(this);
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
void InitializeForTranscrypt() {
decoder_->allow_encrypted_content_for_testing_ = true;
EXPECT_CALL(cdm_context_, GetChromeOsCdmContext())
.WillRepeatedly(Return(&chromeos_cdm_context_));
EXPECT_CALL(cdm_context_, RegisterEventCB(_))
.WillOnce([this](CdmContext::EventCB event_cb) {
return event_callbacks_.Register(std::move(event_cb));
});
EXPECT_CALL(cdm_context_, GetDecryptor())
.WillRepeatedly(Return(&decryptor_));
EXPECT_CALL(chromeos_cdm_context_, GetCdmContextRef())
.WillOnce(
Return(ByMove(std::make_unique<FakeCdmContextRef>(&cdm_context_))));
InitializeDecoder(
base::BindOnce(
&VideoDecoderPipelineTest::CreateGoodMockTranscryptDecoder),
DecoderStatus::Codes::kOk, &cdm_context_);
testing::Mock::VerifyAndClearExpectations(&chromeos_cdm_context_);
testing::Mock::VerifyAndClearExpectations(&cdm_context_);
// GetDecryptor() will be called again, so set that expectation.
EXPECT_CALL(cdm_context_, GetDecryptor())
.WillRepeatedly(Return(&decryptor_));
encrypted_buffer_ =
DecoderBuffer::CopyFrom(kEncryptedData, base::size(kEncryptedData));
transcrypted_buffer_ = DecoderBuffer::CopyFrom(
kTranscryptedData, base::size(kTranscryptedData));
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
static std::unique_ptr<VideoDecoderMixin> CreateNullMockDecoder(
std::unique_ptr<MediaLog> /* media_log */,
scoped_refptr<base::SequencedTaskRunner> /* decoder_task_runner */,
base::WeakPtr<VideoDecoderMixin::Client> /* client */) {
return nullptr;
}
// Creates a MockDecoder with an EXPECT_CALL on Initialize that returns ok.
static std::unique_ptr<VideoDecoderMixin> CreateGoodMockDecoder(
std::unique_ptr<MediaLog> /* media_log */,
scoped_refptr<base::SequencedTaskRunner> /* decoder_task_runner */,
base::WeakPtr<VideoDecoderMixin::Client> /* client */) {
std::unique_ptr<MockDecoder> decoder(new MockDecoder());
EXPECT_CALL(*decoder, Initialize(_, _, _, _, _, _))
.WillOnce(::testing::WithArgs<3>([](VideoDecoder::InitCB init_cb) {
std::move(init_cb).Run(DecoderStatus::Codes::kOk);
}));
EXPECT_CALL(*decoder, NeedsTranscryption()).WillRepeatedly(Return(false));
return std::move(decoder);
}
// Creates a MockDecoder with an EXPECT_CALL on Initialize that returns ok and
// also indicates that it requires transcryption.
static std::unique_ptr<VideoDecoderMixin> CreateGoodMockTranscryptDecoder(
std::unique_ptr<MediaLog> /* media_log */,
scoped_refptr<base::SequencedTaskRunner> /* decoder_task_runner */,
base::WeakPtr<VideoDecoderMixin::Client> /* client */) {
std::unique_ptr<MockDecoder> decoder(new MockDecoder());
EXPECT_CALL(*decoder, Initialize(_, _, _, _, _, _))
.WillOnce(::testing::WithArgs<3>([](VideoDecoder::InitCB init_cb) {
std::move(init_cb).Run(DecoderStatus::Codes::kOk);
}));
EXPECT_CALL(*decoder, NeedsTranscryption()).WillRepeatedly(Return(true));
return std::move(decoder);
}
// Creates a MockDecoder with an EXPECT_CALL on Initialize that returns error.
static std::unique_ptr<VideoDecoderMixin> CreateBadMockDecoder(
std::unique_ptr<MediaLog> /* media_log */,
scoped_refptr<base::SequencedTaskRunner> /* decoder_task_runner */,
base::WeakPtr<VideoDecoderMixin::Client> /* client */) {
std::unique_ptr<MockDecoder> decoder(new MockDecoder());
EXPECT_CALL(*decoder, Initialize(_, _, _, _, _, _))
.WillOnce(::testing::WithArgs<3>([](VideoDecoder::InitCB init_cb) {
std::move(init_cb).Run(DecoderStatus::Codes::kFailed);
}));
EXPECT_CALL(*decoder, NeedsTranscryption()).WillRepeatedly(Return(false));
return std::move(decoder);
}
VideoDecoderMixin* GetUnderlyingDecoder() NO_THREAD_SAFETY_ANALYSIS {
return decoder_->decoder_.get();
}
void DetachDecoderSequenceChecker() NO_THREAD_SAFETY_ANALYSIS {
// |decoder_| will be destroyed on its |decoder_task_runner| via
// DestroyAsync(). This will trip its |decoder_sequence_checker_| if it has
// been pegged to the test task runner, e.g. in PickDecoderOutputFormat().
// Since in that case we don't care about threading, just detach it.
DETACH_FROM_SEQUENCE(decoder_->decoder_sequence_checker_);
if (decoder_->image_processor_) {
// |decoder_->image_processor_->sequence_checker_| is pegged to the test
// task runner because |decoder_->image_processor_| is created when we
// call PickDecoderOutputFormat() from test code.
// |decoder_->image_processor_| is then destroyed on the decoder task
// runner because of VideoDecoderPipeline::DestroyAsync(). Thus the need
// for this detachment.
DETACH_FROM_SEQUENCE(decoder_->image_processor_->sequence_checker_);
}
}
void InvokeWaitingCB(WaitingReason reason) {
decoder_->decoder_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&VideoDecoderPipeline::OnDecoderWaiting,
base::Unretained(decoder_.get()), reason));
}
bool DecoderHasImageProcessor() NO_THREAD_SAFETY_ANALYSIS {
return !!decoder_->image_processor_;
}
scoped_refptr<base::SequencedTaskRunner> GetDecoderTaskRunner()
NO_THREAD_SAFETY_ANALYSIS {
return decoder_->decoder_task_runner_;
}
base::test::TaskEnvironment task_environment_;
const VideoDecoderConfig config_;
#if BUILDFLAG(IS_CHROMEOS_ASH)
MockCdmContext cdm_context_; // Keep this before |decoder_|.
MockChromeOsCdmContext chromeos_cdm_context_;
StrictMock<MockDecryptor> decryptor_;
scoped_refptr<DecoderBuffer> encrypted_buffer_;
scoped_refptr<DecoderBuffer> transcrypted_buffer_;
media::CallbackRegistry<CdmContext::EventCB::RunType> event_callbacks_;
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
std::unique_ptr<VideoFrameConverter> converter_;
std::unique_ptr<VideoDecoderPipeline> decoder_;
MockVideoFramePool* pool_;
};
// Verifies the status code for several typical CreateDecoderFunctionCB cases.
TEST_P(VideoDecoderPipelineTest, Initialize) {
InitializeDecoder(base::BindOnce(GetParam().create_decoder_function_cb),
GetParam().status_code);
EXPECT_EQ(GetParam().status_code == DecoderStatus::Codes::kOk,
!!GetUnderlyingDecoder());
}
const struct DecoderPipelineTestParams kDecoderPipelineTestParams[] = {
// A CreateDecoderFunctionCB that fails to Create() (i.e. returns a
// null Decoder)
{base::BindRepeating(&VideoDecoderPipelineTest::CreateNullMockDecoder),
DecoderStatus::Codes::kFailedToCreateDecoder},
// A CreateDecoderFunctionCB that works fine, i.e. Create()s and
// Initialize()s correctly.
{base::BindRepeating(&VideoDecoderPipelineTest::CreateGoodMockDecoder),
DecoderStatus::Codes::kOk},
// A CreateDecoderFunctionCB for transcryption, where Create() is ok, and
// the decoder will Initialize OK, but then the pipeline will not create the
// transcryptor due to a missing CdmContext. This will succeed if called
// through InitializeForTranscrypt where a CdmContext is set.
{base::BindRepeating(
&VideoDecoderPipelineTest::CreateGoodMockTranscryptDecoder),
DecoderStatus::Codes::kUnsupportedEncryptionMode},
// A CreateDecoderFunctionCB that Create()s ok but fails to Initialize()
// correctly.
{base::BindRepeating(&VideoDecoderPipelineTest::CreateBadMockDecoder),
DecoderStatus::Codes::kFailed},
};
INSTANTIATE_TEST_SUITE_P(All,
VideoDecoderPipelineTest,
testing::ValuesIn(kDecoderPipelineTestParams));
// Verifies the Reset sequence.
TEST_F(VideoDecoderPipelineTest, Reset) {
InitializeDecoder(
base::BindOnce(&VideoDecoderPipelineTest::CreateGoodMockDecoder),
DecoderStatus::Codes::kOk);
// When we call Reset(), we expect GetUnderlyingDecoder()'s Reset() method to
// be called, and when that method Run()s its argument closure, then
// OnResetDone() is expected to be called.
base::RunLoop run_loop;
EXPECT_CALL(*reinterpret_cast<MockDecoder*>(GetUnderlyingDecoder()), Reset(_))
.WillOnce(::testing::WithArgs<0>(
[](base::OnceClosure closure) { std::move(closure).Run(); }));
EXPECT_CALL(*this, OnResetDone())
.WillOnce(RunClosure(run_loop.QuitClosure()));
decoder_->Reset(base::BindOnce(&VideoDecoderPipelineTest::OnResetDone,
base::Unretained(this)));
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
TEST_F(VideoDecoderPipelineTest, TranscryptThenEos) {
InitializeForTranscrypt();
// First send in a DecoderBuffer.
{
InSequence sequence;
EXPECT_CALL(decryptor_, Decrypt(Decryptor::kVideo, encrypted_buffer_, _))
.WillOnce([this](Decryptor::StreamType stream_type,
scoped_refptr<DecoderBuffer> encrypted,
Decryptor::DecryptCB decrypt_cb) {
std::move(decrypt_cb).Run(Decryptor::kSuccess, transcrypted_buffer_);
});
EXPECT_CALL(*reinterpret_cast<MockDecoder*>(GetUnderlyingDecoder()),
Decode(transcrypted_buffer_, _))
.WillOnce([](scoped_refptr<DecoderBuffer> transcrypted,
VideoDecoderMixin::DecodeCB decode_cb) {
std::move(decode_cb).Run(DecoderStatus::Codes::kOk);
});
EXPECT_CALL(*this,
OnDecodeDone(MatchesStatusCode(DecoderStatus::Codes::kOk)));
}
decoder_->Decode(encrypted_buffer_,
base::BindOnce(&VideoDecoderPipelineTest::OnDecodeDone,
base::Unretained(this)));
task_environment_.RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&decryptor_);
testing::Mock::VerifyAndClearExpectations(
reinterpret_cast<MockDecoder*>(GetUnderlyingDecoder()));
testing::Mock::VerifyAndClearExpectations(this);
// Now send in the EOS, this should not invoke Decrypt.
scoped_refptr<DecoderBuffer> eos_buffer = DecoderBuffer::CreateEOSBuffer();
{
InSequence sequence;
EXPECT_CALL(*reinterpret_cast<MockDecoder*>(GetUnderlyingDecoder()),
Decode(eos_buffer, _))
.WillOnce([](scoped_refptr<DecoderBuffer> transcrypted,
VideoDecoderMixin::DecodeCB decode_cb) {
std::move(decode_cb).Run(DecoderStatus::Codes::kOk);
});
EXPECT_CALL(*this,
OnDecodeDone(MatchesStatusCode(DecoderStatus::Codes::kOk)));
}
decoder_->Decode(eos_buffer,
base::BindOnce(&VideoDecoderPipelineTest::OnDecodeDone,
base::Unretained(this)));
task_environment_.RunUntilIdle();
}
TEST_F(VideoDecoderPipelineTest, TranscryptReset) {
InitializeForTranscrypt();
scoped_refptr<DecoderBuffer> encrypted_buffer2 = DecoderBuffer::CopyFrom(
&kEncryptedData[1], base::size(kEncryptedData) - 1);
// Send in a buffer, but don't invoke the Decrypt callback so it stays as
// pending. Then send in 2 more buffers so they are in the queue.
EXPECT_CALL(decryptor_, Decrypt(Decryptor::kVideo, encrypted_buffer_, _))
.Times(1);
decoder_->Decode(encrypted_buffer_,
base::BindOnce(&VideoDecoderPipelineTest::OnDecodeDone,
base::Unretained(this)));
decoder_->Decode(encrypted_buffer2,
base::BindOnce(&VideoDecoderPipelineTest::OnDecodeDone,
base::Unretained(this)));
decoder_->Decode(encrypted_buffer2,
base::BindOnce(&VideoDecoderPipelineTest::OnDecodeDone,
base::Unretained(this)));
task_environment_.RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&decryptor_);
// Now when we reset, we should see 3 decode callbacks occur as well as the
// reset callback.
{
InSequence sequence;
EXPECT_CALL(*reinterpret_cast<MockDecoder*>(GetUnderlyingDecoder()),
Reset(_))
.WillOnce([](base::OnceClosure closure) { std::move(closure).Run(); });
EXPECT_CALL(*this,
OnDecodeDone(MatchesStatusCode(DecoderStatus::Codes::kAborted)))
.Times(3);
EXPECT_CALL(*this, OnResetDone()).Times(1);
}
decoder_->Reset(base::BindOnce(&VideoDecoderPipelineTest::OnResetDone,
base::Unretained(this)));
task_environment_.RunUntilIdle();
}
// Verifies that if we get notified about a new decrypt key while we are
// performing a transcrypt that fails w/out a key, we immediately retry again.
TEST_F(VideoDecoderPipelineTest, TranscryptKeyAddedDuringTranscrypt) {
InitializeForTranscrypt();
// First send in a buffer, which will go to the decryptor and hold on to that
// callback.
Decryptor::DecryptCB saved_decrypt_cb;
EXPECT_CALL(decryptor_, Decrypt(Decryptor::kVideo, encrypted_buffer_, _))
.WillOnce([&saved_decrypt_cb](Decryptor::StreamType stream_type,
scoped_refptr<DecoderBuffer> encrypted,
Decryptor::DecryptCB decrypt_cb) {
saved_decrypt_cb = BindToCurrentLoop(std::move(decrypt_cb));
});
decoder_->Decode(encrypted_buffer_,
base::BindOnce(&VideoDecoderPipelineTest::OnDecodeDone,
base::Unretained(this)));
task_environment_.RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&decryptor_);
// Now we invoke the CDM callback to indicate there is a new key available.
event_callbacks_.Notify(CdmContext::Event::kHasAdditionalUsableKey);
task_environment_.RunUntilIdle();
// Now we have the decryptor callback return with kNoKey which should then
// cause another call into the decryptor which we will have succeed and then
// that should go through decoding. This should not invoke the waiting CB.
{
InSequence sequence;
EXPECT_CALL(decryptor_, Decrypt(Decryptor::kVideo, encrypted_buffer_, _))
.WillOnce([this](Decryptor::StreamType stream_type,
scoped_refptr<DecoderBuffer> encrypted,
Decryptor::DecryptCB decrypt_cb) {
std::move(decrypt_cb).Run(Decryptor::kSuccess, transcrypted_buffer_);
});
EXPECT_CALL(*reinterpret_cast<MockDecoder*>(GetUnderlyingDecoder()),
Decode(transcrypted_buffer_, _))
.WillOnce([](scoped_refptr<DecoderBuffer> transcrypted,
VideoDecoderMixin::DecodeCB decode_cb) {
std::move(decode_cb).Run(DecoderStatus::Codes::kOk);
});
EXPECT_CALL(*this,
OnDecodeDone(MatchesStatusCode(DecoderStatus::Codes::kOk)));
}
EXPECT_CALL(*this, OnWaiting(_)).Times(0);
std::move(saved_decrypt_cb).Run(Decryptor::kNoKey, nullptr);
task_environment_.RunUntilIdle();
}
// Verifies that if we don't have the key during transcrypt, the WaitingCB is
// invoked and then it retries again when we notify it of the new key.
TEST_F(VideoDecoderPipelineTest, TranscryptNoKeyWaitRetry) {
InitializeForTranscrypt();
// First send in a buffer, which will go to the decryptor and indicate there
// is no key. This should also invoke the WaitingCB.
{
InSequence sequence;
EXPECT_CALL(decryptor_, Decrypt(Decryptor::kVideo, encrypted_buffer_, _))
.WillOnce([](Decryptor::StreamType stream_type,
scoped_refptr<DecoderBuffer> encrypted,
Decryptor::DecryptCB decrypt_cb) {
std::move(decrypt_cb).Run(Decryptor::kNoKey, nullptr);
});
EXPECT_CALL(*this, OnWaiting(WaitingReason::kNoDecryptionKey)).Times(1);
}
decoder_->Decode(encrypted_buffer_,
base::BindOnce(&VideoDecoderPipelineTest::OnDecodeDone,
base::Unretained(this)));
task_environment_.RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&decryptor_);
testing::Mock::VerifyAndClearExpectations(this);
// Now we invoke the CDM callback to indicate there is a new key available.
// This should invoke the decryptor again which we will have succeed and
// complete the decode operation.
{
InSequence sequence;
EXPECT_CALL(decryptor_, Decrypt(Decryptor::kVideo, encrypted_buffer_, _))
.WillOnce([this](Decryptor::StreamType stream_type,
scoped_refptr<DecoderBuffer> encrypted,
Decryptor::DecryptCB decrypt_cb) {
std::move(decrypt_cb).Run(Decryptor::kSuccess, transcrypted_buffer_);
});
EXPECT_CALL(*reinterpret_cast<MockDecoder*>(GetUnderlyingDecoder()),
Decode(transcrypted_buffer_, _))
.WillOnce([](scoped_refptr<DecoderBuffer> transcrypted,
VideoDecoderMixin::DecodeCB decode_cb) {
std::move(decode_cb).Run(DecoderStatus::Codes::kOk);
});
EXPECT_CALL(*this,
OnDecodeDone(MatchesStatusCode(DecoderStatus::Codes::kOk)));
}
event_callbacks_.Notify(CdmContext::Event::kHasAdditionalUsableKey);
task_environment_.RunUntilIdle();
}
TEST_F(VideoDecoderPipelineTest, TranscryptError) {
InitializeForTranscrypt();
{
InSequence sequence;
EXPECT_CALL(decryptor_, Decrypt(Decryptor::kVideo, encrypted_buffer_, _))
.WillOnce([](Decryptor::StreamType stream_type,
scoped_refptr<DecoderBuffer> encrypted,
Decryptor::DecryptCB decrypt_cb) {
std::move(decrypt_cb).Run(Decryptor::kError, nullptr);
});
EXPECT_CALL(*this,
OnDecodeDone(MatchesStatusCode(DecoderStatus::Codes::kFailed)));
}
decoder_->Decode(encrypted_buffer_,
base::BindOnce(&VideoDecoderPipelineTest::OnDecodeDone,
base::Unretained(this)));
task_environment_.RunUntilIdle();
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
// Verifies the algorithm for choosing formats in PickDecoderOutputFormat works
// as expected.
TEST_F(VideoDecoderPipelineTest, PickDecoderOutputFormat) {
constexpr gfx::Size kSize(320, 240);
constexpr gfx::Rect kVisibleRect(320, 240);
constexpr size_t kMaxNumOfFrames = 4u;
constexpr uint64_t kModifier = ~DRM_FORMAT_MOD_LINEAR;
const struct {
std::vector<PixelLayoutCandidate> input_candidates;
PixelLayoutCandidate expected_chosen_candidate;
} test_vectors[] = {
// Easy cases: one candidate that is supported, should be chosen.
{{PixelLayoutCandidate{Fourcc(Fourcc::NV12), kSize, kModifier}},
PixelLayoutCandidate{Fourcc(Fourcc::NV12), kSize, kModifier}},
{{PixelLayoutCandidate{Fourcc(Fourcc::YV12), kSize, kModifier}},
PixelLayoutCandidate{Fourcc(Fourcc::YV12), kSize, kModifier}},
{{PixelLayoutCandidate{Fourcc(Fourcc::P010), kSize, kModifier}},
PixelLayoutCandidate{Fourcc(Fourcc::P010), kSize, kModifier}},
// Two candidates, both supported: pick as per implementation.
{{PixelLayoutCandidate{Fourcc(Fourcc::NV12), kSize, kModifier},
PixelLayoutCandidate{Fourcc(Fourcc::YV12), kSize, kModifier}},
PixelLayoutCandidate{Fourcc(Fourcc::NV12), kSize, kModifier}},
{{PixelLayoutCandidate{Fourcc(Fourcc::YV12), kSize, kModifier},
PixelLayoutCandidate{Fourcc(Fourcc::NV12), kSize, kModifier}},
PixelLayoutCandidate{Fourcc(Fourcc::NV12), kSize, kModifier}},
{{PixelLayoutCandidate{Fourcc(Fourcc::NV12), kSize, kModifier},
PixelLayoutCandidate{Fourcc(Fourcc::P010), kSize, kModifier}},
PixelLayoutCandidate{Fourcc(Fourcc::NV12), kSize, kModifier}},
// Two candidates, only one supported, the supported one should be picked.
{{PixelLayoutCandidate{Fourcc(Fourcc::YU16), kSize, kModifier},
PixelLayoutCandidate{Fourcc(Fourcc::NV12), kSize, kModifier}},
PixelLayoutCandidate{Fourcc(Fourcc::NV12), kSize, kModifier}},
{{PixelLayoutCandidate{Fourcc(Fourcc::YU16), kSize, kModifier},
PixelLayoutCandidate{Fourcc(Fourcc::YV12), kSize, kModifier}},
PixelLayoutCandidate{Fourcc(Fourcc::YV12), kSize, kModifier}},
{{PixelLayoutCandidate{Fourcc(Fourcc::YU16), kSize, kModifier},
PixelLayoutCandidate{Fourcc(Fourcc::P010), kSize, kModifier}},
PixelLayoutCandidate{Fourcc(Fourcc::P010), kSize, kModifier}}};
for (const auto& test_vector : test_vectors) {
const Fourcc& expected_fourcc =
test_vector.expected_chosen_candidate.fourcc;
const gfx::Size& expected_coded_size =
test_vector.expected_chosen_candidate.size;
std::vector<ColorPlaneLayout> planes(
VideoFrame::NumPlanes(expected_fourcc.ToVideoPixelFormat()));
EXPECT_CALL(*pool_,
Initialize(expected_fourcc, expected_coded_size, kVisibleRect,
/*natural_size=*/kVisibleRect.size(),
kMaxNumOfFrames, /*use_protected=*/false))
.WillOnce(Return(*GpuBufferLayout::Create(
expected_fourcc, expected_coded_size, std::move(planes),
/*modifier=*/kModifier)));
auto status_or_chosen_candidate = decoder_->PickDecoderOutputFormat(
test_vector.input_candidates, kVisibleRect,
/*decoder_natural_size=*/kVisibleRect.size(),
/*output_size=*/absl::nullopt, /*num_of_pictures=*/kMaxNumOfFrames,
/*use_protected=*/false, /*need_aux_frame_pool=*/false, absl::nullopt);
ASSERT_TRUE(status_or_chosen_candidate.has_value());
const PixelLayoutCandidate chosen_candidate =
std::move(status_or_chosen_candidate).value();
EXPECT_EQ(test_vector.expected_chosen_candidate, chosen_candidate)
<< " expected: "
<< test_vector.expected_chosen_candidate.fourcc.ToString()
<< ", actual: " << chosen_candidate.fourcc.ToString();
EXPECT_FALSE(DecoderHasImageProcessor());
testing::Mock::VerifyAndClearExpectations(pool_);
}
DetachDecoderSequenceChecker();
}
// These tests only work on non-linux vaapi systems, since on linux, there is no
// support for different modifiers.
#if BUILDFLAG(USE_VAAPI) && !BUILDFLAG(IS_LINUX)
// Verifies the algorithm for choosing formats in PickDecoderOutputFormat works
// as expected when the pool returns linear buffers. It should allocate an image
// processor in those cases.
TEST_F(VideoDecoderPipelineTest, PickDecoderOutputFormatLinearModifier) {
constexpr gfx::Size kSize(320, 240);
constexpr gfx::Rect kVisibleRect(320, 240);
constexpr size_t kMaxNumOfFrames = 4u;
const Fourcc kFourcc(Fourcc::NV12);
auto image_processor =
std::make_unique<MockImageProcessor>(GetDecoderTaskRunner());
ImageProcessorBackend::PortConfig port_config(
Fourcc(Fourcc::NV12), gfx::Size(320, 240), {}, gfx::Rect(320, 240), {});
EXPECT_CALL(*image_processor, input_config())
.WillRepeatedly(testing::ReturnRef(port_config));
EXPECT_CALL(*image_processor, output_config())
.WillRepeatedly(testing::ReturnRef(port_config));
base::MockCallback<VideoDecoderPipeline::CreateImageProcessorCBForTesting>
image_processor_cb;
EXPECT_CALL(image_processor_cb, Run(_, _, kSize, _))
.WillOnce(Return(testing::ByMove(std::move(image_processor))));
SetCreateImageProcessorCBForTesting(image_processor_cb.Get());
// Modifier should be the linear format.
GpuBufferLayout gpu_buffer_layout = *GpuBufferLayout::Create(
kFourcc, kSize,
std::vector<ColorPlaneLayout>(
VideoFrame::NumPlanes(kFourcc.ToVideoPixelFormat())),
/*modifier=*/DRM_FORMAT_MOD_LINEAR);
EXPECT_CALL(*pool_, Initialize(_, _, _, _, _, _))
.WillRepeatedly(Return(gpu_buffer_layout));
PixelLayoutCandidate candidate{Fourcc(Fourcc::NV12), kSize,
/*modifier=*/~DRM_FORMAT_MOD_LINEAR};
auto status_or_chosen_candidate = decoder_->PickDecoderOutputFormat(
{candidate}, kVisibleRect,
/*decoder_natural_size=*/kVisibleRect.size(),
/*output_size=*/absl::nullopt, /*num_of_pictures=*/kMaxNumOfFrames,
/*use_protected=*/false, /*need_aux_frame_pool=*/false, absl::nullopt);
EXPECT_TRUE(status_or_chosen_candidate.has_value());
// Main concern is that the image processor was set.
EXPECT_TRUE(DecoderHasImageProcessor());
DetachDecoderSequenceChecker();
}
// Verifies the algorithm for choosing formats in PickDecoderOutputFormat works
// as expected when the frame pool returns buffers that have an unsupported
// modifier.
TEST_F(VideoDecoderPipelineTest, PickDecoderOutputFormatUnsupportedModifier) {
constexpr gfx::Size kSize(320, 240);
constexpr gfx::Rect kVisibleRect(320, 240);
constexpr size_t kMaxNumOfFrames = 4u;
const Fourcc kFourcc(Fourcc::NV12);
// Modifier is *not* the linear format.
GpuBufferLayout gpu_buffer_layout = *GpuBufferLayout::Create(
kFourcc, kSize,
std::vector<ColorPlaneLayout>(
VideoFrame::NumPlanes(kFourcc.ToVideoPixelFormat())),
/*modifier=*/~DRM_FORMAT_MOD_LINEAR);
EXPECT_CALL(*pool_, Initialize(_, _, _, _, _, _))
.WillRepeatedly(Return(gpu_buffer_layout));
// Make sure the modifier mismatches the |gpu_buffer_layout|'s
constexpr uint64_t modifier = ~DRM_FORMAT_MOD_LINEAR + 1;
PixelLayoutCandidate candidate{Fourcc(Fourcc::NV12), kSize, modifier};
auto status_or_chosen_candidate = decoder_->PickDecoderOutputFormat(
{candidate}, kVisibleRect,
/*decoder_natural_size=*/kVisibleRect.size(),
/*output_size=*/absl::nullopt, /*num_of_pictures=*/kMaxNumOfFrames,
/*use_protected=*/false, /*need_aux_frame_pool=*/false, absl::nullopt);
EXPECT_TRUE(status_or_chosen_candidate.has_error());
EXPECT_FALSE(DecoderHasImageProcessor());
DetachDecoderSequenceChecker();
}
#endif // BUILDFLAG(USE_VAAPI) && !BUILDFLAG(IS_LINUX)
// Verifies that ReleaseAllFrames is called on the frame pool when we receive
// the kDecoderStateLost event through the waiting callback. This can occur
// during protected content playback on Intel.
TEST_F(VideoDecoderPipelineTest, RebuildFramePoolsOnStateLost) {
InitializeDecoder(
base::BindOnce(&VideoDecoderPipelineTest::CreateGoodMockDecoder),
DecoderStatus::Codes::kOk);
// Simulate the waiting callback from the decoder for kDecoderStateLost.
EXPECT_CALL(*this, OnWaiting(media::WaitingReason::kDecoderStateLost));
InvokeWaitingCB(media::WaitingReason::kDecoderStateLost);
task_environment_.RunUntilIdle();
// Invoke Reset() as a client would do, and we then expect that to invoke the
// method to rebuild the frame pool.
EXPECT_CALL(*reinterpret_cast<MockDecoder*>(GetUnderlyingDecoder()), Reset(_))
.WillOnce(::testing::WithArgs<0>(
[](base::OnceClosure closure) { std::move(closure).Run(); }));
EXPECT_CALL(*this, OnResetDone());
EXPECT_CALL(*pool_, ReleaseAllFrames());
decoder_->Reset(base::BindOnce(&VideoDecoderPipelineTest::OnResetDone,
base::Unretained(this)));
task_environment_.RunUntilIdle();
}
} // namespace media