// 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