// Copyright 2013 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/cdm/library_cdm/clear_key_cdm/cdm_video_decoder.h"

#include <memory>
#include <optional>
#include <utility>

#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/containers/queue.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/no_destructor.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
// Necessary to convert async media::VideoDecoder to sync CdmVideoDecoder.
// Typically not recommended for production code, but is ok here since
// ClearKeyCdm is only for testing.
#include "base/run_loop.h"
#include "base/task/single_thread_task_executor.h"
#include "media/base/decoder_status.h"
#include "media/base/media_switches.h"
#include "media/base/media_util.h"
#include "media/base/video_decoder.h"
#include "media/base/video_frame.h"
#include "media/cdm/cdm_type_conversion.h"
#include "media/cdm/library_cdm/cdm_host_proxy.h"
#include "media/media_buildflags.h"
#include "third_party/libyuv/include/libyuv/planar_functions.h"

#if BUILDFLAG(ENABLE_LIBVPX)
#include "media/filters/vpx_video_decoder.h"
#endif

#if BUILDFLAG(ENABLE_DAV1D_DECODER)
#include "media/filters/dav1d_video_decoder.h"
#endif

#if BUILDFLAG(ENABLE_FFMPEG_VIDEO_DECODERS)
#include "media/filters/ffmpeg_video_decoder.h"
#endif

namespace media {

namespace {

media::VideoDecoderConfig ToClearMediaVideoDecoderConfig(
    const cdm::VideoDecoderConfig_3& config) {
  gfx::Size coded_size(config.coded_size.width, config.coded_size.height);

  VideoDecoderConfig media_config(
      ToMediaVideoCodec(config.codec), ToMediaVideoCodecProfile(config.profile),
      VideoDecoderConfig::AlphaMode::kIsOpaque,
      ToMediaColorSpace(config.color_space), kNoTransformation, coded_size,
      gfx::Rect(coded_size), coded_size,
      std::vector<uint8_t>(
          config.extra_data,
          UNSAFE_TODO(config.extra_data + config.extra_data_size)),
      EncryptionScheme::kUnencrypted);

  return media_config;
}

bool ToCdmVideoFrame(const VideoFrame& video_frame,
                     CdmHostProxy* cdm_host_proxy,
                     CdmVideoDecoder::CdmVideoFrame* cdm_video_frame) {
  CHECK(cdm_video_frame);

  if (!video_frame.HasDirectCpuAccess()) {
    DVLOG(1) << "No direct CPU access to VideoFrame";
    return false;
  }

  if (!IsYuvPlanar(video_frame.format())) {
    DVLOG(1) << "Only YUV planar format supported";
    return false;
  }

  if (VideoFrame::NumPlanes(video_frame.format()) != 3u) {
    DVLOG(1) << "Only 3-plane format supported";
    return false;
  }

  auto cdm_video_format = ToCdmVideoFormat(video_frame.format());
  if (cdm_video_format == cdm::kUnknownVideoFormat) {
    DVLOG(1) << "VideoFrame has unsupported format: " << video_frame.format();
    return false;
  }

  // Get required allocation size for a tightly packed frame.
  auto space_required = VideoFrame::AllocationSize(video_frame.format(),
                                                   video_frame.coded_size());
  auto* buffer = cdm_host_proxy->Allocate(space_required);
  if (!buffer) {
    LOG(ERROR) << __func__ << ": Buffer allocation failed.";
    return false;
  }

  buffer->SetSize(base::checked_cast<uint32_t>(space_required));
  cdm_video_frame->SetFrameBuffer(buffer);
  cdm_video_frame->SetFormat(cdm_video_format);
  cdm_video_frame->SetSize(
      {video_frame.coded_size().width(), video_frame.coded_size().height()});
  cdm_video_frame->SetTimestamp(video_frame.timestamp().InMicroseconds());
  // TODO(crbug.com/40513452): Set ColorSpace here. It's not trivial to convert
  // a gfx::ColorSpace (from VideoFrame) to another other ColorSpace like
  // cdm::ColorSpace.

  static_assert(VideoFrame::Plane::kY == cdm::kYPlane && cdm::kYPlane == 0, "");
  static_assert(VideoFrame::Plane::kU == cdm::kUPlane && cdm::kUPlane == 1, "");
  static_assert(VideoFrame::Plane::kV == cdm::kVPlane && cdm::kVPlane == 2, "");

  uint8_t* dst = buffer->Data();
  uint32_t offset = 0;
  for (int plane = 0; plane < 3; ++plane) {
    const uint8_t* src = video_frame.data(plane);
    int src_stride = video_frame.stride(plane);
    int row_bytes = video_frame.row_bytes(plane);
    int rows = video_frame.rows(plane);

    auto cdm_plane = static_cast<cdm::VideoPlane>(plane);
    cdm_video_frame->SetPlaneOffset(cdm_plane, offset);
    // Since it's tightly packed, the stride is the same as row bytes.
    cdm_video_frame->SetStride(cdm_plane, row_bytes);

    libyuv::CopyPlane(src, src_stride, dst, row_bytes, row_bytes, rows);
    UNSAFE_TODO(dst += row_bytes * rows);
    offset += row_bytes * rows;
  }

  DCHECK_GE(space_required, offset) << ": Space mismatch";
  return true;
}

// Media VideoDecoders typically assumes a global environment where a lot of
// things are already setup in the process,
// e.g. base::SingleThreadTaskRunnerCurrentDefautHandle and
// base::CommandLine. These will be available in the component build because the
// CDM and the host is depending on the same base/ target. In static build, they
// will not be available and we have to setup it by ourselves.
void SetupGlobalEnvironmentIfNeeded() {
  // Creating a base::SingleThreadTaskExecutor to setup
  // base::SingleThreadTaskRunner::CurrentDefaultHandle.
  if (!base::SingleThreadTaskRunner::HasCurrentDefault()) {
    static base::NoDestructor<base::SingleThreadTaskExecutor> task_executor;
  }

  // Initialize CommandLine if not already initialized. Since this is a DLL,
  // just use empty arguments.
  if (!base::CommandLine::InitializedForCurrentProcess()) {
#if BUILDFLAG(IS_WIN)
    // Use InitUsingArgvForTesting() instead of Init() to avoid dependency on
    // shell32 API which might not work in the sandbox. See crbug.com/1242710.
    base::CommandLine::InitUsingArgvForTesting(0, nullptr);
#else
    base::CommandLine::Init(0, nullptr);
#endif
  }
}

// Adapts a media::VideoDecoder to a CdmVideoDecoder. Media VideoDecoders
// operations are asynchronous, often posting callbacks to the task runner. The
// CdmVideoDecoder operations are synchronous. Therefore, after calling
// media::VideoDecoder, we need to run a RunLoop manually and wait for the
// asynchronous operation to finish. The RunLoop must be of type
// |kNestableTasksAllowed| because we could be running the RunLoop in a task,
// e.g. in component builds when we share the same task runner as the host. In
// a static build, this is not necessary.
class VideoDecoderAdapter final : public CdmVideoDecoder {
 public:
  VideoDecoderAdapter(CdmHostProxy* cdm_host_proxy,
                      std::unique_ptr<VideoDecoder> video_decoder)
      : cdm_host_proxy_(cdm_host_proxy),
        video_decoder_(std::move(video_decoder)) {
    CHECK(cdm_host_proxy_);
  }

  VideoDecoderAdapter(const VideoDecoderAdapter&) = delete;
  VideoDecoderAdapter& operator=(const VideoDecoderAdapter&) = delete;

  ~VideoDecoderAdapter() final = default;

  // CdmVideoDecoder implementation.
  DecoderStatus Initialize(const cdm::VideoDecoderConfig_3& config) final {
    auto clear_config = ToClearMediaVideoDecoderConfig(config);
    DVLOG(1) << __func__ << ": " << clear_config.AsHumanReadableString();
    CHECK(!last_init_result_.has_value());

    // Initialize |video_decoder_| and wait for completion.
    base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
    video_decoder_->Initialize(
        clear_config,
        /* low_delay = */ false,
        /* cdm_context = */ nullptr,
        base::BindOnce(&VideoDecoderAdapter::OnInitialized,
                       weak_factory_.GetWeakPtr(), run_loop.QuitClosure()),
        base::BindRepeating(&VideoDecoderAdapter::OnVideoFrameReady,
                            weak_factory_.GetWeakPtr()),
        /* waiting_cb = */ base::DoNothing());
    run_loop.Run();

    auto result = std::move(last_init_result_.value());
    last_init_result_.reset();

    return result;
  }

  void Deinitialize() final {
    // Do nothing since |video_decoder_| supports reinitialization without
    // the need to deinitialize first.
  }

  void Reset() final {
    DVLOG(2) << __func__;

    // Reset |video_decoder_| and wait for completion.
    base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
    video_decoder_->Reset(base::BindOnce(&VideoDecoderAdapter::OnReset,
                                         weak_factory_.GetWeakPtr(),
                                         run_loop.QuitClosure()));
    run_loop.Run();
  }

  cdm::Status Decode(scoped_refptr<DecoderBuffer> buffer,
                     CdmVideoFrame* decoded_frame) final {
    DVLOG(3) << __func__;
    CHECK(!last_decode_status_.has_value());

    // Call |video_decoder_| Decode() and wait for completion.
    base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
    video_decoder_->Decode(
        std::move(buffer),
        base::BindOnce(&VideoDecoderAdapter::OnDecoded,
                       weak_factory_.GetWeakPtr(), run_loop.QuitClosure()));
    run_loop.Run();

    auto decode_status = last_decode_status_.value();
    last_decode_status_.reset();

    // "kAborted" shouldn't happen during a sync decode, so treat it as an
    // error.
    DCHECK_NE(decode_status.code(), DecoderStatus::Codes::kAborted);

    if (!decode_status.is_ok())
      return cdm::kDecodeError;

    if (decoded_video_frames_.empty())
      return cdm::kNeedMoreData;

    auto video_frame = decoded_video_frames_.front();
    decoded_video_frames_.pop();

    return ToCdmVideoFrame(*video_frame, cdm_host_proxy_, decoded_frame)
               ? cdm::kSuccess
               : cdm::kDecodeError;
  }

 private:
  void OnInitialized(base::OnceClosure quit_closure, DecoderStatus status) {
    DVLOG(1) << __func__ << " success = " << status.is_ok();
    CHECK(!last_init_result_.has_value());
    last_init_result_ = std::move(status);
    std::move(quit_closure).Run();
  }

  void OnVideoFrameReady(scoped_refptr<VideoFrame> video_frame) {
    // Do not queue EOS frames, which is not needed.
    if (video_frame->metadata().end_of_stream)
      return;

    decoded_video_frames_.push(std::move(video_frame));
  }

  void OnReset(base::OnceClosure quit_closure) {
    VideoFrameQueue empty_queue;
    std::swap(decoded_video_frames_, empty_queue);
    std::move(quit_closure).Run();
  }

  void OnDecoded(base::OnceClosure quit_closure, DecoderStatus decode_status) {
    CHECK(!last_decode_status_.has_value());
    last_decode_status_ = std::move(decode_status);
    std::move(quit_closure).Run();
  }

  const raw_ptr<CdmHostProxy> cdm_host_proxy_;
  std::unique_ptr<VideoDecoder> video_decoder_;

  // Results of |video_decoder_| operations. Set iff the callback of the
  // operation has been called.
  std::optional<DecoderStatus> last_init_result_;
  std::optional<DecoderStatus> last_decode_status_;

  // Queue of decoded video frames.
  using VideoFrameQueue = base::queue<scoped_refptr<VideoFrame>>;
  VideoFrameQueue decoded_video_frames_;

  base::WeakPtrFactory<VideoDecoderAdapter> weak_factory_{this};
};

}  // namespace

std::unique_ptr<CdmVideoDecoder> CreateVideoDecoder(
    CdmHostProxy* cdm_host_proxy,
    const cdm::VideoDecoderConfig_3& config,
    MediaLog* media_log) {
  SetupGlobalEnvironmentIfNeeded();

  std::unique_ptr<VideoDecoder> video_decoder;

#if BUILDFLAG(ENABLE_LIBVPX)
  if (config.codec == cdm::kCodecVp8 || config.codec == cdm::kCodecVp9)
    video_decoder = std::make_unique<VpxVideoDecoder>();
#endif

#if BUILDFLAG(ENABLE_DAV1D_DECODER)
  if (config.codec == cdm::kCodecAv1) {
    video_decoder = std::make_unique<Dav1dVideoDecoder>(media_log->Clone());
  }
#endif

#if BUILDFLAG(ENABLE_FFMPEG_VIDEO_DECODERS)
  if (!video_decoder)
    video_decoder = std::make_unique<FFmpegVideoDecoder>(media_log->Clone());
#endif

  if (!video_decoder)
    return nullptr;

  return std::make_unique<VideoDecoderAdapter>(cdm_host_proxy,
                                               std::move(video_decoder));
}

}  // namespace media
