chromium/media/gpu/vaapi/vaapi_video_decoder_unittest.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/gpu/vaapi/vaapi_video_decoder.h"

#if !BUILDFLAG(IS_CHROMEOS_ASH)
#error This file should only be built for Ash.
#endif

#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/synchronization/waitable_event.h"
#include "base/task/thread_pool.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/task_environment.h"
#include "chromeos/components/cdm_factory_daemon/chromeos_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/test_helpers.h"
#include "media/gpu/accelerated_video_decoder.h"
#include "media/gpu/vaapi/vaapi_video_decoder_delegate.h"
#include "media/gpu/vaapi/vaapi_wrapper.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using base::test::RunClosure;
using ::testing::_;
using ::testing::ByMove;
using testing::InSequence;
using ::testing::Return;
using ::testing::StrictMock;

namespace media {
namespace {
constexpr std::string kEmptyData = "";
constexpr std::string kInvalidData = "ThisIsInvalidData";
constexpr uint8_t kEncodedData[] = {1, 2, 3};

constexpr gfx::Size kCodedSize(128, 128);
VideoDecoderConfig DefaultVideoDecoderConfig() {
  const VideoDecoderConfig config(
      media::VideoCodec::kVP8, VP8PROFILE_ANY,
      VideoDecoderConfig::AlphaMode::kIsOpaque, VideoColorSpace(),
      kNoTransformation, kCodedSize, gfx::Rect(kCodedSize), kCodedSize,
      EmptyExtraData(), EncryptionScheme::kUnencrypted);
  DCHECK(config.IsValidConfig());
  return config;
}
}  // namespace

MATCHER_P(MatchesStatusCode, status_code, "") {
  return arg.code() == status_code;
}

scoped_refptr<DecoderBuffer> CreateDecoderBuffer(
    const base::span<const uint8_t>& bitstream) {
  scoped_refptr<DecoderBuffer> buffer = DecoderBuffer::CopyFrom(bitstream);
  EXPECT_NE(buffer.get(), nullptr);
  return buffer;
}

class MockVaapiWrapper : public VaapiWrapper {
 public:
  explicit MockVaapiWrapper(CodecMode mode)
      : VaapiWrapper(VADisplayStateHandle(), mode) {}

 private:
  ~MockVaapiWrapper() override = default;
};

class MockVideoDecoderMixinClient : public VideoDecoderMixin::Client {
 public:
  MOCK_METHOD(DmabufVideoFramePool*, GetVideoFramePool, (), (const, override));
  MOCK_METHOD(void, PrepareChangeResolution, (), (override));
  MOCK_METHOD(void, NotifyEstimatedMaxDecodeRequests, (int), (override));
  MOCK_METHOD(CroStatus::Or<ImageProcessor::PixelLayoutCandidate>,
              PickDecoderOutputFormat,
              (const std::vector<ImageProcessor::PixelLayoutCandidate>&,
               const gfx::Rect&,
               const gfx::Size&,
               std::optional<gfx::Size>,
               size_t,
               bool,
               bool,
               std::optional<DmabufVideoFramePool::CreateFrameCB>),
              (override));
  MOCK_METHOD(void, InitCallback, (DecoderStatus), ());

  base::WeakPtrFactory<MockVideoDecoderMixinClient> weak_ptr_factory_{this};
};

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_METHOD1(GetHwConfigData,
               void(chromeos::ChromeOsCdmContext::GetHwConfigDataCB));
  MOCK_METHOD1(GetScreenResolutions,
               void(chromeos::ChromeOsCdmContext::GetScreenResolutionsCB));
  MOCK_METHOD0(GetCdmContextRef, std::unique_ptr<CdmContextRef>());
  MOCK_CONST_METHOD0(UsingArcCdm, bool());
  MOCK_CONST_METHOD0(IsRemoteCdm, bool());
  MOCK_METHOD2(AllocateSecureBuffer,
               void(uint32_t,
                    chromeos::ChromeOsCdmContext::AllocateSecureBufferCB));
  MOCK_METHOD4(ParseEncryptedSliceHeader,
               void(uint64_t,
                    uint32_t,
                    const std::vector<uint8_t>&,
                    ParseEncryptedSliceHeaderCB));
};

class FakeCdmContextRef : public CdmContextRef {
 public:
  FakeCdmContextRef(CdmContext* cdm_context) : cdm_context_(cdm_context) {}
  ~FakeCdmContextRef() override = default;

  CdmContext* GetCdmContext() override { return cdm_context_; }

 private:
  raw_ptr<CdmContext> cdm_context_;
};

class MockAcceleratedVideoDecoder : public AcceleratedVideoDecoder {
 public:
  MockAcceleratedVideoDecoder() = default;
  ~MockAcceleratedVideoDecoder() override = default;

  MOCK_METHOD(void, SetStream, (int32_t, const DecoderBuffer&), (override));
  MOCK_METHOD(bool, Flush, (), (override));
  MOCK_METHOD(void, Reset, (), (override));
  MOCK_METHOD(DecodeResult, Decode, (), (override));
  MOCK_METHOD(gfx::Size, GetPicSize, (), (const, override));
  MOCK_METHOD(gfx::Rect, GetVisibleRect, (), (const, override));
  MOCK_METHOD(VideoCodecProfile, GetProfile, (), (const, override));
  MOCK_METHOD(uint8_t, GetBitDepth, (), (const, override));
  MOCK_METHOD(VideoChromaSampling, GetChromaSampling, (), (const, override));
  MOCK_METHOD(VideoColorSpace, GetVideoColorSpace, (), (const, override));
  MOCK_METHOD(std::optional<gfx::HDRMetadata>,
              GetHDRMetadata,
              (),
              (const, override));
  MOCK_METHOD(size_t, GetRequiredNumOfPictures, (), (const, override));
  MOCK_METHOD(size_t, GetNumReferenceFrames, (), (const, override));
};

class VaapiVideoDecoderTest : public ::testing::Test {
 public:
  VaapiVideoDecoderTest() = default;
  ~VaapiVideoDecoderTest() override = default;

  void SetUp() override {
    mock_vaapi_wrapper_ =
        base::MakeRefCounted<MockVaapiWrapper>(VaapiWrapper::kDecode);
    mock_vaapi_wrapper_->sequence_checker_.DetachFromSequence();
    ResetDecoder();
  }

  void ResetDecoder() {
    auto mock_accelerated_video_decoder =
        std::make_unique<StrictMock<MockAcceleratedVideoDecoder>>();
    mock_accelerated_video_decoder_ = mock_accelerated_video_decoder.get();
    decoder_ = VaapiVideoDecoder::Create(
        std::make_unique<media::NullMediaLog>(),
        base::SequencedTaskRunner::GetCurrentDefault(),
        client_.weak_ptr_factory_.GetWeakPtr());
    DCHECK_CALLED_ON_VALID_SEQUENCE(vaapi_decoder()->sequence_checker_);
    vaapi_decoder()->vaapi_wrapper_ = mock_vaapi_wrapper_;
    vaapi_decoder()->decoder_ = std::move(mock_accelerated_video_decoder);
  }

  void InitializeVaapiVideoDecoder(
      const DecoderStatus::Codes& status_code,
      const VideoDecoderConfig& config = DefaultVideoDecoderConfig(),
      CdmContext* cdm_context = nullptr) {
    base::RunLoop run_loop;
    EXPECT_CALL(client_, InitCallback(MatchesStatusCode(status_code)))
        .WillOnce(RunClosure(run_loop.QuitClosure()));
    vaapi_decoder()->Initialize(
        config, /*low_delay=*/false, /*cdm_context=*/cdm_context,
        base::BindOnce(&MockVideoDecoderMixinClient::InitCallback,
                       client_.weak_ptr_factory_.GetWeakPtr()),
        /*output_cb=*/base::DoNothing(),
        /*waiting_cb*/ base::DoNothing());
    ASSERT_NE(vaapi_decoder(), nullptr);
    DCHECK_CALLED_ON_VALID_SEQUENCE(vaapi_decoder()->sequence_checker_);
    ASSERT_NE(vaapi_decoder()->vaapi_wrapper_, nullptr);
    ASSERT_NE(vaapi_decoder()->decoder_, nullptr);
    run_loop.Run();
    testing::Mock::VerifyAndClearExpectations(this);
  }

  void Decode(scoped_refptr<DecoderBuffer> buffer,
              AcceleratedVideoDecoder::DecodeResult mock_decoder_result,
              DecoderStatus::Codes vaapi_decoder_status) {
    ASSERT_TRUE(mock_accelerated_video_decoder_);
    {
      InSequence sequence;
      EXPECT_CALL(*mock_accelerated_video_decoder_, SetStream(_, _));
      EXPECT_CALL(*mock_accelerated_video_decoder_, Decode())
          .WillOnce(Return(mock_decoder_result));
      EXPECT_CALL(*this,
                  OnDecodeCompleted(MatchesStatusCode(vaapi_decoder_status)));
    }
    vaapi_decoder()->Decode(
        std::move(buffer),
        base::BindOnce(&VaapiVideoDecoderTest::OnDecodeCompleted,
                       base::Unretained(this)));
    task_environment_.RunUntilIdle();
  }

  VaapiVideoDecoder* vaapi_decoder() {
    return reinterpret_cast<VaapiVideoDecoder*>(decoder_.get());
  }

  MOCK_METHOD(void, OnDecodeCompleted, (DecoderStatus), ());
  MOCK_METHOD(void, OnResetDone, (), ());

  MockCdmContext cdm_context_;
  MockChromeOsCdmContext chromeos_cdm_context_;
  media::CallbackRegistry<CdmContext::EventCB::RunType> event_callbacks_;
  std::unique_ptr<VideoDecoderMixin> decoder_;
  scoped_refptr<MockVaapiWrapper> mock_vaapi_wrapper_;
  base::test::SingleThreadTaskEnvironment task_environment_;
  MockVideoDecoderMixinClient client_;
  raw_ptr<StrictMock<MockAcceleratedVideoDecoder>>
      mock_accelerated_video_decoder_ = nullptr;
};

TEST_F(VaapiVideoDecoderTest, Initialize) {
  InitializeVaapiVideoDecoder(DecoderStatus::Codes::kOk);
  EXPECT_FALSE(vaapi_decoder()->NeedsTranscryption());
}

// Verifies that Initialize() fails when trying to decode encrypted content with
// a missing CdmContext.
TEST_F(VaapiVideoDecoderTest,
       InitializeFailsDueToMissingCdmContextForEncryptedContent) {
  InitializeVaapiVideoDecoder(
      DecoderStatus::Codes::kUnsupportedEncryptionMode,
      VideoDecoderConfig(VideoCodec::kVP8, VP8PROFILE_ANY,
                         VideoDecoderConfig::AlphaMode::kIsOpaque,
                         VideoColorSpace(), kNoTransformation, kCodedSize,
                         gfx::Rect(kCodedSize), kCodedSize, EmptyExtraData(),
                         EncryptionScheme::kCenc));
}

// Verifies that Initialize() fails when trying to decode encrypted content with
// VP8 video codec as it's not supported by VA-API.
TEST_F(VaapiVideoDecoderTest, InitializeFailsDueToEncryptedContentForVP8) {
  EXPECT_CALL(cdm_context_, GetChromeOsCdmContext())
      .WillRepeatedly(Return(&chromeos_cdm_context_));
  InitializeVaapiVideoDecoder(
      DecoderStatus::Codes::kUnsupportedEncryptionMode,
      VideoDecoderConfig(VideoCodec::kVP8, VP8PROFILE_ANY,
                         VideoDecoderConfig::AlphaMode::kIsOpaque,
                         VideoColorSpace(), kNoTransformation, kCodedSize,
                         gfx::Rect(kCodedSize), kCodedSize, EmptyExtraData(),
                         EncryptionScheme::kCenc),
      &cdm_context_);
  testing::Mock::VerifyAndClearExpectations(&chromeos_cdm_context_);
  testing::Mock::VerifyAndClearExpectations(&cdm_context_);
}

// Verifies that Initialize() succeeds for VP9 encrypted content.
TEST_F(VaapiVideoDecoderTest, InitializeForVP9EncryptedContent) {
  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(chromeos_cdm_context_, GetCdmContextRef())
      .WillOnce(
          Return(ByMove(std::make_unique<FakeCdmContextRef>(&cdm_context_))));
  EXPECT_CALL(chromeos_cdm_context_, IsRemoteCdm()).WillOnce(Return(false));
  InitializeVaapiVideoDecoder(
      DecoderStatus::Codes::kOk,
      VideoDecoderConfig(VideoCodec::kVP9, VP9PROFILE_PROFILE0,
                         VideoDecoderConfig::AlphaMode::kIsOpaque,
                         VideoColorSpace(), kNoTransformation, kCodedSize,
                         gfx::Rect(kCodedSize), kCodedSize, EmptyExtraData(),
                         EncryptionScheme::kCenc),
      &cdm_context_);
  EXPECT_FALSE(vaapi_decoder()->NeedsTranscryption());
  testing::Mock::VerifyAndClearExpectations(&chromeos_cdm_context_);
  testing::Mock::VerifyAndClearExpectations(&cdm_context_);
}

TEST_F(VaapiVideoDecoderTest, DecodeSucceeds) {
  InitializeVaapiVideoDecoder(DecoderStatus::Codes::kOk);
  EXPECT_FALSE(vaapi_decoder()->NeedsTranscryption());
  auto buffer = CreateDecoderBuffer(base::as_byte_span(kEncodedData));
  Decode(buffer, AcceleratedVideoDecoder::DecodeResult::kRanOutOfStreamData,
         DecoderStatus::Codes::kOk);
  testing::Mock::VerifyAndClearExpectations(mock_accelerated_video_decoder_);
}

TEST_F(VaapiVideoDecoderTest, DecodeFails) {
  InitializeVaapiVideoDecoder(DecoderStatus::Codes::kOk);
  EXPECT_FALSE(vaapi_decoder()->NeedsTranscryption());
  auto buffer = CreateDecoderBuffer(base::as_byte_span(kInvalidData));
  Decode(buffer, AcceleratedVideoDecoder::DecodeResult::kDecodeError,
         DecoderStatus::Codes::kFailed);
  testing::Mock::VerifyAndClearExpectations(mock_accelerated_video_decoder_);
}

// Verifies that kConfigChange event can be triggered correctly.
TEST_F(VaapiVideoDecoderTest, ConfigChange) {
  InitializeVaapiVideoDecoder(DecoderStatus::Codes::kOk);
  EXPECT_FALSE(vaapi_decoder()->NeedsTranscryption());
  auto buffer = CreateDecoderBuffer(base::as_byte_span(kEmptyData));
  Decode(buffer, AcceleratedVideoDecoder::DecodeResult::kRanOutOfStreamData,
         DecoderStatus::Codes::kOk);
  buffer = CreateDecoderBuffer(kEncodedData);
  EXPECT_CALL(client_, PrepareChangeResolution());
  Decode(buffer, AcceleratedVideoDecoder::DecodeResult::kConfigChange,
         DecoderStatus::Codes::kAborted);
  testing::Mock::VerifyAndClearExpectations(mock_accelerated_video_decoder_);
}

// Verifies the Reset sequence.
TEST_F(VaapiVideoDecoderTest, Reset) {
  InitializeVaapiVideoDecoder(DecoderStatus::Codes::kOk);
  EXPECT_FALSE(vaapi_decoder()->NeedsTranscryption());
  base::RunLoop run_loop;
  EXPECT_CALL(*mock_accelerated_video_decoder_, Reset());
  EXPECT_CALL(*this, OnResetDone())
      .WillOnce(RunClosure(run_loop.QuitClosure()));
  vaapi_decoder()->Reset(base::BindOnce(&VaapiVideoDecoderTest::OnResetDone,
                                        base::Unretained(this)));
  task_environment_.RunUntilIdle();
}

}  // namespace media