chromium/media/gpu/mac/video_toolbox_video_decoder.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 "media/gpu/mac/video_toolbox_video_decoder.h"

#include <CoreMedia/CoreMedia.h>
#include <VideoToolbox/VideoToolbox.h>

#include <memory>
#include <utility>

#include "base/apple/scoped_cftyperef.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/scoped_policy.h"
#include "base/task/bind_post_task.h"
#include "media/base/decoder_status.h"
#include "media/base/media_log.h"
#include "media/base/media_switches.h"
#include "media/base/supported_types.h"
#include "media/base/video_frame.h"
#include "media/gpu/accelerated_video_decoder.h"
#include "media/gpu/av1_decoder.h"
#include "media/gpu/h264_decoder.h"
#include "media/gpu/mac/video_toolbox_av1_accelerator.h"
#include "media/gpu/mac/video_toolbox_decompression_metadata.h"
#include "media/gpu/mac/video_toolbox_h264_accelerator.h"
#include "media/gpu/mac/video_toolbox_vp9_accelerator.h"
#include "media/gpu/vp9_decoder.h"
#include "ui/gfx/geometry/size.h"

#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
#include "media/gpu/h265_decoder.h"
#include "media/gpu/mac/video_toolbox_h265_accelerator.h"
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)

namespace media {

namespace {

bool SupportsH264() {
  return VTIsHardwareDecodeSupported(kCMVideoCodecType_H264);
}

bool InitializeVP9() {
#if BUILDFLAG(IS_MAC)
  VTRegisterSupplementalVideoDecoderIfAvailable(kCMVideoCodecType_VP9);
  return VTIsHardwareDecodeSupported(kCMVideoCodecType_VP9);
#else
  // TODO(crbug.com/40269929): Enable VP9 on iOS.
  return false;
#endif
}

bool SupportsVP9() {
  static const bool initialized = InitializeVP9();
  return initialized;
}

bool SupportsAV1() {
  return VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1);
}

#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
bool SupportsHEVC() {
  return base::FeatureList::IsEnabled(media::kPlatformHEVCDecoderSupport);
}
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)

}  // namespace

VideoToolboxVideoDecoder::VideoToolboxVideoDecoder(
    scoped_refptr<base::SequencedTaskRunner> task_runner,
    std::unique_ptr<MediaLog> media_log,
    const gpu::GpuDriverBugWorkarounds& gpu_workarounds,
    scoped_refptr<base::SequencedTaskRunner> gpu_task_runner,
    GetCommandBufferStubCB get_stub_cb)
    : task_runner_(std::move(task_runner)),
      media_log_(std::move(media_log)),
      gpu_workarounds_(gpu_workarounds),
      gpu_task_runner_(std::move(gpu_task_runner)),
      get_stub_cb_(std::move(get_stub_cb)),
      video_toolbox_(
          task_runner_,
          media_log_->Clone(),
          base::BindRepeating(&VideoToolboxVideoDecoder::OnVideoToolboxOutput,
                              base::Unretained(this)),
          base::BindRepeating(&VideoToolboxVideoDecoder::OnVideoToolboxError,
                              base::Unretained(this))),
      output_queue_(task_runner_) {
  DVLOG(1) << __func__;
}

VideoToolboxVideoDecoder::~VideoToolboxVideoDecoder() {
  DVLOG(1) << __func__;
}

bool VideoToolboxVideoDecoder::NeedsBitstreamConversion() const {
  DVLOG(4) << __func__;
  return true;
}

int VideoToolboxVideoDecoder::GetMaxDecodeRequests() const {
  DVLOG(4) << __func__;
  // This is kMaxVideoFrames, and it seems to have worked okay so far.
  return 4;
}

VideoDecoderType VideoToolboxVideoDecoder::GetDecoderType() const {
  DVLOG(4) << __func__;
  return VideoDecoderType::kVideoToolbox;
}

void VideoToolboxVideoDecoder::Initialize(const VideoDecoderConfig& config,
                                          bool low_delay,
                                          CdmContext* cdm_context,
                                          InitCB init_cb,
                                          const OutputCB& output_cb,
                                          const WaitingCB& waiting_cb) {
  DVLOG(1) << __func__;
  DCHECK(decode_cbs_.empty());
  DCHECK(config.IsValidConfig());

  if (has_error_) {
    task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(init_cb), DecoderStatus::Codes::kFailed));
    return;
  }

  // TODO(crbug.com/40227557): Distinguish unsupported profile from unsupported
  // codec.
  // TODO(crbug.com/40227557): Make sure that config.profile() matches
  // config.codec().
  // TODO(crbug.com/40227557): Check that the size is supported.
  bool profile_supported = false;
  for (const auto& supported_config :
       GetSupportedVideoDecoderConfigs(gpu_workarounds_)) {
    if (supported_config.profile_min <= config.profile() &&
        config.profile() <= supported_config.profile_max) {
      profile_supported = true;
      break;
    }
  }

  // If we don't have support support for a given codec, try to initialize
  // anyways -- otherwise we're certain to fail playback.
  if (!profile_supported && IsBuiltInVideoCodec(config.codec())) {
    task_runner_->PostTask(
        FROM_HERE, base::BindOnce(std::move(init_cb),
                                  DecoderStatus::Codes::kUnsupportedProfile));
    NotifyError(DecoderStatus::Codes::kUnsupportedProfile);
    return;
  }

  if (config.is_encrypted()) {
    task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(init_cb),
                       DecoderStatus::Codes::kUnsupportedEncryptionMode));
    NotifyError(DecoderStatus::Codes::kUnsupportedEncryptionMode);
    return;
  }

  // If this is a reconfiguration, drop in-flight outputs.
  if (accelerator_) {
    ResetInternal(DecoderStatus::Codes::kAborted);
  }

  // Create a new Accelerator for the configuration.
  auto accelerator_decode_cb = base::BindRepeating(
      &VideoToolboxVideoDecoder::OnAcceleratorDecode, base::Unretained(this));
  auto accelerator_output_cb = base::BindRepeating(
      &VideoToolboxVideoDecoder::OnAcceleratorOutput, base::Unretained(this));

  switch (VideoCodecProfileToVideoCodec(config.profile())) {
    case VideoCodec::kH264:
      accelerator_ = std::make_unique<H264Decoder>(
          std::make_unique<VideoToolboxH264Accelerator>(
              media_log_->Clone(), std::move(accelerator_decode_cb),
              std::move(accelerator_output_cb)),
          config.profile(), config.color_space_info());
      break;

    case VideoCodec::kVP9:
      accelerator_ = std::make_unique<VP9Decoder>(
          std::make_unique<VideoToolboxVP9Accelerator>(
              media_log_->Clone(), config.hdr_metadata(),
              std::move(accelerator_decode_cb),
              std::move(accelerator_output_cb)),
          config.profile(), config.color_space_info());
      break;

    case VideoCodec::kAV1:
      accelerator_ = std::make_unique<AV1Decoder>(
          std::make_unique<VideoToolboxAV1Accelerator>(
              media_log_->Clone(), config.hdr_metadata(),
              std::move(accelerator_decode_cb),
              std::move(accelerator_output_cb)),
          config.profile(), config.color_space_info());
      break;

#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
    case VideoCodec::kHEVC:
      accelerator_ = std::make_unique<H265Decoder>(
          std::make_unique<VideoToolboxH265Accelerator>(
              media_log_->Clone(), std::move(accelerator_decode_cb),
              std::move(accelerator_output_cb)),
          config.profile(), config.color_space_info());
      break;
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)

    default:
      task_runner_->PostTask(
          FROM_HERE, base::BindOnce(std::move(init_cb),
                                    DecoderStatus::Codes::kUnsupportedCodec));
      NotifyError(DecoderStatus::Codes::kUnsupportedCodec);
      return;
  }

  // Save the active configuration.
  config_ = config;
  output_queue_.SetOutputCB(output_cb);

  task_runner_->PostTask(
      FROM_HERE, base::BindOnce(std::move(init_cb), DecoderStatus::Codes::kOk));
}

void VideoToolboxVideoDecoder::Decode(scoped_refptr<DecoderBuffer> buffer,
                                      DecodeCB decode_cb) {
  DVLOG(3) << __func__ << " pts=" << buffer->timestamp().InMilliseconds();

  if (has_error_) {
    task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(decode_cb), DecoderStatus::Codes::kFailed));
    return;
  }

  // Flushes are handled differently from ordinary decodes.
  if (buffer->end_of_stream()) {
    if (!accelerator_->Flush()) {
      task_runner_->PostTask(
          FROM_HERE, base::BindOnce(std::move(decode_cb),
                                    DecoderStatus::Codes::kMalformedBitstream));
      NotifyError(DecoderStatus::Codes::kMalformedBitstream);
      return;
    }
    // Must be called after `accelerator_->Flush()` so that all outputs will
    // have been scheduled already.
    output_queue_.Flush(std::move(decode_cb));
    return;
  }

  decode_cbs_.push(std::move(decode_cb));
  accelerator_->SetStream(-1, *buffer);
  while (true) {
    // `active_decode_` is used in OnAcceleratorDecode() callbacks to look up
    // decode metadata.
    active_decode_ = buffer;
    AcceleratedVideoDecoder::DecodeResult result = accelerator_->Decode();
    active_decode_.reset();

    switch (result) {
      case AcceleratedVideoDecoder::kDecodeError:
      case AcceleratedVideoDecoder::kRanOutOfSurfaces:
      case AcceleratedVideoDecoder::kTryAgain:
        // More specific reasons are logged to the media log.
        NotifyError(DecoderStatus::Codes::kMalformedBitstream);
        return;

      case AcceleratedVideoDecoder::kConfigChange:
        continue;

      case AcceleratedVideoDecoder::kRanOutOfStreamData:
        // The accelerator may not have produced any sample for decoding.
        ReleaseDecodeCallbacks();
        return;
    }
  }
}

void VideoToolboxVideoDecoder::Reset(base::OnceClosure reset_cb) {
  DVLOG(1) << __func__;

  if (has_error_) {
    task_runner_->PostTask(FROM_HERE, std::move(reset_cb));
    return;
  }

  ResetInternal(DecoderStatus::Codes::kAborted);
  task_runner_->PostTask(FROM_HERE, std::move(reset_cb));
}

void VideoToolboxVideoDecoder::NotifyError(DecoderStatus status) {
  DVLOG(1) << __func__;

  if (has_error_) {
    return;
  }

  has_error_ = true;
  ResetInternal(status);
}

void VideoToolboxVideoDecoder::ResetInternal(DecoderStatus status) {
  DVLOG(4) << __func__;

  while (!decode_cbs_.empty()) {
    task_runner_->PostTask(
        FROM_HERE, base::BindOnce(std::move(decode_cbs_.front()), status));
    decode_cbs_.pop();
  }

  if (accelerator_) {
    accelerator_->Reset();
  }
  video_toolbox_.Reset();
  output_queue_.Reset(status);

  // Drop in-flight conversions.
  converter_weak_this_factory_.InvalidateWeakPtrs();
  num_conversions_ = 0;
}

void VideoToolboxVideoDecoder::ReleaseDecodeCallbacks() {
  DVLOG(4) << __func__;
  DCHECK(!has_error_);

  while (decode_cbs_.size() > video_toolbox_.NumDecodes() + num_conversions_) {
    task_runner_->PostTask(FROM_HERE,
                           base::BindOnce(std::move(decode_cbs_.front()),
                                          DecoderStatus::Codes::kOk));
    decode_cbs_.pop();
  }
}

void VideoToolboxVideoDecoder::OnAcceleratorDecode(
    base::apple::ScopedCFTypeRef<CMSampleBufferRef> sample,
    VideoToolboxDecompressionSessionMetadata session_metadata,
    scoped_refptr<CodecPicture> picture) {
  DVLOG(4) << __func__
           << " pts=" << active_decode_->timestamp().InMilliseconds();
  DCHECK(active_decode_);

  auto metadata = std::make_unique<VideoToolboxDecodeMetadata>();
  metadata->picture = std::move(picture);
  metadata->timestamp = active_decode_->timestamp();
  metadata->duration = active_decode_->duration();
  metadata->aspect_ratio = config_.aspect_ratio();
  metadata->color_space = accelerator_->GetVideoColorSpace().ToGfxColorSpace();
  if (!metadata->color_space.IsValid()) {
    // Note: It is expected that the accelerated video decoders are already
    // doing something similar, since the config color space is being provided
    // to them.
    metadata->color_space = config_.color_space_info().ToGfxColorSpace();
  }

  metadata->hdr_metadata = accelerator_->GetHDRMetadata();
  if (!metadata->hdr_metadata) {
    // Note: The VP9 accelerator contains this same logic so that the format
    // description can include HDR metadata (there is no in-band HDR metadata
    // in VP9). The other accelerators use only in-band HDR metadata.
    metadata->hdr_metadata = config_.hdr_metadata();
  }

  metadata->session_metadata = session_metadata;

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

void VideoToolboxVideoDecoder::OnAcceleratorOutput(
    scoped_refptr<CodecPicture> picture) {
  DVLOG(3) << __func__;
  output_queue_.SchedulePicture(std::move(picture));
}

void VideoToolboxVideoDecoder::OnVideoToolboxOutput(
    base::apple::ScopedCFTypeRef<CVImageBufferRef> image,
    std::unique_ptr<VideoToolboxDecodeMetadata> metadata) {
  DVLOG(4) << __func__ << " pts=" << metadata->timestamp.InMilliseconds();

  if (has_error_) {
    return;
  }

  // Check if the frame was dropped.
  // TODO(crbug.com/40227557): Notify the output queue of dropped frames.
  if (!image) {
    ReleaseDecodeCallbacks();
    return;
  }

  // Lazily create `converter_`.
  if (!converter_) {
    converter_ = base::MakeRefCounted<VideoToolboxFrameConverter>(
        gpu_task_runner_, media_log_->Clone(), std::move(get_stub_cb_));
  }

  gpu_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          &VideoToolboxFrameConverter::Convert, converter_, std::move(image),
          std::move(metadata),
          base::BindPostTask(
              task_runner_,
              base::BindOnce(&VideoToolboxVideoDecoder::OnConverterOutput,
                             converter_weak_this_factory_.GetWeakPtr()))));

  ++num_conversions_;
}

void VideoToolboxVideoDecoder::OnVideoToolboxError(DecoderStatus status) {
  DVLOG(1) << __func__;
  NotifyError(std::move(status));
}

void VideoToolboxVideoDecoder::OnConverterOutput(
    scoped_refptr<VideoFrame> frame,
    std::unique_ptr<VideoToolboxDecodeMetadata> metadata) {
  DVLOG(4) << __func__ << " pts=" << metadata->timestamp.InMilliseconds();

  if (has_error_) {
    return;
  }

  if (!frame) {
    // More specific reasons are logged to the media log.
    NotifyError(DecoderStatus::Codes::kFailedToGetVideoFrame);
    return;
  }

  CHECK_GT(num_conversions_, 0u);
  --num_conversions_;

  // The output queue expects that all decode callbacks have been called at the
  // time that a flush completes (all outputs are fulfilled), so we must release
  // before fulfilling pictures (at least during a flush).
  //
  // It would be possible to obtain tighter bounds on the backpressure by moving
  // responsibility for releasing callbacks to the output queue implementation.
  ReleaseDecodeCallbacks();

  output_queue_.FulfillPicture(std::move(metadata->picture), std::move(frame));
}

// static
std::vector<SupportedVideoDecoderConfig>
VideoToolboxVideoDecoder::GetSupportedVideoDecoderConfigs(
    const gpu::GpuDriverBugWorkarounds& gpu_workarounds) {
  std::vector<SupportedVideoDecoderConfig> supported;

  // TODO(crbug.com/40227557): Test support for other H.264 profiles.
  // TODO(crbug.com/40227557): Exclude resolutions that are not accelerated.
  // TODO(crbug.com/40227557): Check if higher resolutions are supported.
  if (!gpu_workarounds.disable_accelerated_h264_decode && SupportsH264()) {
    supported.emplace_back(
        /*profile_min=*/H264PROFILE_BASELINE,
        /*profile_max=*/H264PROFILE_HIGH,
        /*coded_size_min=*/gfx::Size(16, 16),
        /*coded_size_max=*/gfx::Size(4096, 4096),
        /*allow_encrypted=*/false,
        /*require_encrypted=*/false);
  }

  if (!gpu_workarounds.disable_accelerated_vp9_decode && SupportsVP9()) {
    supported.emplace_back(
        /*profile_min=*/VP9PROFILE_PROFILE0,
        /*profile_max=*/VP9PROFILE_PROFILE0,
        /*coded_size_min=*/gfx::Size(16, 16),
        /*coded_size_max=*/gfx::Size(4096, 4096),
        /*allow_encrypted=*/false,
        /*require_encrypted=*/false);
    if (!gpu_workarounds.disable_accelerated_vp9_profile2_decode) {
      supported.emplace_back(
          /*profile_min=*/VP9PROFILE_PROFILE2,
          /*profile_max=*/VP9PROFILE_PROFILE2,
          /*coded_size_min=*/gfx::Size(16, 16),
          /*coded_size_max=*/gfx::Size(4096, 4096),
          /*allow_encrypted=*/false,
          /*require_encrypted=*/false);
    }
  }

  if (!gpu_workarounds.disable_accelerated_av1_decode && SupportsAV1()) {
    supported.emplace_back(
        /*profile_min=*/AV1PROFILE_PROFILE_MAIN,
        /*profile_max=*/AV1PROFILE_PROFILE_MAIN,
        /*coded_size_min=*/gfx::Size(16, 16),
        /*coded_size_max=*/gfx::Size(8192, 8192),
        /*allow_encrypted=*/false,
        /*require_encrypted=*/false);
  }

#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
  if (!gpu_workarounds.disable_accelerated_hevc_decode && SupportsHEVC()) {
    supported.emplace_back(
        /*profile_min=*/HEVCPROFILE_MIN,
        /*profile_max=*/HEVCPROFILE_MAX,
        /*coded_size_min=*/gfx::Size(16, 16),
        /*coded_size_max=*/gfx::Size(8192, 8192),
        /*allow_encrypted=*/false,
        /*require_encrypted=*/false);
    supported.emplace_back(
        /*profile_min=*/HEVCPROFILE_REXT,
        /*profile_max=*/HEVCPROFILE_REXT,
        /*coded_size_min=*/gfx::Size(16, 16),
        /*coded_size_max=*/gfx::Size(8192, 8192),
        /*allow_encrypted=*/false,
        /*require_encrypted=*/false);
  }
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)

  return supported;
}

}  // namespace media