chromium/media/gpu/mac/vt_video_encode_accelerator_mac.mm

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

#import <Foundation/Foundation.h>

#include <memory>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/osstatus_logging.h"
#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/mac/mac_util.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"

using base::apple::CFToNSPtrCast;
using base::apple::NSToCFPtrCast;

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

namespace media {
namespace {

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/40876392): 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)
};

bool IsSVCSupported(const VideoCodec& codec) {
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER) && defined(ARCH_CPU_ARM_FAMILY)
  // macOS 14.0+ support SVC HEVC encoding for Apple Silicon chips only.
  if (codec == VideoCodec::kHEVC) {
    if (@available(macOS 14.0, iOS 17.0, *)) {
      return true;
    }
    return false;
  }
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER) &&
        // defined(ARCH_CPU_ARM_FAMILY)
  if (@available(macOS LOW_LATENCY_AND_SVC_AVAILABLE_VER, *)) {
    return codec == VideoCodec::kH264;
  }
  return false;
}

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_IN_MIGRATION();
  }
  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_IN_MIGRATION();
  }
  return kCMVideoCodecType_H264;
}

VideoEncoderInfo GetVideoEncoderInfo(VTSessionRef compression_session,
                                     VideoCodecProfile profile) {
  VideoEncoderInfo info;
  info.implementation_name = "VideoToolbox";
#if BUILDFLAG(IS_MAC)
  info.is_hardware_accelerated = false;

  base::apple::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.get());
  }
#else
  // iOS is always hardware-accelerated.
  info.is_hardware_accelerated = true;
#endif

  std::optional<int> max_frame_delay_property;
  base::apple::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.get(), 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::apple::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;
};

VTVideoEncodeAccelerator::VTVideoEncodeAccelerator()
    : 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;
  // Advertise VBR here, even though the peak bitrate is never actually used.
  // See RequestEncodingParametersChange() for more details.
  profile.rate_control_modes = VideoEncodeAccelerator::kConstantMode |
                               VideoEncodeAccelerator::kVariableMode;
  // L1T1 = no additional spatial and temporal layer = always supported.
  profile.scalability_modes.push_back(SVCScalabilityMode::kL1T1);
  if (IsSVCSupported(VideoCodec::kH264)) {
    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;
  }
  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;
  // Advertise VBR here, even though the peak bitrate is never actually used.
  // See RequestEncodingParametersChange() for more details.
  profile.rate_control_modes = VideoEncodeAccelerator::kConstantMode |
                               VideoEncodeAccelerator::kVariableMode;
  // L1T1 = no additional spatial and temporal layer = always supported.
  profile.scalability_modes.push_back(SVCScalabilityMode::kL1T1);
  if (IsSVCSupported(VideoCodec::kHEVC)) {
    profile.scalability_modes.push_back(SVCScalabilityMode::kL1T2);
  }

  for (const auto& supported_profile : kSupportedProfiles) {
    if (VideoCodecProfileToVideoCodec(supported_profile) == VideoCodec::kHEVC) {
      // macOS doesn't support HEVC software encoding on both Intel and Apple
      // Silicon Macs.
      profile.is_software_codec = false;
      profile.profile = supported_profile;
      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;
  frame_rate_ = config.framerate;
  bitrate_ = config.bitrate;
  bitstream_buffer_size_ = config.input_visible_size.GetArea();
  require_low_delay_ = config.require_low_delay;

  if (codec_ == VideoCodec::kH264) {
    required_encoder_type_ = config.required_encoder_type;
  } else if (config.required_encoder_type == Config::EncoderType::kSoftware) {
    DLOG(ERROR) << "Software encoder selection is only allowed for H264.";
  }

  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_.get(), 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
    //     passthrough 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.get());
    if (encoder_color_space_ && frame_cs != encoder_color_space_) {
      if (pending_encodes_) {
        auto status = VTCompressionSessionCompleteFrames(
            compression_session_.get(), 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();
    }
  }

  NSDictionary* frame_props = @{
    CFToNSPtrCast(kVTEncodeFrameOptionKey_ForceKeyFrame) : force_keyframe ? @YES
                                                                          : @NO
  };

  // VideoToolbox uses timestamps for rate control purposes, but we can't rely
  // on real frame timestamps to be consistent with configured frame rate.
  // That's why we map real frame timestamps to generate ones that a
  // monotonically increase according to the configured frame rate.
  // Outputs will still be assigned real timestamps from frame objects.
  auto generate_timestamp = AssignMonotonicTimestamp();
  auto timestamp_cm =
      CMTimeMake(generate_timestamp.InMicroseconds(), USEC_PER_SEC);
  auto duration_cm = CMTimeMake(
      (base::Seconds(1) / frame_rate_).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()));

  // 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_.get(), pixel_buffer.get(), timestamp_cm, duration_cm,
      NSToCFPtrCast(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,
    const std::optional<gfx::Size>& size) {
  std::ostringstream parameters_description;
  parameters_description << ": bitrate=" << bitrate.ToString()
                         << ": framerate=" << framerate;
  if (size.has_value()) {
    parameters_description << ": frame size=" << size->width() << "x"
                           << size->height();
  }
  DVLOG(3) << __func__ << parameters_description.str();
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (size.has_value()) {
    NotifyErrorStatus({EncoderStatus::Codes::kEncoderUnsupportedConfig,
                       "Update output frame size is not supported"});
    return;
  }

  if (!compression_session_) {
    NotifyErrorStatus(
        {EncoderStatus::Codes::kEncoderIllegalState, "No compression session"});
    return;
  }

  frame_rate_ = framerate;
  video_toolbox::SessionPropertySetter session_property_setter(
      compression_session_);
  if (!session_property_setter.Set(kVTCompressionPropertyKey_ExpectedFrameRate,
                                   frame_rate_)) {
    NotifyErrorStatus(
        {EncoderStatus::Codes::kSystemAPICallError, "Can't change frame rate"});
    return;
  }
  if (!session_property_setter.Set(
          kVTCompressionPropertyKey_AverageBitRate,
          static_cast<int32_t>(bitrate.target_bps()))) {
    NotifyErrorStatus({EncoderStatus::Codes::kSystemAPICallError,
                       "Can't change average bitrate"});
    return;
  }
  // Here in case of VBR we'd like to set more relaxed bitrate constraints.
  // It looks like setting VTCompressionPropertyKey_DataRateLimits should be
  // appropriate her, but it is NOT compatible with
  // EnableLowLatencyRateControl even though this fact is not documented.
  // Even in non low latency mode VTCompressionPropertyKey_DataRateLimits tends
  // to make the encoder undershoot set bitrate.
  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_.get(), 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;
}

// 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::CreateForDropFrame(
                                      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::apple::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;
  }

  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_IN_MIGRATION();
      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_, std::nullopt);
  return true;
}

bool VTVideoEncodeAccelerator::CreateCompressionSession(
    VideoCodec codec,
    const gfx::Size& input_size) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  NSMutableDictionary* encoder_spec = [NSMutableDictionary dictionary];

  // iOS is always hardware-accelerated while on mac, encoder configuration
  // handling is necessary.
#if BUILDFLAG(IS_MAC)
  if (required_encoder_type_ == Config::EncoderType::kHardware) {
    encoder_spec[CFToNSPtrCast(
        kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder)] =
        @YES;
  } else {
    encoder_spec[CFToNSPtrCast(
        kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder)] =
        @NO;
  }

  if (required_encoder_type_ == Config::EncoderType::kSoftware) {
    encoder_spec[CFToNSPtrCast(
        kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder)] =
        @NO;
  }
#endif

  if (@available(macOS LOW_LATENCY_AND_SVC_AVAILABLE_VER, *)) {
    if (require_low_delay_ && IsSVCSupported(codec)) {
      encoder_spec[CFToNSPtrCast(
          kVTVideoEncoderSpecification_EnableLowLatencyRateControl)] = @YES;
    }
  }

  // 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), NSToCFPtrCast(encoder_spec),
      /*sourceImageBufferAttributes=*/nullptr,
      /*compressedDataAllocator=*/nullptr,
      &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;
  }
  if (!session_property_setter.Set(kVTCompressionPropertyKey_RealTime,
                                   require_low_delay_)) {
    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;
  }
  // This property may suddenly become unsupported when a second compression
  // session is created if the codec is H.265 and CPU arch is x64, so we can
  // always check if the property is supported before setting it.
  if (session_property_setter.IsSupported(
          kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration)) {
    if (!session_property_setter.Set(
            kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, 240)) {
      NotifyErrorStatus(
          {EncoderStatus::Codes::kEncoderUnsupportedConfig,
           "Failed to set max keyframe interval duration to 240 seconds"});
      return false;
    }
  } else {
    DLOG(WARNING) << "MaxKeyFrameIntervalDuration is not supported";
  }

  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";
  }

  if (num_temporal_layers_ != 2) {
    return true;
  }

  if (!IsSVCSupported(codec)) {
    NotifyErrorStatus(
        {EncoderStatus::Codes::kEncoderUnsupportedConfig,
         "SVC encoding is not supported on this OS version or hardware"});
    return false;
  }

  if (@available(macOS LOW_LATENCY_AND_SVC_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;
    }
  }

  return true;
}

void VTVideoEncodeAccelerator::DestroyCompressionSession() {
  if (compression_session_) {
    VTCompressionSessionInvalidate(compression_session_.get());
    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));
}

base::TimeDelta VTVideoEncodeAccelerator::AssignMonotonicTimestamp() {
  const base::TimeDelta step = base::Seconds(1) / frame_rate_;
  auto result = next_timestamp_;
  next_timestamp_ += step;
  return result;
}

}  // namespace media