// 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.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "media/gpu/mac/video_toolbox_av1_accelerator.h"
#include "base/numerics/safe_conversions.h"
#include "media/base/media_log.h"
#include "media/base/video_types.h"
#include "media/gpu/mac/vt_config_util.h"
#include "third_party/libgav1/src/src/obu_parser.h"
namespace media {
VideoToolboxAV1Accelerator::VideoToolboxAV1Accelerator(
std::unique_ptr<MediaLog> media_log,
std::optional<gfx::HDRMetadata> hdr_metadata,
DecodeCB decode_cb,
OutputCB output_cb)
: media_log_(std::move(media_log)),
hdr_metadata_(std::move(hdr_metadata)),
decode_cb_(std::move(decode_cb)),
output_cb_(std::move(output_cb)) {
DVLOG(1) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
VideoToolboxAV1Accelerator::~VideoToolboxAV1Accelerator() {
DVLOG(1) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
AV1Decoder::AV1Accelerator::Status VideoToolboxAV1Accelerator::SetStream(
base::span<const uint8_t> stream,
const DecryptConfig* decrypt_config) {
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
temporal_unit_data_.reset();
format_processed_ = false;
// Create block buffer.
OSStatus status = CMBlockBufferCreateEmpty(
/*structure_allocator=*/kCFAllocatorDefault,
/*sub_block_capacity=*/0,
/*flags=*/0, temporal_unit_data_.InitializeInto());
if (status != noErr) {
OSSTATUS_MEDIA_LOG(ERROR, status, media_log_.get())
<< "CMBlockBufferCreateWithMemoryBlock()";
return Status::kFail;
}
// Allocate memory.
status =
CMBlockBufferAppendMemoryBlock(/*the_buffer=*/temporal_unit_data_.get(),
/*memory_block=*/nullptr,
/*block_length=*/stream.size(),
/*block_allocator=*/kCFAllocatorDefault,
/*custom_block_source=*/nullptr,
/*offset_to_data=*/0,
/*data_length=*/stream.size(),
/*flags=*/0);
if (status != noErr) {
OSSTATUS_MEDIA_LOG(ERROR, status, media_log_.get())
<< "CMBlockBufferAppendMemoryBlock()";
return Status::kFail;
}
status = CMBlockBufferAssureBlockMemory(temporal_unit_data_.get());
if (status != noErr) {
OSSTATUS_MEDIA_LOG(ERROR, status, media_log_.get())
<< "CMBlockBufferAssureBlockMemory()";
return Status::kFail;
}
// Copy the data.
status = CMBlockBufferReplaceDataBytes(
/*sourceBytes=*/stream.data(),
/*destinationBuffer=*/temporal_unit_data_.get(),
/*offsetIntoDestination=*/0,
/*dataLength=*/stream.size());
if (status != noErr) {
OSSTATUS_MEDIA_LOG(ERROR, status, media_log_.get())
<< "CMBlockBufferReplaceDataBytes()";
return Status::kFail;
}
return Status::kOk;
}
scoped_refptr<AV1Picture> VideoToolboxAV1Accelerator::CreateAV1Picture(
bool apply_grain) {
DVLOG(4) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return base::MakeRefCounted<AV1Picture>();
}
VideoToolboxAV1Accelerator::Status VideoToolboxAV1Accelerator::SubmitDecode(
const AV1Picture& pic,
const libgav1::ObuSequenceHeader& sequence_header,
const AV1ReferenceFrameVector& ref_frames,
const libgav1::Vector<libgav1::TileBuffer>& tile_buffers,
base::span<const uint8_t> data) {
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// It's only necessary to process format the once per temporal unit.
if (format_processed_) {
return Status::kOk;
}
// Update `active_format_`.
if (!ProcessFormat(pic, sequence_header, data)) {
return Status::kFail;
}
format_processed_ = true;
return Status::kOk;
}
bool VideoToolboxAV1Accelerator::OutputPicture(const AV1Picture& pic) {
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!temporal_unit_data_ || !active_format_) {
return false;
}
// Wrap the temporal unit in a sample.
size_t data_size = CMBlockBufferGetDataLength(temporal_unit_data_.get());
base::apple::ScopedCFTypeRef<CMSampleBufferRef> sample;
OSStatus status = CMSampleBufferCreate(
/*allocator=*/kCFAllocatorDefault,
/*dataBuffer=*/temporal_unit_data_.get(),
/*dataReady=*/true,
/*makeDataReadyCallback=*/nullptr,
/*makeDataReadyRefcon=*/nullptr,
/*formatDescription=*/active_format_.get(),
/*numSamples=*/1,
/*numSampleTimingEntries=*/0,
/*sampleTimingArray=*/nullptr,
/*numSampleSizeEntries=*/1,
/*sampleSizeArray=*/&data_size, sample.InitializeInto());
if (status != noErr) {
OSSTATUS_MEDIA_LOG(ERROR, status, media_log_.get())
<< "CMSampleBufferCreate()";
return false;
}
// Submit for decoding.
// TODO(crbug.com/40227557): Replace all const ref AV1Picture with
// scoped_refptr.
decode_cb_.Run(std::move(sample), session_metadata_,
base::WrapRefCounted(const_cast<AV1Picture*>(&pic)));
// Schedule output.
output_cb_.Run(base::WrapRefCounted(const_cast<AV1Picture*>(&pic)));
temporal_unit_data_.reset();
return true;
}
bool VideoToolboxAV1Accelerator::ProcessFormat(
const AV1Picture& pic,
const libgav1::ObuSequenceHeader& sequence_header,
base::span<const uint8_t> data) {
DVLOG(4) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(crbug.com/40227557): Consider merging with CreateFormatExtensions() to
// avoid converting back and forth.
VideoColorSpace color_space = pic.get_colorspace();
VideoCodecProfile profile;
switch (sequence_header.profile) {
case libgav1::kProfile0:
profile = AV1PROFILE_PROFILE_MAIN;
break;
case libgav1::kProfile1:
profile = AV1PROFILE_PROFILE_HIGH;
break;
case libgav1::kProfile2:
profile = AV1PROFILE_PROFILE_PRO;
break;
default:
profile = VIDEO_CODEC_PROFILE_UNKNOWN;
break;
}
std::optional<gfx::HDRMetadata> hdr_metadata = pic.hdr_metadata();
if (!hdr_metadata) {
hdr_metadata = hdr_metadata_;
}
// TODO(crbug.com/40936765): Should this be the current frame size, or the
// sequence max frame size?
gfx::Size coded_size(base::strict_cast<int>(pic.frame_header.width),
base::strict_cast<int>(pic.frame_header.height));
// If the parameters have changed, generate a new format.
if (color_space != active_color_space_ || profile != active_profile_ ||
hdr_metadata != active_hdr_metadata_ ||
coded_size != active_coded_size_) {
active_format_.reset();
// Generate the av1c.
size_t av1c_size = 0;
std::unique_ptr<uint8_t[]> av1c =
libgav1::ObuParser::GetAV1CodecConfigurationBox(
data.data(), data.size(), &av1c_size);
base::span<const uint8_t> av1c_span =
base::make_span(av1c.get(), av1c_size);
// Build a format configuration with AV1 extensions.
base::apple::ScopedCFTypeRef<CFDictionaryRef> format_config =
CreateFormatExtensions(kCMVideoCodecType_AV1, profile,
sequence_header.color_config.bitdepth,
color_space, hdr_metadata, av1c_span);
if (!format_config) {
MEDIA_LOG(ERROR, media_log_.get())
<< "Failed to create format extensions";
return false;
}
// Create the format description.
base::apple::ScopedCFTypeRef<CMFormatDescriptionRef> format;
OSStatus status = CMVideoFormatDescriptionCreate(
/*allocator=*/kCFAllocatorDefault,
/*codecType=*/kCMVideoCodecType_AV1,
/*width=*/coded_size.width(),
/*height=*/coded_size.height(),
/*extensions=*/format_config.get(), active_format_.InitializeInto());
if (status != noErr) {
OSSTATUS_MEDIA_LOG(ERROR, status, media_log_.get())
<< "CMVideoFormatDescriptionCreate()";
return false;
}
// Save the configuration for later comparison.
active_color_space_ = color_space;
active_profile_ = profile;
active_hdr_metadata_ = hdr_metadata;
active_coded_size_ = coded_size;
// Update session configuration.
session_metadata_ = VideoToolboxDecompressionSessionMetadata{
/*allow_software_decoding=*/false,
/*bit_depth=*/
base::checked_cast<uint8_t>(sequence_header.color_config.bitdepth),
/*chroma_sampling=*/VideoChromaSampling::k420,
/*has_alpha=*/false,
/*visible_rect=*/pic.visible_rect()};
}
return true;
}
} // namespace media