// Copyright 2017 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/filters/offloading_video_decoder.h"

#include <algorithm>
#include <memory>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/synchronization/atomic_flag.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "media/base/cdm_context.h"
#include "media/base/decoder_buffer.h"
#include "media/base/video_frame.h"

namespace media {

// Helper class which manages cancellation of Decode() after Reset() and makes
// it easier to destruct on the proper thread.
class CancellationHelper {
 public:
  CancellationHelper(std::unique_ptr<OffloadableVideoDecoder> decoder)
      : cancellation_flag_(std::make_unique<base::AtomicFlag>()),
        decoder_(std::move(decoder)) {}

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

  // Safe to call from any thread.
  void Cancel() { cancellation_flag_->Set(); }

  void Decode(scoped_refptr<DecoderBuffer> buffer,
              VideoDecoder::DecodeCB decode_cb) {
    if (cancellation_flag_->IsSet()) {
      std::move(decode_cb).Run(DecoderStatus::Codes::kAborted);
      return;
    }

    decoder_->Decode(std::move(buffer), std::move(decode_cb));
  }

  void Reset(base::OnceClosure reset_cb) {
    // OffloadableVideoDecoders are required to have a synchronous Reset(), so
    // we don't need to wait for the Reset to complete. Despite this, we don't
    // want to run |reset_cb| before we've reset the cancellation flag or the
    // client may end up issuing another Reset() before this code runs.
    decoder_->Reset(base::DoNothing());
    cancellation_flag_ = std::make_unique<base::AtomicFlag>();
    std::move(reset_cb).Run();
  }

  OffloadableVideoDecoder* decoder() const { return decoder_.get(); }

 private:
  std::unique_ptr<base::AtomicFlag> cancellation_flag_;
  std::unique_ptr<OffloadableVideoDecoder> decoder_;
};

OffloadingVideoDecoder::OffloadingVideoDecoder(
    int min_offloading_width,
    std::vector<VideoCodec> supported_codecs,
    std::unique_ptr<OffloadableVideoDecoder> decoder)
    : min_offloading_width_(min_offloading_width),
      supported_codecs_(std::move(supported_codecs)),
      helper_(std::make_unique<CancellationHelper>(std::move(decoder))) {
  DETACH_FROM_SEQUENCE(sequence_checker_);
}

OffloadingVideoDecoder::~OffloadingVideoDecoder() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // The |helper_| must always be destroyed on the |offload_task_runner_| since
  // we may still have tasks posted to it.
  if (offload_task_runner_)
    offload_task_runner_->DeleteSoon(FROM_HERE, std::move(helper_));
}

VideoDecoderType OffloadingVideoDecoder::GetDecoderType() const {
  // This call is expected to be static and safe to call from any thread.
  return helper_->decoder()->GetDecoderType();
}

void OffloadingVideoDecoder::Initialize(const VideoDecoderConfig& config,
                                        bool low_delay,
                                        CdmContext* cdm_context,
                                        InitCB init_cb,
                                        const OutputCB& output_cb,
                                        const WaitingCB& waiting_cb) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(config.IsValidConfig());

  const bool disable_offloading =
      config.is_encrypted() ||
      config.coded_size().width() < min_offloading_width_ ||
      !std::ranges::contains(supported_codecs_, config.codec());

  if (initialized_) {
    initialized_ = false;

    // We're transitioning from offloading to no offloading, so detach from the
    // offloading thread so we can run on the media thread.
    if (disable_offloading && offload_task_runner_) {
      offload_task_runner_->PostTaskAndReply(
          FROM_HERE,
          base::BindOnce(&OffloadableVideoDecoder::Detach,
                         base::Unretained(helper_->decoder())),
          // We must trampoline back through OffloadingVideoDecoder because it's
          // possible for this class to be destroyed during Initialize().
          base::BindOnce(&OffloadingVideoDecoder::Initialize,
                         weak_factory_.GetWeakPtr(), config, low_delay,
                         cdm_context, std::move(init_cb), output_cb,
                         waiting_cb));
      return;
    }

    // We're transitioning from no offloading to offloading, so detach from the
    // media thread so we can run on the offloading thread.
    if (!disable_offloading && !offload_task_runner_)
      helper_->decoder()->Detach();
  }

  DCHECK(!initialized_);
  initialized_ = true;

  // Offloaded decoders expect asynchronous execution of callbacks; even if we
  // aren't currently using the offload thread.
  InitCB bound_init_cb = base::BindPostTaskToCurrentDefault(std::move(init_cb));
  OutputCB bound_output_cb = base::BindPostTaskToCurrentDefault(output_cb);

  // If we're not offloading just pass through to the wrapped decoder.
  if (disable_offloading) {
    offload_task_runner_ = nullptr;
    helper_->decoder()->Initialize(config, low_delay, cdm_context,
                                   std::move(bound_init_cb), bound_output_cb,
                                   waiting_cb);
    return;
  }

  if (!offload_task_runner_) {
    offload_task_runner_ = base::ThreadPool::CreateSequencedTaskRunner(
        {base::MayBlock(), base::TaskPriority::USER_BLOCKING});
  }

  offload_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&OffloadableVideoDecoder::Initialize,
                     base::Unretained(helper_->decoder()), config, low_delay,
                     cdm_context, std::move(bound_init_cb), bound_output_cb,
                     waiting_cb));
}

void OffloadingVideoDecoder::Decode(scoped_refptr<DecoderBuffer> buffer,
                                    DecodeCB decode_cb) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(buffer);
  DCHECK(decode_cb);

  DecodeCB bound_decode_cb =
      base::BindPostTaskToCurrentDefault(std::move(decode_cb));
  if (!offload_task_runner_) {
    helper_->decoder()->Decode(std::move(buffer), std::move(bound_decode_cb));
    return;
  }

  offload_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&CancellationHelper::Decode,
                                base::Unretained(helper_.get()),
                                std::move(buffer), std::move(bound_decode_cb)));
}

void OffloadingVideoDecoder::Reset(base::OnceClosure reset_cb) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  base::OnceClosure bound_reset_cb =
      base::BindPostTaskToCurrentDefault(std::move(reset_cb));
  if (!offload_task_runner_) {
    helper_->Reset(std::move(bound_reset_cb));
  } else {
    helper_->Cancel();
    offload_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&CancellationHelper::Reset,
                                  base::Unretained(helper_.get()),
                                  std::move(bound_reset_cb)));
  }
}

int OffloadingVideoDecoder::GetMaxDecodeRequests() const {
  // If we're offloading, try to parallelize decodes as well. Take care when
  // adjusting this number as it may dramatically increase memory usage and
  // reduce seek times. See http://crbug.com/731841.
  //
  // The current value of 2 was determined via experimental adjustment until a
  // 4K60 VP9 playback dropped zero frames.
  return offload_task_runner_ ? 2 : 1;
}

}  // namespace media
