blob: 392859ae290820ae736f6ed848af8d41a3bbc151 [file]
// Copyright 2016 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/mac/vt_video_encode_accelerator_mac.h"
#include <memory>
#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/mac_logging.h"
#include "base/memory/shared_memory_mapping.h"
#include "base/memory/unsafe_shared_memory_region.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "media/base/bitrate.h"
#include "media/base/bitstream_buffer.h"
#include "media/base/mac/color_space_util_mac.h"
#include "media/base/mac/video_frame_mac.h"
#include "media/base/media_log.h"
#include "media/base/media_switches.h"
#include "media/base/video_codecs.h"
#include "media/base/video_frame.h"
#include "media/base/video_types.h"
#include "media/video/video_encode_accelerator.h"
// This is a min version of macOS where we want to support SVC encoding via
// EnableLowLatencyRateControl flag. The flag is actually supported since 11.3,
// but there we see frame drops even with ample bitrate budget. Excessive frame
// drops were fixed in 12.0.1.
#define LOW_LATENCY_FLAG_AVAILABLE_VER 12.0.1
namespace media {
namespace {
constexpr size_t kBitsPerByte = 8;
constexpr size_t kDefaultFrameRateNumerator = 30;
constexpr size_t kDefaultFrameRateDenominator = 1;
constexpr size_t kMaxFrameRateNumerator = 120;
constexpr size_t kMaxFrameRateDenominator = 1;
constexpr size_t kNumInputBuffers = 3;
constexpr gfx::Size kDefaultSupportedResolution = gfx::Size(640, 480);
// TODO(crbug.com/1380682): We should add a function like a
// `GetVideoEncodeAcceleratorProfileIsSupported`, to test the
// real support status with a give resolution, framerate etc,
// instead of query a "supportedProfile" list.
constexpr gfx::Size kMaxSupportedResolution = gfx::Size(4096, 2304);
constexpr VideoCodecProfile kSupportedProfiles[] = {
H264PROFILE_BASELINE,
H264PROFILE_MAIN,
H264PROFILE_HIGH,
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
// macOS actually start supporting HEVC since macOS 10.13+, but we only
// support decoding HEVC on macOS 11.0+ due to the failure of create a
// decompression session on some device, so limit this to macOS 11.0 as
// well.
HEVCPROFILE_MAIN,
#endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
};
static CFStringRef VideoCodecProfileToVTProfile(VideoCodecProfile profile) {
switch (profile) {
case H264PROFILE_BASELINE:
return kVTProfileLevel_H264_Baseline_AutoLevel;
case H264PROFILE_MAIN:
return kVTProfileLevel_H264_Main_AutoLevel;
case H264PROFILE_HIGH:
return kVTProfileLevel_H264_High_AutoLevel;
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
case HEVCPROFILE_MAIN:
return kVTProfileLevel_HEVC_Main_AutoLevel;
#endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
default:
NOTREACHED();
}
return kVTProfileLevel_H264_Baseline_AutoLevel;
}
static CMVideoCodecType VideoCodecToCMVideoCodec(VideoCodec codec) {
switch (codec) {
case VideoCodec::kH264:
return kCMVideoCodecType_H264;
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
case VideoCodec::kHEVC:
return kCMVideoCodecType_HEVC;
#endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
default:
NOTREACHED();
}
return kCMVideoCodecType_H264;
}
base::ScopedCFTypeRef<CFArrayRef> CreateRateLimitArray(const Bitrate& bitrate) {
std::vector<CFNumberRef> limits;
switch (bitrate.mode()) {
case Bitrate::Mode::kConstant: {
// CBR should be enforces with granularity of a second.
float target_interval = 1.0;
int32_t target_bitrate = bitrate.target_bps() / kBitsPerByte;
limits.push_back(
CFNumberCreate(nullptr, kCFNumberSInt32Type, &target_bitrate));
limits.push_back(
CFNumberCreate(nullptr, kCFNumberFloat32Type, &target_interval));
break;
}
case Bitrate::Mode::kVariable: {
// 5 seconds should be an okay interval for VBR to enforce the long-term
// limit.
float avg_interval = 5.0;
int32_t avg_bitrate = base::saturated_cast<int32_t>(
bitrate.target_bps() / kBitsPerByte * avg_interval);
// And the peak bitrate is measured per-second in a way similar to CBR.
float peak_interval = 1.0;
int32_t peak_bitrate = bitrate.peak_bps() / kBitsPerByte;
limits.push_back(
CFNumberCreate(nullptr, kCFNumberSInt32Type, &peak_bitrate));
limits.push_back(
CFNumberCreate(nullptr, kCFNumberFloat32Type, &peak_interval));
limits.push_back(
CFNumberCreate(nullptr, kCFNumberSInt32Type, &avg_bitrate));
limits.push_back(
CFNumberCreate(nullptr, kCFNumberFloat32Type, &avg_interval));
break;
}
default:
NOTREACHED();
}
base::ScopedCFTypeRef<CFArrayRef> result(CFArrayCreate(
kCFAllocatorDefault, reinterpret_cast<const void**>(limits.data()),
limits.size(), &kCFTypeArrayCallBacks));
for (auto* number : limits)
CFRelease(number);
return result;
}
VideoEncoderInfo GetVideoEncoderInfo(VTSessionRef compression_session,
VideoCodecProfile profile) {
VideoEncoderInfo info;
info.implementation_name = "VideoToolbox";
info.is_hardware_accelerated = false;
base::ScopedCFTypeRef<CFBooleanRef> cf_using_hardware;
if (VTSessionCopyProperty(
compression_session,
kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder,
kCFAllocatorDefault, cf_using_hardware.InitializeInto()) == 0) {
info.is_hardware_accelerated = CFBooleanGetValue(cf_using_hardware);
}
absl::optional<int> max_frame_delay_property;
base::ScopedCFTypeRef<CFNumberRef> max_frame_delay_count;
if (VTSessionCopyProperty(
compression_session, kVTCompressionPropertyKey_MaxFrameDelayCount,
kCFAllocatorDefault, max_frame_delay_count.InitializeInto()) == 0) {
int32_t frame_delay;
if (CFNumberGetValue(max_frame_delay_count, kCFNumberSInt32Type,
&frame_delay) &&
frame_delay != kVTUnlimitedFrameDelayCount) {
max_frame_delay_property = frame_delay;
}
}
// Not all VideoToolbox encoders are created equal. The numbers below match
// the characteristics of an Apple Silicon M1 laptop. It has been noted that,
// for example, the HW encoder in a 2014 (Intel) machine has a smaller
// capacity. And while overestimating the capacity is not a problem,
// underestimating the frame delay is, so these numbers might need tweaking
// in the face of new evidence.
if (info.is_hardware_accelerated) {
info.frame_delay = 0;
info.input_capacity = 10;
} else {
info.frame_delay =
profile == H264PROFILE_BASELINE || profile == HEVCPROFILE_MAIN ? 0 : 13;
info.input_capacity = info.frame_delay.value() + 4;
}
if (max_frame_delay_property.has_value()) {
info.frame_delay =
std::min(info.frame_delay.value(), max_frame_delay_property.value());
info.input_capacity =
std::min(info.input_capacity.value(), max_frame_delay_property.value());
}
return info;
}
} // namespace
struct VTVideoEncodeAccelerator::InProgressFrameEncode {
InProgressFrameEncode(scoped_refptr<VideoFrame> frame,
const gfx::ColorSpace& frame_cs)
: frame(frame), encoded_color_space(frame_cs) {}
const scoped_refptr<VideoFrame> frame;
const gfx::ColorSpace encoded_color_space;
};
struct VTVideoEncodeAccelerator::EncodeOutput {
EncodeOutput() = delete;
EncodeOutput(VTEncodeInfoFlags info_flags,
CMSampleBufferRef sbuf,
const InProgressFrameEncode& frame_info)
: info(info_flags),
sample_buffer(sbuf, base::scoped_policy::RETAIN),
capture_timestamp(frame_info.frame->timestamp()),
encoded_color_space(frame_info.encoded_color_space) {}
EncodeOutput(const EncodeOutput&) = delete;
EncodeOutput& operator=(const EncodeOutput&) = delete;
const VTEncodeInfoFlags info;
const base::ScopedCFTypeRef<CMSampleBufferRef> sample_buffer;
const base::TimeDelta capture_timestamp;
const gfx::ColorSpace encoded_color_space;
};
struct VTVideoEncodeAccelerator::BitstreamBufferRef {
BitstreamBufferRef() = delete;
BitstreamBufferRef(int32_t id,
base::WritableSharedMemoryMapping mapping,
size_t size)
: id(id), mapping(std::move(mapping)), size(size) {}
BitstreamBufferRef(const BitstreamBufferRef&) = delete;
BitstreamBufferRef& operator=(const BitstreamBufferRef&) = delete;
const int32_t id;
const base::WritableSharedMemoryMapping mapping;
const size_t size;
};
// .5 is set as a minimum to prevent overcompensating for large temporary
// overshoots. We don't want to degrade video quality too badly.
// .95 is set to prevent oscillations. When a lower bitrate is set on the
// encoder than previously set, its output seems to have a brief period of
// drastically reduced bitrate, so we want to avoid that. In steady state
// conditions, 0.95 seems to give us better overall bitrate over long periods
// of time.
VTVideoEncodeAccelerator::VTVideoEncodeAccelerator()
: bitrate_adjuster_(.5, .95),
task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {
encoder_weak_ptr_ = encoder_weak_factory_.GetWeakPtr();
}
VTVideoEncodeAccelerator::~VTVideoEncodeAccelerator() {
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
VideoEncodeAccelerator::SupportedProfiles
VTVideoEncodeAccelerator::GetSupportedH264Profiles() {
SupportedProfiles profiles;
bool supported =
CreateCompressionSession(VideoCodec::kH264, kDefaultSupportedResolution);
DestroyCompressionSession();
if (!supported) {
DVLOG(1) << "Hardware H.264 encode acceleration is not available on this "
"platform.";
return profiles;
}
SupportedProfile profile;
profile.max_resolution = kMaxSupportedResolution;
profile.max_framerate_numerator = kMaxFrameRateNumerator;
profile.max_framerate_denominator = kMaxFrameRateDenominator;
profile.rate_control_modes = VideoEncodeAccelerator::kConstantMode |
VideoEncodeAccelerator::kVariableMode;
profile.scalability_modes.push_back(SVCScalabilityMode::kL1T1);
if (__builtin_available(macOS LOW_LATENCY_FLAG_AVAILABLE_VER, *))
profile.scalability_modes.push_back(SVCScalabilityMode::kL1T2);
for (const auto& supported_profile : kSupportedProfiles) {
if (VideoCodecProfileToVideoCodec(supported_profile) == VideoCodec::kH264) {
#if defined(ARCH_CPU_X86_FAMILY)
for (const auto& min_resolution : {gfx::Size(640, 1), gfx::Size(1, 480)})
#else
const auto min_resolution = gfx::Size();
#endif
{
profile.min_resolution = min_resolution;
profile.is_software_codec = false;
profile.profile = supported_profile;
profiles.push_back(profile);
// macOS doesn't provide a way to enumerate codec details, so just
// assume software codec support is the same as hardware, but with
// the lowest possible minimum resolution.
profile.min_resolution = gfx::Size(2, 2);
profile.is_software_codec = true;
profiles.push_back(profile);
}
}
}
return profiles;
}
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
VideoEncodeAccelerator::SupportedProfiles
VTVideoEncodeAccelerator::GetSupportedHEVCProfiles() {
SupportedProfiles profiles;
if (!base::FeatureList::IsEnabled(kPlatformHEVCEncoderSupport))
return profiles;
if (__builtin_available(macOS 11.0, *)) {
bool supported = CreateCompressionSession(VideoCodec::kHEVC,
kDefaultSupportedResolution);
DestroyCompressionSession();
if (!supported) {
DVLOG(1) << "Hardware HEVC encode acceleration is not available on this "
"platform.";
return profiles;
}
SupportedProfile profile;
profile.max_resolution = kMaxSupportedResolution;
profile.max_framerate_numerator = kMaxFrameRateNumerator;
profile.max_framerate_denominator = kMaxFrameRateDenominator;
profile.rate_control_modes = VideoEncodeAccelerator::kConstantMode |
VideoEncodeAccelerator::kVariableMode;
for (const auto& supported_profile : kSupportedProfiles) {
if (VideoCodecProfileToVideoCodec(supported_profile) ==
VideoCodec::kHEVC) {
profile.is_software_codec = false;
profile.profile = supported_profile;
profiles.push_back(profile);
// macOS doesn't provide a way to enumerate codec details, so just
// assume software codec support is the same as hardware, but with
// the lowest possible minimum resolution.
profile.min_resolution = gfx::Size(2, 2);
profile.is_software_codec = true;
profiles.push_back(profile);
}
}
}
return profiles;
}
#endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
VideoEncodeAccelerator::SupportedProfiles
VTVideoEncodeAccelerator::GetSupportedProfiles() {
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
SupportedProfiles profiles;
for (const auto& supported_profile : GetSupportedH264Profiles())
profiles.push_back(supported_profile);
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
for (const auto& supported_profile : GetSupportedHEVCProfiles())
profiles.push_back(supported_profile);
#endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
return profiles;
}
bool VTVideoEncodeAccelerator::Initialize(const Config& config,
Client* client,
std::unique_ptr<MediaLog> media_log) {
DVLOG(3) << __func__ << ": " << config.AsHumanReadableString();
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(client);
// Clients are expected to call Flush() before reinitializing the encoder.
DCHECK_EQ(pending_encodes_, 0);
if (config.input_format != PIXEL_FORMAT_I420 &&
config.input_format != PIXEL_FORMAT_NV12) {
MEDIA_LOG(ERROR, media_log)
<< "Input format not supported= "
<< VideoPixelFormatToString(config.input_format);
return false;
}
if (!base::Contains(kSupportedProfiles, config.output_profile)) {
MEDIA_LOG(ERROR, media_log) << "Output profile not supported= "
<< GetProfileName(config.output_profile);
return false;
}
profile_ = config.output_profile;
codec_ = VideoCodecProfileToVideoCodec(config.output_profile);
client_ = client;
input_visible_size_ = config.input_visible_size;
if (config.initial_framerate.has_value())
frame_rate_ = config.initial_framerate.value();
else
frame_rate_ = kDefaultFrameRateNumerator / kDefaultFrameRateDenominator;
bitrate_ = config.bitrate;
bitstream_buffer_size_ = config.input_visible_size.GetArea();
require_low_delay_ = config.require_low_delay;
if (codec_ == VideoCodec::kH264 || codec_ == VideoCodec::kHEVC) {
required_encoder_type_ = config.required_encoder_type;
} else {
DLOG(ERROR) << "Software encoder selection is only allowed for H264/H265.";
}
if (config.HasTemporalLayer())
num_temporal_layers_ = config.spatial_layers.front().num_of_temporal_layers;
if (num_temporal_layers_ > 2) {
MEDIA_LOG(ERROR, media_log) << "Unsupported number of SVC temporal layers.";
return false;
}
if (!ResetCompressionSession(codec_)) {
MEDIA_LOG(ERROR, media_log) << "Failed creating compression session.";
return false;
}
auto encoder_info = GetVideoEncoderInfo(compression_session_, profile_);
// Report whether hardware encode is being used.
if (!encoder_info.is_hardware_accelerated) {
MEDIA_LOG(INFO, media_log) << "VideoToolbox selected a software encoder.";
}
media_log_ = std::move(media_log);
client_->NotifyEncoderInfoChange(encoder_info);
client_->RequireBitstreamBuffers(kNumInputBuffers, input_visible_size_,
bitstream_buffer_size_);
return true;
}
void VTVideoEncodeAccelerator::Encode(scoped_refptr<VideoFrame> frame,
bool force_keyframe) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(compression_session_);
DCHECK(frame);
auto pixel_buffer = WrapVideoFrameInCVPixelBuffer(frame);
if (!pixel_buffer) {
NotifyErrorStatus({EncoderStatus::Codes::kEncoderFailedEncode,
"WrapVideoFrameInCVPixelBuffer failed"});
return;
}
if (can_set_encoder_color_space_) {
// WrapVideoFrameInCVPixelBuffer() will do a few different things depending
// on the input buffer type:
// * If it's an IOSurface, the underlying attached color space will
// passthough to the pixel buffer.
// * If we're uploading to a new pixel buffer and the provided frame color
// space is valid that'll be set on the pixel buffer.
// * If the frame color space is not valid, BT709 will be assumed.
auto frame_cs = GetImageBufferColorSpace(pixel_buffer);
if (encoder_color_space_ && frame_cs != encoder_color_space_) {
if (pending_encodes_) {
auto status = VTCompressionSessionCompleteFrames(compression_session_,
kCMTimeInvalid);
if (status != noErr) {
NotifyErrorStatus(
{EncoderStatus::Codes::kEncoderFailedFlush,
"flush failed: " + logging::DescriptionFromOSStatus(status)});
return;
}
}
if (!ResetCompressionSession(codec_)) {
// ResetCompressionSession() invokes NotifyErrorStatus() on failure.
return;
}
encoder_color_space_.reset();
}
if (!encoder_color_space_) {
encoder_color_space_ = frame_cs;
SetEncoderColorSpace();
}
}
base::ScopedCFTypeRef<CFDictionaryRef> frame_props =
video_toolbox::DictionaryWithKeyValue(
kVTEncodeFrameOptionKey_ForceKeyFrame,
force_keyframe ? kCFBooleanTrue : kCFBooleanFalse);
auto timestamp_cm =
CMTimeMake(frame->timestamp().InMicroseconds(), USEC_PER_SEC);
// Wrap information we'll need after the frame is encoded in a heap object.
// We'll get the pointer back from the VideoToolbox completion callback.
auto request = std::make_unique<InProgressFrameEncode>(
std::move(frame), encoder_color_space_.value_or(gfx::ColorSpace()));
if (bitrate_.mode() == Bitrate::Mode::kConstant) {
// In CBR mode, we adjust bitrate before every encode based on past history
// of bitrate adherence.
SetAdjustedConstantBitrate(bitrate_adjuster_.GetAdjustedBitrateBps());
}
// We can pass the ownership of |request| to the encode callback if
// successful. Otherwise let it fall out of scope.
OSStatus status = VTCompressionSessionEncodeFrame(
compression_session_, pixel_buffer, timestamp_cm, kCMTimeInvalid,
frame_props, reinterpret_cast<void*>(request.get()), nullptr);
if (status != noErr) {
NotifyErrorStatus({EncoderStatus::Codes::kEncoderFailedEncode,
"VTCompressionSessionEncodeFrame failed: " +
logging::DescriptionFromOSStatus(status)});
} else {
++pending_encodes_;
CHECK(request.release());
}
}
void VTVideoEncodeAccelerator::UseOutputBitstreamBuffer(
BitstreamBuffer buffer) {
DVLOG(3) << __func__ << ": buffer size=" << buffer.size();
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (buffer.size() < bitstream_buffer_size_) {
NotifyErrorStatus({EncoderStatus::Codes::kInvalidOutputBuffer,
"Output BitstreamBuffer isn't big enough: " +
base::NumberToString(buffer.size()) + " vs. " +
base::NumberToString(bitstream_buffer_size_)});
return;
}
auto mapping = buffer.TakeRegion().Map();
if (!mapping.IsValid()) {
NotifyErrorStatus({EncoderStatus::Codes::kSystemAPICallError,
"Failed mapping shared memory"});
return;
}
auto buffer_ref = std::make_unique<BitstreamBufferRef>(
buffer.id(), std::move(mapping), buffer.size());
// If there is already EncodeOutput waiting, copy its output first.
if (!encoder_output_queue_.empty()) {
auto encode_output = std::move(encoder_output_queue_.front());
encoder_output_queue_.pop_front();
ReturnBitstreamBuffer(std::move(encode_output), std::move(buffer_ref));
return;
}
bitstream_buffer_queue_.push_back(std::move(buffer_ref));
}
void VTVideoEncodeAccelerator::RequestEncodingParametersChange(
const Bitrate& bitrate,
uint32_t framerate) {
DVLOG(3) << __func__ << ": bitrate=" << bitrate.ToString()
<< ": framerate=" << framerate;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!compression_session_) {
NotifyErrorStatus(
{EncoderStatus::Codes::kEncoderIllegalState, "No compression session"});
return;
}
frame_rate_ = framerate;
video_toolbox::SessionPropertySetter session_property_setter(
compression_session_);
session_property_setter.Set(kVTCompressionPropertyKey_ExpectedFrameRate,
frame_rate_);
switch (bitrate.mode()) {
case Bitrate::Mode::kConstant:
if (bitrate.target_bps() != static_cast<uint32_t>(target_bitrate_)) {
target_bitrate_ = bitrate.target_bps();
bitrate_adjuster_.SetTargetBitrateBps(target_bitrate_);
SetAdjustedConstantBitrate(bitrate_adjuster_.GetAdjustedBitrateBps());
}
break;
case Bitrate::Mode::kVariable:
SetVariableBitrate(bitrate);
break;
default:
NOTREACHED();
}
bitrate_ = bitrate;
}
void VTVideoEncodeAccelerator::Destroy() {
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DestroyCompressionSession();
delete this;
}
void VTVideoEncodeAccelerator::Flush(FlushCallback flush_callback) {
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(flush_callback);
if (!compression_session_) {
std::move(flush_callback).Run(/*success=*/false);
return;
}
// Even though this will block until all frames are returned, the frames will
// be posted to the current task runner, so we can't run the flush callback
// at this time.
OSStatus status =
VTCompressionSessionCompleteFrames(compression_session_, kCMTimeInvalid);
if (status != noErr) {
OSSTATUS_DLOG(ERROR, status)
<< " VTCompressionSessionCompleteFrames failed: ";
std::move(flush_callback).Run(/*success=*/false);
return;
}
pending_flush_cb_ = std::move(flush_callback);
MaybeRunFlushCallback();
}
bool VTVideoEncodeAccelerator::IsFlushSupported() {
return true;
}
void VTVideoEncodeAccelerator::SetAdjustedConstantBitrate(uint32_t bitrate) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (bitrate == encoder_set_bitrate_)
return;
encoder_set_bitrate_ = bitrate;
video_toolbox::SessionPropertySetter session_property_setter(
compression_session_);
[[maybe_unused]] bool rv = session_property_setter.Set(
kVTCompressionPropertyKey_AverageBitRate,
base::saturated_cast<int32_t>(encoder_set_bitrate_));
rv &= session_property_setter.Set(
kVTCompressionPropertyKey_DataRateLimits,
CreateRateLimitArray(Bitrate::ConstantBitrate(bitrate)));
DLOG_IF(ERROR, !rv)
<< "Couldn't change bitrate parameters of encode session.";
}
void VTVideoEncodeAccelerator::SetVariableBitrate(const Bitrate& bitrate) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(bitrate.mode() == Bitrate::Mode::kVariable);
video_toolbox::SessionPropertySetter session_property_setter(
compression_session_);
[[maybe_unused]] bool rv =
session_property_setter.Set(kVTCompressionPropertyKey_AverageBitRate,
static_cast<int32_t>(bitrate.target_bps()));
rv &= session_property_setter.Set(kVTCompressionPropertyKey_DataRateLimits,
CreateRateLimitArray(bitrate));
DLOG_IF(ERROR, !rv)
<< "Couldn't change bitrate parameters of encode session.";
}
// static
void VTVideoEncodeAccelerator::CompressionCallback(void* encoder_opaque,
void* request_opaque,
OSStatus status,
VTEncodeInfoFlags info,
CMSampleBufferRef sbuf) {
// This function may be called asynchronously, on a different thread from the
// one that calls VTCompressionSessionEncodeFrame.
DVLOG(3) << __func__;
auto* encoder = reinterpret_cast<VTVideoEncodeAccelerator*>(encoder_opaque);
DCHECK(encoder);
// InProgressFrameEncode holds timestamp information of the encoded frame.
std::unique_ptr<InProgressFrameEncode> frame_info(
reinterpret_cast<InProgressFrameEncode*>(request_opaque));
// EncodeOutput holds onto CMSampleBufferRef when posting task between
// threads.
auto encode_output = std::make_unique<EncodeOutput>(info, sbuf, *frame_info);
// This method is NOT called on |task_runner_|, so we still need to
// post a task back to it to do work.
encoder->task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&VTVideoEncodeAccelerator::CompressionCallbackTask,
encoder->encoder_weak_ptr_, status,
std::move(encode_output)));
}
void VTVideoEncodeAccelerator::CompressionCallbackTask(
OSStatus status,
std::unique_ptr<EncodeOutput> encode_output) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
--pending_encodes_;
DCHECK_GE(pending_encodes_, 0);
if (status != noErr) {
NotifyErrorStatus(
{EncoderStatus::Codes::kEncoderFailedEncode,
"Encode failed: " + logging::DescriptionFromOSStatus(status)});
return;
}
// If there isn't any BitstreamBuffer to copy into, add it to a queue for
// later use.
if (bitstream_buffer_queue_.empty()) {
encoder_output_queue_.push_back(std::move(encode_output));
return;
}
auto buffer_ref = std::move(bitstream_buffer_queue_.front());
bitstream_buffer_queue_.pop_front();
ReturnBitstreamBuffer(std::move(encode_output), std::move(buffer_ref));
}
void VTVideoEncodeAccelerator::ReturnBitstreamBuffer(
std::unique_ptr<EncodeOutput> encode_output,
std::unique_ptr<VTVideoEncodeAccelerator::BitstreamBufferRef> buffer_ref) {
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (encode_output->info & kVTEncodeInfo_FrameDropped) {
DVLOG(2) << " frame dropped";
client_->BitstreamBufferReady(
buffer_ref->id,
BitstreamBufferMetadata(0, false, encode_output->capture_timestamp));
MaybeRunFlushCallback();
return;
}
auto* sample_attachments = static_cast<CFDictionaryRef>(
CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(
encode_output->sample_buffer.get(), true),
0));
const bool keyframe = !CFDictionaryContainsKey(
sample_attachments, kCMSampleAttachmentKey_NotSync);
bool belongs_to_base_layer = true;
if (CFBooleanRef value_ptr = base::mac::GetValueFromDictionary<CFBooleanRef>(
sample_attachments, kCMSampleAttachmentKey_IsDependedOnByOthers)) {
belongs_to_base_layer = static_cast<bool>(CFBooleanGetValue(value_ptr));
}
size_t used_buffer_size = 0;
const bool copy_rv = video_toolbox::CopySampleBufferToAnnexBBuffer(
codec_, encode_output->sample_buffer.get(), keyframe, buffer_ref->size,
static_cast<char*>(buffer_ref->mapping.memory()), &used_buffer_size);
if (!copy_rv) {
DLOG(ERROR) << "Cannot copy output from SampleBuffer to AnnexBBuffer.";
used_buffer_size = 0;
}
if (bitrate_.mode() == Bitrate::Mode::kConstant) {
// In CBR mode, we let bitrate adjuster know how much encoded data was
// produced to better control bitrate adherence.
bitrate_adjuster_.Update(used_buffer_size);
}
BitstreamBufferMetadata md(used_buffer_size, keyframe,
encode_output->capture_timestamp);
switch (codec_) {
case VideoCodec::kH264:
md.h264.emplace().temporal_idx = belongs_to_base_layer ? 0 : 1;
break;
case VideoCodec::kHEVC:
md.h265.emplace().temporal_idx = belongs_to_base_layer ? 0 : 1;
break;
default:
NOTREACHED();
break;
}
md.encoded_color_space = encode_output->encoded_color_space;
client_->BitstreamBufferReady(buffer_ref->id, std::move(md));
MaybeRunFlushCallback();
}
bool VTVideoEncodeAccelerator::ResetCompressionSession(VideoCodec codec) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DestroyCompressionSession();
if (!CreateCompressionSession(codec, input_visible_size_)) {
return false;
}
if (!ConfigureCompressionSession(codec)) {
return false;
}
RequestEncodingParametersChange(bitrate_, frame_rate_);
return true;
}
bool VTVideoEncodeAccelerator::CreateCompressionSession(
VideoCodec codec,
const gfx::Size& input_size) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::vector<CFTypeRef> encoder_keys{
kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder};
std::vector<CFTypeRef> encoder_values{required_encoder_type_ ==
Config::EncoderType::kHardware
? kCFBooleanTrue
: kCFBooleanFalse};
if (required_encoder_type_ == Config::EncoderType::kSoftware) {
encoder_keys.push_back(
kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder);
encoder_values.push_back(kCFBooleanFalse);
}
if (__builtin_available(macOS LOW_LATENCY_FLAG_AVAILABLE_VER, *)) {
// Remove the validation once HEVC SVC mode is supported on macOS.
if (require_low_delay_ && codec == VideoCodec::kH264) {
encoder_keys.push_back(
kVTVideoEncoderSpecification_EnableLowLatencyRateControl);
encoder_values.push_back(kCFBooleanTrue);
}
}
base::ScopedCFTypeRef<CFDictionaryRef> encoder_spec =
video_toolbox::DictionaryWithKeysAndValues(
encoder_keys.data(), encoder_values.data(), encoder_keys.size());
// Create the compression session.
// Note that the encoder object is given to the compression session as the
// callback context using a raw pointer. The C API does not allow us to use a
// smart pointer, nor is this encoder ref counted. However, this is still
// safe, because we 1) we own the compression session and 2) we tear it down
// safely. When destructing the encoder, the compression session is flushed
// and invalidated. Internally, VideoToolbox will join all of its threads
// before returning to the client. Therefore, when control returns to us, we
// are guaranteed that the output callback will not execute again.
OSStatus status = VTCompressionSessionCreate(
kCFAllocatorDefault, input_size.width(), input_size.height(),
VideoCodecToCMVideoCodec(codec), encoder_spec,
nullptr /* sourceImageBufferAttributes */,
nullptr /* compressedDataAllocator */,
&VTVideoEncodeAccelerator::CompressionCallback,
reinterpret_cast<void*>(this), compression_session_.InitializeInto());
if (status != noErr) {
// IMPORTANT: ScopedCFTypeRef::release() doesn't call CFRelease().
// In case of an error VTCompressionSessionCreate() is not supposed to
// write a non-null value into compression_session_, but just in case,
// we'll clear it without calling CFRelease() because it can be unsafe
// to call on a not fully created session.
(void)compression_session_.release();
NotifyErrorStatus({EncoderStatus::Codes::kEncoderInitializationError,
"VTCompressionSessionCreate failed: " +
logging::DescriptionFromOSStatus(status)});
return false;
}
DVLOG(3) << " VTCompressionSession created with input size="
<< input_size.ToString();
return true;
}
bool VTVideoEncodeAccelerator::ConfigureCompressionSession(VideoCodec codec) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(compression_session_);
video_toolbox::SessionPropertySetter session_property_setter(
compression_session_);
if (!session_property_setter.Set(kVTCompressionPropertyKey_ProfileLevel,
VideoCodecProfileToVTProfile(profile_))) {
NotifyErrorStatus({EncoderStatus::Codes::kEncoderUnsupportedProfile,
"Unsupported profile: " + GetProfileName(profile_)});
return false;
}
// Remove the validation once HEVC SVC mode is supported on macOS.
if (!session_property_setter.Set(
kVTCompressionPropertyKey_RealTime,
require_low_delay_ && codec == VideoCodec::kH264)) {
NotifyErrorStatus(
{EncoderStatus::Codes::kEncoderUnsupportedConfig,
"The video encoder doesn't support compression in real time"});
return false;
}
if (!session_property_setter.Set(
kVTCompressionPropertyKey_AllowFrameReordering, false)) {
NotifyErrorStatus(
{EncoderStatus::Codes::kEncoderUnsupportedConfig,
"The video encoder doesn't support non frame reordering compression"});
return false;
}
// Limit keyframe output to 4 minutes, see https://crbug.com/658429.
if (!session_property_setter.Set(
kVTCompressionPropertyKey_MaxKeyFrameInterval, 7200)) {
NotifyErrorStatus({EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Failed to set max keyframe interval to 7200 frames"});
return false;
}
if (!session_property_setter.Set(
kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, 240)) {
NotifyErrorStatus(
{EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Failed to set max keyframe interval duration to 240 seconds"});
return false;
}
if (session_property_setter.IsSupported(
kVTCompressionPropertyKey_MaxFrameDelayCount)) {
if (!session_property_setter.Set(
kVTCompressionPropertyKey_MaxFrameDelayCount,
static_cast<int>(kNumInputBuffers))) {
NotifyErrorStatus({EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Failed to set max frame delay count to " +
base::NumberToString(kNumInputBuffers)});
return false;
}
} else {
DLOG(WARNING) << "MaxFrameDelayCount is not supported";
}
// Remove the validation once HEVC SVC mode is supported on macOS.
if (num_temporal_layers_ == 2 && codec_ == VideoCodec::kH264) {
if (__builtin_available(macOS LOW_LATENCY_FLAG_AVAILABLE_VER, *)) {
if (!session_property_setter.IsSupported(
kVTCompressionPropertyKey_BaseLayerFrameRateFraction)) {
NotifyErrorStatus({EncoderStatus::Codes::kEncoderUnsupportedConfig,
"BaseLayerFrameRateFraction is not supported"});
return false;
}
if (!session_property_setter.Set(
kVTCompressionPropertyKey_BaseLayerFrameRateFraction, 0.5)) {
NotifyErrorStatus({EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Setting BaseLayerFrameRate property failed"});
return false;
}
} else {
NotifyErrorStatus({EncoderStatus::Codes::kEncoderUnsupportedConfig,
"SVC encoding is not supported on this OS version"});
return false;
}
}
return true;
}
void VTVideoEncodeAccelerator::DestroyCompressionSession() {
if (compression_session_) {
VTCompressionSessionInvalidate(compression_session_);
compression_session_.reset();
}
}
void VTVideoEncodeAccelerator::MaybeRunFlushCallback() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!pending_flush_cb_)
return;
if (pending_encodes_ || !encoder_output_queue_.empty())
return;
std::move(pending_flush_cb_).Run(/*success=*/true);
}
void VTVideoEncodeAccelerator::SetEncoderColorSpace() {
if (!encoder_color_space_ || !encoder_color_space_->IsValid()) {
return;
}
CFStringRef primary, transfer, matrix;
if (!GetImageBufferColorValues(*encoder_color_space_, &primary, &transfer,
&matrix)) {
DLOG(ERROR) << "Failed to set bitstream color space: "
<< encoder_color_space_->ToString();
return;
}
video_toolbox::SessionPropertySetter session_property_setter(
compression_session_);
if (!session_property_setter.IsSupported(
kVTCompressionPropertyKey_ColorPrimaries) ||
!session_property_setter.IsSupported(
kVTCompressionPropertyKey_TransferFunction) ||
!session_property_setter.IsSupported(
kVTCompressionPropertyKey_YCbCrMatrix)) {
DLOG(ERROR) << "VTCompressionSession doesn't support color space settings.";
can_set_encoder_color_space_ = false;
return;
}
if (!session_property_setter.Set(kVTCompressionPropertyKey_ColorPrimaries,
primary) ||
!session_property_setter.Set(kVTCompressionPropertyKey_TransferFunction,
transfer) ||
!session_property_setter.Set(kVTCompressionPropertyKey_YCbCrMatrix,
matrix)) {
DLOG(ERROR) << "Failed to set color space on VTCompressionSession.";
can_set_encoder_color_space_ = false;
return;
}
DVLOG(1) << "Set encoder color space to: "
<< encoder_color_space_->ToString();
}
void VTVideoEncodeAccelerator::NotifyErrorStatus(EncoderStatus status) {
CHECK(!status.is_ok());
LOG(ERROR) << "Call NotifyErrorStatus(): code="
<< static_cast<int>(status.code())
<< ", message=" << status.message();
if (media_log_) {
MEDIA_LOG(ERROR, media_log_) << status.message();
}
// NotifyErrorStatus() can be called without calling Initialize() in the case
// of GetSupportedProfiles().
if (!client_) {
return;
}
client_->NotifyErrorStatus(std::move(status));
}
} // namespace media