| // 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/test/gmock_callback_support.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" |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| #include "chromeos/components/cdm_factory_daemon/chromeos_cdm_context.h" |
| #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 { |
| |
| 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, |
| StatusOr<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>()); |
| }; |
| // 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) |
| |
| 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; |
| StatusCode 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(Status)); |
| MOCK_METHOD1(OnOutput, void(scoped_refptr<VideoFrame>)); |
| MOCK_METHOD0(OnResetDone, void()); |
| MOCK_METHOD1(OnDecodeDone, void(Status)); |
| MOCK_METHOD1(OnWaiting, void(WaitingReason)); |
| |
| void SetCreateDecoderFunctionCB(VideoDecoderPipeline::CreateDecoderFunctionCB |
| function) NO_THREAD_SAFETY_ANALYSIS { |
| decoder_->create_decoder_function_cb_ = 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, |
| StatusCode 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), |
| StatusCode::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(OkStatus()); |
| })); |
| 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(OkStatus()); |
| })); |
| 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(StatusCode::kDecoderInitializationFailed); |
| })); |
| EXPECT_CALL(*decoder, NeedsTranscryption()).WillRepeatedly(Return(false)); |
| return std::move(decoder); |
| } |
| |
| VideoDecoderMixin* GetUnderlyingDecoder() NO_THREAD_SAFETY_ANALYSIS { |
| return decoder_->decoder_.get(); |
| } |
| |
| void DetachDecoderSequenceChecker() { |
| // |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_); |
| } |
| |
| void InvokeWaitingCB(WaitingReason reason) { |
| decoder_->decoder_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&VideoDecoderPipeline::OnDecoderWaiting, |
| base::Unretained(decoder_.get()), reason)); |
| } |
| |
| 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 == StatusCode::kOk, |
| !!GetUnderlyingDecoder()); |
| } |
| |
| const struct DecoderPipelineTestParams kDecoderPipelineTestParams[] = { |
| // A CreateDecoderFunctionCB that fails to Create() (i.e. returns a |
| // null Decoder) |
| {base::BindRepeating(&VideoDecoderPipelineTest::CreateNullMockDecoder), |
| StatusCode::kDecoderFailedCreation}, |
| |
| // A CreateDecoderFunctionCB that works fine, i.e. Create()s and |
| // Initialize()s correctly. |
| {base::BindRepeating(&VideoDecoderPipelineTest::CreateGoodMockDecoder), |
| StatusCode::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), |
| StatusCode::kDecoderMissingCdmForEncryptedContent}, |
| |
| // A CreateDecoderFunctionCB that Create()s ok but fails to Initialize() |
| // correctly. |
| {base::BindRepeating(&VideoDecoderPipelineTest::CreateBadMockDecoder), |
| StatusCode::kDecoderInitializationFailed}, |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| VideoDecoderPipelineTest, |
| testing::ValuesIn(kDecoderPipelineTestParams)); |
| |
| // Verifies the Reset sequence. |
| TEST_F(VideoDecoderPipelineTest, Reset) { |
| InitializeDecoder( |
| base::BindOnce(&VideoDecoderPipelineTest::CreateGoodMockDecoder), |
| StatusCode::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(OkStatus()); |
| }); |
| EXPECT_CALL(*this, OnDecodeDone(MatchesStatusCode(StatusCode::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(OkStatus()); |
| }); |
| EXPECT_CALL(*this, OnDecodeDone(MatchesStatusCode(StatusCode::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(DecodeStatus::ABORTED))) |
| .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(OkStatus()); |
| }); |
| EXPECT_CALL(*this, OnDecodeDone(MatchesStatusCode(StatusCode::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(OkStatus()); |
| }); |
| EXPECT_CALL(*this, OnDecodeDone(MatchesStatusCode(StatusCode::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(StatusCode::DECODE_ERROR))); |
| } |
| 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; |
| |
| const struct { |
| std::vector<std::pair<Fourcc, gfx::Size>> input_candidates; |
| std::pair<Fourcc, gfx::Size> expected_chosen_candidate; |
| } test_vectors[] = { |
| // Easy cases: one candidate that is supported, should be chosen. |
| {{std::pair<Fourcc, gfx::Size>(Fourcc::NV12, kSize)}, |
| std::pair<Fourcc, gfx::Size>(Fourcc::NV12, kSize)}, |
| {{std::pair<Fourcc, gfx::Size>(Fourcc::YV12, kSize)}, |
| std::pair<Fourcc, gfx::Size>(Fourcc::YV12, kSize)}, |
| {{std::pair<Fourcc, gfx::Size>(Fourcc::P010, kSize)}, |
| std::pair<Fourcc, gfx::Size>(Fourcc::P010, kSize)}, |
| // Two candidates, both supported: pick as per implementation. |
| {{std::pair<Fourcc, gfx::Size>(Fourcc::NV12, kSize), |
| std::pair<Fourcc, gfx::Size>(Fourcc::YV12, kSize)}, |
| std::pair<Fourcc, gfx::Size>(Fourcc::NV12, kSize)}, |
| {{std::pair<Fourcc, gfx::Size>(Fourcc::YV12, kSize), |
| std::pair<Fourcc, gfx::Size>(Fourcc::NV12, kSize)}, |
| std::pair<Fourcc, gfx::Size>(Fourcc::NV12, kSize)}, |
| {{std::pair<Fourcc, gfx::Size>(Fourcc::NV12, kSize), |
| std::pair<Fourcc, gfx::Size>(Fourcc::P010, kSize)}, |
| std::pair<Fourcc, gfx::Size>(Fourcc::NV12, kSize)}, |
| // Two candidates, only one supported, the supported one should be picked. |
| {{std::pair<Fourcc, gfx::Size>(Fourcc::YU16, kSize), |
| std::pair<Fourcc, gfx::Size>(Fourcc::NV12, kSize)}, |
| std::pair<Fourcc, gfx::Size>(Fourcc::NV12, kSize)}, |
| {{std::pair<Fourcc, gfx::Size>(Fourcc::YU16, kSize), |
| std::pair<Fourcc, gfx::Size>(Fourcc::YV12, kSize)}, |
| std::pair<Fourcc, gfx::Size>(Fourcc::YV12, kSize)}, |
| {{std::pair<Fourcc, gfx::Size>(Fourcc::YU16, kSize), |
| std::pair<Fourcc, gfx::Size>(Fourcc::P010, kSize)}, |
| std::pair<Fourcc, gfx::Size>(Fourcc::P010, kSize)}}; |
| |
| for (const auto& test_vector : test_vectors) { |
| const Fourcc& expected_fourcc = test_vector.expected_chosen_candidate.first; |
| const gfx::Size& expected_coded_size = |
| test_vector.expected_chosen_candidate.second; |
| 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=*/0u))); |
| 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); |
| ASSERT_TRUE(status_or_chosen_candidate.has_value()); |
| const auto chosen_candidate = std::move(status_or_chosen_candidate).value(); |
| EXPECT_EQ(test_vector.expected_chosen_candidate, chosen_candidate) |
| << " expected: " |
| << test_vector.expected_chosen_candidate.first.ToString() |
| << ", actual: " << chosen_candidate.first.ToString(); |
| testing::Mock::VerifyAndClearExpectations(pool_); |
| } |
| DetachDecoderSequenceChecker(); |
| } |
| |
| // 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), |
| StatusCode::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 |