chromium/media/gpu/mac/video_toolbox_decompression_session_manager_unittest.cc

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

#include <stdint.h>
#include <memory>

#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "media/base/decoder_status.h"
#include "media/base/media_util.h"
#include "media/gpu/mac/video_toolbox_decompression_metadata.h"
#include "media/gpu/mac/video_toolbox_decompression_session_manager.h"
#include "media/gpu/mac/video_toolbox_decompression_session.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using testing::_;

namespace media {

namespace {

MATCHER_P(MetadataEq, id, "") {
  return arg->timestamp == base::Microseconds(id);
}

std::unique_ptr<VideoToolboxDecodeMetadata> CreateMetadata(int id) {
  auto metadata = std::make_unique<VideoToolboxDecodeMetadata>();
  metadata->timestamp = base::Microseconds(id);
  return metadata;
}

base::apple::ScopedCFTypeRef<CMFormatDescriptionRef> CreateFormat() {
  base::apple::ScopedCFTypeRef<CMFormatDescriptionRef> format;
  OSStatus status =
      CMFormatDescriptionCreate(kCFAllocatorDefault, kCMMediaType_Video, 'test',
                                nullptr, format.InitializeInto());
  CHECK_EQ(status, noErr);
  return format;
}

base::apple::ScopedCFTypeRef<CMSampleBufferRef> CreateSample(
    CMFormatDescriptionRef format) {
  base::apple::ScopedCFTypeRef<CMSampleBufferRef> sample;
  OSStatus status = CMSampleBufferCreate(
      kCFAllocatorDefault, nullptr, true, nullptr, nullptr, format, 0, 0,
      nullptr, 0, nullptr, sample.InitializeInto());
  CHECK_EQ(status, noErr);
  return sample;
}

base::apple::ScopedCFTypeRef<CVImageBufferRef> CreateImage() {
  base::apple::ScopedCFTypeRef<CVImageBufferRef> image;
  OSStatus status =
      CVPixelBufferCreate(kCFAllocatorDefault, /*width=*/16, /*height=*/16,
                          kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
                          nullptr, image.InitializeInto());
  CHECK_EQ(status, noErr);
  return image;
}

class FakeDecompressionSession : public VideoToolboxDecompressionSession {
 public:
  explicit FakeDecompressionSession(
      VideoToolboxDecompressionSessionImpl::OutputCB output_cb)
      : output_cb_(std::move(output_cb)) {}

  ~FakeDecompressionSession() override = default;

  bool Create(CMFormatDescriptionRef format,
              CFDictionaryRef decoder_config,
              CFDictionaryRef image_config) override {
    CHECK(!IsValid());
    ++creations;
    if (can_create) {
      valid_ = true;
    }
    return can_create;
  }

  void Invalidate() override {
    valid_ = false;
    pending_decodes_ = {};
  }

  bool IsValid() override { return valid_; }

  bool CanAcceptFormat(CMFormatDescriptionRef format) override {
    CHECK(valid_);
    return can_accept_format;
  }

  bool DecodeFrame(CMSampleBufferRef sample, uintptr_t context) override {
    if (can_decode_frame) {
      pending_decodes_.push(context);
    }
    return can_decode_frame;
  }

  // Output the first pending decode, with an image.
  void CompleteDecode() {
    CHECK(!pending_decodes_.empty());

    uintptr_t context = pending_decodes_.front();
    OSStatus status = noErr;
    VTDecodeInfoFlags flags = 0;
    base::apple::ScopedCFTypeRef<CVImageBufferRef> image = CreateImage();

    pending_decodes_.pop();
    output_cb_.Run(context, status, flags, std::move(image));
  }

  // Output the first pending decode, with an error code.
  void FailDecode() {
    CHECK(!pending_decodes_.empty());

    uintptr_t context = pending_decodes_.front();
    OSStatus status = -1;
    VTDecodeInfoFlags flags = 0;
    base::apple::ScopedCFTypeRef<CVImageBufferRef> image;

    pending_decodes_.pop();
    output_cb_.Run(context, status, flags, std::move(image));
  }

  size_t NumDecodes() { return pending_decodes_.size(); }

  bool can_create = true;
  bool can_accept_format = true;
  bool can_decode_frame = true;

  size_t creations = 0;

 private:
  VideoToolboxDecompressionSessionImpl::OutputCB output_cb_;
  bool valid_ = false;
  base::queue<uintptr_t> pending_decodes_;
};

}  // namespace

class VideoToolboxDecompressionSessionManagerTest : public testing::Test {
 public:
  VideoToolboxDecompressionSessionManagerTest() {
    video_toolbox_.SetDecompressionSessionForTesting(
        base::WrapUnique(decompression_session_.get()));
  }

  ~VideoToolboxDecompressionSessionManagerTest() override = default;

 protected:
  MOCK_METHOD1(OnError, void(DecoderStatus));
  MOCK_METHOD2(OnOutput,
               void(base::apple::ScopedCFTypeRef<CVImageBufferRef>,
                    std::unique_ptr<VideoToolboxDecodeMetadata>));

  base::test::TaskEnvironment task_environment_;
  VideoToolboxDecompressionSessionManager video_toolbox_{
      task_environment_.GetMainThreadTaskRunner(),
      std::make_unique<NullMediaLog>(),
      base::BindRepeating(&VideoToolboxDecompressionSessionManagerTest::OnOutput,
                          base::Unretained(this)),
      base::BindOnce(&VideoToolboxDecompressionSessionManagerTest::OnError,
                     base::Unretained(this))};
  raw_ptr<FakeDecompressionSession> decompression_session_{
      new FakeDecompressionSession(
          base::BindRepeating(&VideoToolboxDecompressionSessionManager::OnOutput,
                              base::Unretained(&video_toolbox_)))};
};

TEST_F(VideoToolboxDecompressionSessionManagerTest, Construct) {}

TEST_F(VideoToolboxDecompressionSessionManagerTest, Decode) {
  auto format = CreateFormat();
  auto sample = CreateSample(format.get());
  auto metadata = CreateMetadata(0);

  video_toolbox_.Decode(sample, std::move(metadata));

  EXPECT_EQ(video_toolbox_.NumDecodes(), 1ul);
  EXPECT_EQ(decompression_session_->NumDecodes(), 1ul);

  EXPECT_CALL(*this, OnOutput(_, MetadataEq(0)));

  decompression_session_->CompleteDecode();

  task_environment_.RunUntilIdle();

  EXPECT_EQ(video_toolbox_.NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->creations, 1ul);
}

TEST_F(VideoToolboxDecompressionSessionManagerTest, CreateFailure) {
  auto format = CreateFormat();
  auto sample = CreateSample(format.get());
  auto metadata = CreateMetadata(0);

  decompression_session_->can_create = false;

  EXPECT_CALL(*this, OnError(_));

  video_toolbox_.Decode(sample, std::move(metadata));

  task_environment_.RunUntilIdle();

  EXPECT_EQ(video_toolbox_.NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->creations, 1ul);
}

TEST_F(VideoToolboxDecompressionSessionManagerTest, CompatibleFormatChange) {
  auto format0 = CreateFormat();
  auto format1 = CreateFormat();
  auto sample0 = CreateSample(format0.get());
  auto sample1 = CreateSample(format1.get());
  auto metadata0 = CreateMetadata(0);
  auto metadata1 = CreateMetadata(1);

  video_toolbox_.Decode(sample0, std::move(metadata0));
  video_toolbox_.Decode(sample1, std::move(metadata1));

  EXPECT_EQ(video_toolbox_.NumDecodes(), 2ul);
  EXPECT_EQ(decompression_session_->NumDecodes(), 2ul);

  EXPECT_CALL(*this, OnOutput(_, MetadataEq(0)));
  EXPECT_CALL(*this, OnOutput(_, MetadataEq(1)));

  decompression_session_->CompleteDecode();
  decompression_session_->CompleteDecode();

  task_environment_.RunUntilIdle();

  EXPECT_EQ(video_toolbox_.NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->creations, 1ul);
}

TEST_F(VideoToolboxDecompressionSessionManagerTest, IncompatibleFormatChange) {
  auto format0 = CreateFormat();
  auto format1 = CreateFormat();
  auto sample0 = CreateSample(format0.get());
  auto sample1 = CreateSample(format1.get());
  auto metadata0 = CreateMetadata(0);
  auto metadata1 = CreateMetadata(1);

  // CanAcceptFormat() is only called when necessary, so this only affects the
  // second sample.
  decompression_session_->can_accept_format = false;

  video_toolbox_.Decode(sample0, std::move(metadata0));
  video_toolbox_.Decode(sample1, std::move(metadata1));

  EXPECT_EQ(video_toolbox_.NumDecodes(), 2ul);
  // The second decode will not be started until after the first session is
  // invalidated (which happens after the first CompleteDecode()).
  EXPECT_EQ(decompression_session_->NumDecodes(), 1ul);

  EXPECT_CALL(*this, OnOutput(_, MetadataEq(0)));
  EXPECT_CALL(*this, OnOutput(_, MetadataEq(1)));

  decompression_session_->CompleteDecode();
  decompression_session_->CompleteDecode();

  task_environment_.RunUntilIdle();

  EXPECT_EQ(video_toolbox_.NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->creations, 2ul);
}

TEST_F(VideoToolboxDecompressionSessionManagerTest, DecodeError_Early) {
  auto format = CreateFormat();
  auto sample = CreateSample(format.get());
  auto metadata = CreateMetadata(0);

  decompression_session_->can_decode_frame = false;

  EXPECT_CALL(*this, OnError(_));

  video_toolbox_.Decode(sample, std::move(metadata));

  task_environment_.RunUntilIdle();

  EXPECT_EQ(video_toolbox_.NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->creations, 1ul);
}

TEST_F(VideoToolboxDecompressionSessionManagerTest, DecodeError_Late) {
  auto format = CreateFormat();
  auto sample = CreateSample(format.get());
  auto metadata = CreateMetadata(0);

  video_toolbox_.Decode(sample, std::move(metadata));

  EXPECT_EQ(video_toolbox_.NumDecodes(), 1ul);
  EXPECT_EQ(decompression_session_->NumDecodes(), 1ul);

  EXPECT_CALL(*this, OnError(_));

  decompression_session_->FailDecode();

  task_environment_.RunUntilIdle();

  EXPECT_EQ(video_toolbox_.NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->NumDecodes(), 0ul);
  EXPECT_EQ(decompression_session_->creations, 1ul);
}

}  // namespace media