// Copyright 2024 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/cast/encoding/media_video_encoder_wrapper.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/task/bind_post_task.h"
#include "base/task/thread_pool.h"
#include "build/build_config.h"
#include "media/base/async_destroy_video_encoder.h"
#include "media/base/encoder_status.h"
#include "media/base/media_switches.h"
#include "media/base/media_util.h"
#include "media/base/video_codecs.h"
#include "media/base/video_encoder.h"
#include "media/base/video_encoder_metrics_provider.h"
#include "media/base/video_frame.h"
#include "media/cast/cast_config.h"
#include "media/cast/common/openscreen_conversion_helpers.h"
#include "media/cast/common/sender_encoded_frame.h"
#include "media/cast/constants.h"
#include "media/cast/encoding/encoding_support.h"
#include "media/cast/encoding/video_encoder.h"
#include "media/media_buildflags.h"
#include "media/video/gpu_video_accelerator_factories.h"
#include "media/video/video_encode_accelerator_adapter.h"
#include "media/video/video_encoder_info.h"
#include "media_video_encoder_wrapper.h"

#if BUILDFLAG(ENABLE_LIBVPX)
#include "media/video/vpx_video_encoder.h"
#endif

#if BUILDFLAG(ENABLE_LIBAOM)
#include "media/video/av1_video_encoder.h"
#endif  // BUILDFLAG(ENABLE_LIBAOM)

namespace media::cast {
namespace {

std::unique_ptr<media::VideoEncoder> CreateHardwareEncoder(
    media::GpuVideoAcceleratorFactories& gpu_factories,
    scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
  // TODO(crbug.com/282984511): consider providing a non-null MediaLog.
  return std::make_unique<
      media::AsyncDestroyVideoEncoder<media::VideoEncodeAcceleratorAdapter>>(
      std::make_unique<media::VideoEncodeAcceleratorAdapter>(
          &gpu_factories, std::make_unique<media::NullMediaLog>(),
          std::move(task_runner)));
}

// TODO(crbug.com/282984511): consider adding support for H264 here, using
// the media::OpenH264VideoEncoder.
std::unique_ptr<media::VideoEncoder> CreateSoftwareEncoder(VideoCodec codec) {
  switch (codec) {
#if BUILDFLAG(ENABLE_LIBVPX)
    case VideoCodec::kVP8:
    case VideoCodec::kVP9:
      return std::make_unique<media::VpxVideoEncoder>();
#endif
#if BUILDFLAG(ENABLE_LIBAOM)
    case VideoCodec::kAV1:
      return std::make_unique<media::Av1VideoEncoder>();
#endif  // BUILDFLAG(ENABLE_LIBAOM)
    default:
      NOTREACHED() << "Unhandled codec. value=" << std::to_underlying(codec);
  }
}

// Encoder utilization is calculated differently depending on encoder
// acceleration:
//   * Hardware encoders are pipelined, so we use the maximum of the backlog of
//     frames waiting to be processed, and the processing time of each frame
//     relative to the target frame duration.
//   * Software encoders are typically synchronous, so we can directly map the
//     processing time of each frame relative to the target frame duration.
double CalculateEncoderUtilization(bool is_hardware_encoder,
                                   size_t backlog_size,
                                   base::TimeDelta processing_time,
                                   base::TimeDelta target_frame_duration) {
  double utilization = 0.0;
  if (is_hardware_encoder) {
    // The "pipeline" is the set of frames that is actively undergoing
    // processing. This is similar in size to the backlog but represents a
    // separate stage in the encoding process. This value is chosen based on the
    // capacity of a typical hardware encoder.
    static constexpr int kPipelineSize = 3;

    // The "backlog" is the set of frames that has been submitted for encoding
    // but not yet taken for processing. Our value is based on the size of the
    // pipeline, since the backlog is fully saturated when we have more frames
    // than the pipeline can process.
    static constexpr int kMaxBacklogSize = kPipelineSize + 1;

    // 1. Backlog Model: Is the encoder keeping up with the submission rate?
    double backlog_utilization =
        static_cast<double>(backlog_size) / kMaxBacklogSize;

    // 2. Latency Model: Is the pipeline taking too long glass-to-glass?
    double latency_utilization =
        processing_time / (target_frame_duration * kPipelineSize);

    // Hardware utilization is the worst-case of the two metrics.
    utilization = std::max(backlog_utilization, latency_utilization);
  } else {
    // Software is synchronous/bounded, so latency directly maps to CPU load.
    utilization = processing_time / target_frame_duration;
  }

  // Clamp to a safe range to prevent PID controller explosion on anomalies
  // (e.g., system sleep or thread starvation).
  return std::clamp(utilization, 0.0, 2.0);
}

// Must be called on the ENCODER thread, which resolves to VIDEO for hardware
// encoding, and MAIN for software encoding.
void CallInitializeEncoder(media::VideoEncoder& encoder,
                           VideoCodecProfile profile,
                           const media::VideoEncoder::Options& options,
                           media::VideoEncoder::EncoderInfoCB info_cb,
                           media::VideoEncoder::OutputCB output_cb,
                           media::VideoEncoder::EncoderStatusCB done_cb) {
  encoder.Initialize(profile, options, std::move(info_cb), std::move(output_cb),
                     std::move(done_cb));

  // Our callbacks post to the correct thread, instead of needing the encoder to
  // manage posting back.
  encoder.DisablePostedCallbacks();
}

// Must be called on the ENCODER thread, which resolves to VIDEO for hardware
// encoding, and MAIN for software encoding.
void CallEncodeVideoFrame(
    media::VideoEncoder& encoder,
    scoped_refptr<media::VideoFrame> video_frame,
    const media::VideoEncoder::EncodeOptions& encode_options,
    media::VideoEncoder::EncoderStatusCB done_cb) {
  encoder.Encode(std::move(video_frame), encode_options, std::move(done_cb));
}

// Must be called on the ENCODER thread, which resolves to VIDEO for hardware
// encoding, and MAIN for software encoding.
//
// NOTE: since this call depends on a successful flush, `flush_result` is
// checked before the encoder is accessed.
void CallChangeOptions(media::VideoEncoder& encoder,
                       media::VideoEncoder::Options options,
                       media::VideoEncoder::OutputCB output_cb,
                       media::VideoEncoder::EncoderStatusCB done_cb) {
  encoder.ChangeOptions(std::move(options), std::move(output_cb),
                        std::move(done_cb));
}

// Must be called on the ENCODER thread, which resolves to VIDEO for hardware
// encoding, and MAIN for software encoding.
void CallFlush(media::VideoEncoder& encoder,
               media::VideoEncoder::EncoderStatusCB done_cb) {
  encoder.Flush(std::move(done_cb));
}

// TODO(crbug.com/282984511): just use EncoderStatus directly once we remove
// media::cast::VideoEncoder.
OperationalStatus ToOperationalStatus(EncoderStatus status) {
  switch (status.code()) {
    case EncoderStatus::Codes::kOk:
      return STATUS_INITIALIZED;

    case EncoderStatus::Codes::kEncoderInitializeNeverCompleted:
    case EncoderStatus::Codes::kEncoderInitializeTwice:
    case EncoderStatus::Codes::kEncoderInitializationError:
      return STATUS_CODEC_INIT_FAILED;

    case EncoderStatus::Codes::kEncoderUnsupportedProfile:
    case EncoderStatus::Codes::kEncoderUnsupportedCodec:
      return STATUS_UNSUPPORTED_CODEC;

    case EncoderStatus::Codes::kEncoderUnsupportedConfig:
      return STATUS_INVALID_CONFIGURATION;

    // Most encoder statuses can just be considered runtime errors.
    default:
      return STATUS_CODEC_RUNTIME_ERROR;
  }
}

}  // namespace

MediaVideoEncoderWrapper::MediaVideoEncoderWrapper(
    scoped_refptr<CastEnvironment> cast_environment,
    const FrameSenderConfig& video_config,
    std::unique_ptr<VideoEncoderMetricsProvider> metrics_provider,
    StatusChangeCallback status_change_cb,
    media::GpuVideoAcceleratorFactories* gpu_factories)
    : cast_environment_(std::move(cast_environment)),
      metrics_provider_(std::move(metrics_provider)),
      status_change_cb_(std::move(status_change_cb)),
      gpu_factories_(gpu_factories),
      is_hardware_encoder_(video_config.use_hardware_encoder),
      codec_(video_config.video_codec()),
      target_frame_duration_(base::Seconds(1) / video_config.max_frame_rate),
      encoder_(nullptr,
               base::OnTaskRunnerDeleter(cast_environment_->GetTaskRunner(
                   CastEnvironment::ThreadId::kVideo))),
      frame_complexity_estimator_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE})) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  CHECK(metrics_provider_);
  CHECK(status_change_cb_);

  encode_options_.key_frame = true;
  options_.bitrate = Bitrate::ConstantBitrate(
      base::checked_cast<uint32_t>(video_config.start_bitrate));

  // NOTE: the H264 encoder can produce either AVC or annexb formatted frames.
  // Annexb is better supported by Cast receivers.
  if (codec_ == media::VideoCodec::kH264) {
    options_.avc.produce_annexb = true;
  }

  // NOTE: since we don't actually know the frame size until the first
  // frame, the encoder will not get created until the first call to
  // `EncodeVideoFrame`. However, it is ready to start receiving frames.
  status_change_cb_.Run(STATUS_INITIALIZED);
}

MediaVideoEncoderWrapper::~MediaVideoEncoderWrapper() {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  weak_factory_.InvalidateWeakPtrs();
}

bool MediaVideoEncoderWrapper::EncodeVideoFrame(
    scoped_refptr<media::VideoFrame> video_frame,
    base::TimeTicks reference_time,
    FrameEncodedCallback frame_encoded_callback) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  CHECK(!video_frame->visible_rect().IsEmpty());

  // TODO(crbug.com/282984511): consider adding optimization to store frames
  // that are queued for encoding and sending them once updating options is
  // complete.
  if (num_pending_updates_ > 0) {
    return false;
  }

  // Construct and initialize the encoder on the first call to this method.
  const gfx::Size frame_size = video_frame->visible_rect().size();
  if (frame_size != options_.frame_size) {
    if (options_.frame_size.IsEmpty()) {
      // First frame, reconstruct immediately to avoid startup latency.
      options_.frame_size = frame_size;
      ConstructEncoder();
    } else if (!target_resize_size_ || frame_size != *target_resize_size_) {
      // Debounce subsequent size changes. Only restart timer if the new size
      // is different from the current target.
      target_resize_size_ = frame_size;
      resize_debounce_timer_.Start(
          FROM_HERE, base::Milliseconds(250),
          base::BindOnce(
              &MediaVideoEncoderWrapper::ReconstructEncoderForNewSize,
              weak_factory_.GetWeakPtr(), frame_size));
    }
  } else if (resize_debounce_timer_.IsRunning()) {
    // If the frame size returns to the original size before the timer fires,
    // cancel the resize.
    resize_debounce_timer_.Stop();
    target_resize_size_.reset();
  }
  CHECK(encoder_);

  frame_complexity_estimator_
      .AsyncCall(encode_options_.key_frame
                     ? &FrameComplexityEstimator::EstimateForKeyFrame
                     : &FrameComplexityEstimator::EstimateForDeltaFrame)
      .WithArgs(std::cref(*video_frame))
      .Then(base::BindOnce(
          &MediaVideoEncoderWrapper::OnComplexityEstimated,
          weak_factory_.GetWeakPtr(), video_frame, encode_options_,
          reference_time, std::move(frame_encoded_callback), encoder_version_));

  encode_options_.key_frame = false;

  return true;
}

void MediaVideoEncoderWrapper::OnComplexityEstimated(
    scoped_refptr<media::VideoFrame> video_frame,
    media::VideoEncoder::EncodeOptions encode_options,
    base::TimeTicks reference_time,
    FrameEncodedCallback frame_encoded_callback,
    int encoder_version,
    std::optional<double> estimated_complexity) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));

  if (encoder_version != encoder_version_) {
    // The encoder was reconstructed while we were estimating the quantizer.
    // Drop this frame as it was meant for the old encoder.
    std::move(frame_encoded_callback).Run(nullptr);
    return;
  }

  CHECK(encoder_);

  recent_metadata_.emplace(
      video_frame->metadata().capture_begin_time,
      video_frame->metadata().capture_end_time, base::TimeTicks::Now(),
      ToRtpTimeTicks(video_frame->timestamp(), kVideoFrequency), reference_time,
      GetFrameDuration(*video_frame), estimated_complexity,
      std::move(frame_encoded_callback));

  // Now that `GetFrameDuration` has been called, we can update the last frame
  // timestamp. It must be monotonically increasing.
  if (last_frame_timestamp_) {
    CHECK_GT(video_frame->timestamp(), last_frame_timestamp_.value());
  }
  last_frame_timestamp_ = video_frame->timestamp();

  auto encode_task = base::BindOnce(
      &CallEncodeVideoFrame, std::ref(*encoder_), std::move(video_frame),
      encode_options,
      CreateCallback(&MediaVideoEncoderWrapper::OnFrameEncodeDone,
                     encoder_version, reference_time));

  if (num_pending_updates_ > 0) {
    pending_encodes_.push_back(std::move(encode_task));
  } else {
    CallEncoderOnCorrectThread(std::move(encode_task));
  }
}

// Inform the encoder about the new target bit rate.
void MediaVideoEncoderWrapper::SetBitRate(uint32_t new_bit_rate) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  options_.bitrate = Bitrate::ConstantBitrate(new_bit_rate);

  // If this method is called before the encoder_ is constructed, the bitrate
  // will be set as part of construction.
  if (encoder_) {
    // If we have an encoder, we need to update its options.
    UpdateEncoderOptions();
  }
}

void MediaVideoEncoderWrapper::ReconstructEncoderForNewSize(
    const gfx::Size& new_size) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  target_resize_size_.reset();
  if (new_size != options_.frame_size) {
    options_.frame_size = new_size;
    ConstructEncoder();
  }
}

// Inform the encoder to encode the next frame as a key frame.
void MediaVideoEncoderWrapper::GenerateKeyFrame() {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  encode_options_.key_frame = true;
}

void MediaVideoEncoderWrapper::OnEncodedFrame(
    int encoder_version,
    VideoEncoderOutput output,
    std::optional<media::VideoEncoder::CodecDescription> description) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  if (encoder_version != encoder_version_) {
    return;
  }

  CachedMetadata& metadata = recent_metadata_.front();
  auto encoded_frame = std::make_unique<SenderEncodedFrame>();
  encoded_frame->is_key_frame = output.key_frame;
  encoded_frame->frame_id = next_frame_id_++;
  encoded_frame->referenced_frame_id = encoded_frame->is_key_frame
                                           ? encoded_frame->frame_id
                                           : encoded_frame->frame_id - 1;
  encoded_frame->rtp_timestamp = metadata.rtp_timestamp;
  encoded_frame->reference_time = metadata.reference_time;

  encoded_frame->encode_completion_time = cast_environment_->NowTicks();

  const base::TimeDelta processing_time =
      encoded_frame->encode_completion_time - metadata.encode_start_time;
  encoded_frame->encoder_utilization =
      ComputeAndRecordUtilization(processing_time);

  encoded_frame->lossiness =
      metadata.estimated_complexity
          ? ComputeLossiness(output.data.size(), metadata.frame_duration,
                             *metadata.estimated_complexity)
          : 0.0f;

  encoded_frame->capture_begin_time = metadata.capture_begin_time;
  encoded_frame->capture_end_time = metadata.capture_end_time;
  encoded_frame->data = std::move(output.data);

  auto frame_encoded_callback = std::move(metadata.frame_encoded_callback);
  recent_metadata_.pop();
  metrics_provider_->IncrementEncodedFrameCount();
  std::move(frame_encoded_callback).Run(std::move(encoded_frame));
}

double MediaVideoEncoderWrapper::ComputeAndRecordUtilization(
    base::TimeDelta processing_time) {
  const double raw_utilization =
      CalculateEncoderUtilization(is_hardware_encoder_, recent_metadata_.size(),
                                  processing_time, target_frame_duration_);

  // Apply EMA to smooth out I-frame spikes.
  // A weight of 0.1 for the new value is standard for 30-60fps media pipelines.
  static constexpr double kEmaWeight = 0.1;
  if (!ema_encoder_utilization_) {
    ema_encoder_utilization_ = raw_utilization;
  } else {
    ema_encoder_utilization_ = (raw_utilization * kEmaWeight) +
                               (*ema_encoder_utilization_ * (1.0 - kEmaWeight));
  }
  return *ema_encoder_utilization_;
}

// Computes a heuristic representing the visual quality degradation of the
// encoded frame. Upstream rate controllers use this "lossiness" metric to
// decide if they need to drop the framerate to give individual frames a
// larger bit budget.
//
// A higher return value indicates a more heavily compressed (lower quality)
// frame. This is estimated by multiplying the frame's bitrate utilization
// by its estimated spatial/temporal complexity.
double MediaVideoEncoderWrapper::ComputeLossiness(
    size_t data_size,
    base::TimeDelta frame_duration,
    double estimated_complexity) const {
  const double duration = frame_duration.InSecondsF();
  if (duration <= 0.0) {
    return 0.0;
  }

  // 1. Calculate the effective bitrate of this specific frame.
  const double data_size_in_bits = data_size * 8.0;
  const double actual_bitrate = data_size_in_bits / duration;

  // 2. Calculate bitrate utilization compared to target bitrate.
  // This measures how much of our target bandwidth budget this frame consumed.
  // If actual_bitrate > target_bitrate, the encoder is overshooting to handle
  // difficult content.
  const double target_bitrate = options_.bitrate->target_bps();
  CHECK_GT(target_bitrate, 0.0);
  const double bitrate_utilization = actual_bitrate / target_bitrate;

  // 3. Scale utilization by the estimated complexity of the frame.
  // The estimated_complexity represents how difficult the frame is to compress
  // (e.g., high motion or high detail). A frame that uses 100% of the bitrate
  // budget on a blank wall (low complexity) is not very lossy. A frame that
  // uses 100% of the budget on a confetti explosion (high complexity) is
  // assumed to be highly lossy.
  const double raw_lossiness = bitrate_utilization * estimated_complexity;

  // 4. Clamp to a safe upper bound to protect the rate controller's internal
  // state from timestamp jitter anomalies.
  constexpr double kMaxLossiness = 5.0;
  return std::clamp(raw_lossiness, 0.0, kMaxLossiness);
}

void MediaVideoEncoderWrapper::OnEncoderStatus(int encoder_version,
                                               EncoderStatus error) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  if (encoder_version != encoder_version_) {
    return;
  }
  if (!last_recorded_status_ || error != last_recorded_status_.value()) {
    last_recorded_status_ = error;

    if (!error.is_ok()) {
      CHECK(metrics_provider_);
      metrics_provider_->SetError(std::move(error));
    }

    // Running the status change callback may delete `this`, so this should be
    // done last.
    status_change_cb_.Run(ToOperationalStatus(error));
  }
}

void MediaVideoEncoderWrapper::OnEncoderInfo(
    int encoder_version,
    const VideoEncoderInfo& encoder_info) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  if (encoder_version != encoder_version_) {
    return;
  }
  // TODO(crbug.com/282984511): support handling `supports_frame_size_change`
  // property.
}

void MediaVideoEncoderWrapper::SetEncoderForTesting(
    std::unique_ptr<media::VideoEncoder> encoder) {
  encoder_is_overridden_for_testing_ = true;
  SetEncoder(std::move(encoder));
}

MediaVideoEncoderWrapper::CachedMetadata::CachedMetadata(
    std::optional<base::TimeTicks> capture_begin_time,
    std::optional<base::TimeTicks> capture_end_time,
    base::TimeTicks encode_start_time,
    RtpTimeTicks rtp_timestamp,
    base::TimeTicks reference_time,
    base::TimeDelta frame_duration,
    std::optional<double> estimated_complexity,
    FrameEncodedCallback frame_encoded_callback)
    : capture_begin_time(capture_begin_time),
      capture_end_time(capture_end_time),
      encode_start_time(encode_start_time),
      rtp_timestamp(rtp_timestamp),
      reference_time(reference_time),
      frame_duration(frame_duration),
      estimated_complexity(estimated_complexity),
      frame_encoded_callback(std::move(frame_encoded_callback)) {}
MediaVideoEncoderWrapper::CachedMetadata::CachedMetadata() = default;
MediaVideoEncoderWrapper::CachedMetadata::CachedMetadata(
    MediaVideoEncoderWrapper::CachedMetadata&& other) = default;
MediaVideoEncoderWrapper::CachedMetadata&
MediaVideoEncoderWrapper::CachedMetadata::operator=(
    MediaVideoEncoderWrapper::CachedMetadata&& other) = default;
MediaVideoEncoderWrapper::CachedMetadata::~CachedMetadata() = default;

void MediaVideoEncoderWrapper::ConstructEncoder() {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  encoder_version_++;

  // Clear any pending encodes and options updates meant for the old encoder.
  pending_encodes_.clear();
  num_pending_updates_ = 0;

  // Clear metadata and run callbacks with nullptr for any frames that were
  // in-flight to the old encoder, as the old encoder will be deleted and will
  // not emit them.
  while (!recent_metadata_.empty()) {
    auto callback = std::move(recent_metadata_.front().frame_encoded_callback);
    recent_metadata_.pop();
    if (callback) {
      std::move(callback).Run(nullptr);
    }
  }

  if (encoder_is_overridden_for_testing_) {
    // Don't construct a new encoder if it is overridden for testing.
  } else if (is_hardware_encoder_) {
    CHECK(gpu_factories_);
    SetEncoder(CreateHardwareEncoder(
        *gpu_factories_,
        cast_environment_->GetTaskRunner(CastEnvironment::ThreadId::kMain)));
  } else {
    SetEncoder(CreateSoftwareEncoder(codec_));
  }
  CHECK(encoder_);

  const VideoCodecProfile profile = encoding_support::ToProfile(codec_);
  metrics_provider_->Initialize(profile, options_.frame_size,
                                is_hardware_encoder_);

  CallEncoderOnCorrectThread(base::BindOnce(
      &CallInitializeEncoder, std::ref(*encoder_), profile, options_,
      CreateCallback(&MediaVideoEncoderWrapper::OnEncoderInfo,
                     encoder_version_),
      CreateCallback(&MediaVideoEncoderWrapper::OnEncodedFrame,
                     encoder_version_),
      CreateCallback(&MediaVideoEncoderWrapper::OnEncoderStatus,
                     encoder_version_)));
}

void MediaVideoEncoderWrapper::SetEncoder(
    std::unique_ptr<media::VideoEncoder> encoder) {
  // This unusual syntax unwraps `encoder`, which has a default deleter, and
  // transfers ownership to `encoder_`, which has a base::OnTaskRunnerDeleter.
  encoder_.reset(encoder.release());
}

base::TimeDelta MediaVideoEncoderWrapper::GetFrameDuration(
    const VideoFrame& frame) {
  // Frame has duration in metadata, use it.
  if (frame.metadata().frame_duration.has_value()) {
    return frame.metadata().frame_duration.value();
  }

  // No real way to figure out duration, use time passed since the last frame
  // as an educated guess, but clamp it within reasonable limits.
  constexpr auto min_duration = base::Seconds(1.0 / 60.0);
  constexpr auto max_duration = base::Seconds(1.0 / 24.0);
  const base::TimeDelta duration =
      frame.timestamp() - last_frame_timestamp_.value_or(base::TimeDelta{});
  return std::clamp(duration, min_duration, max_duration);
}

// The VideoEncoder API requires you to Flush() and wait before calling
// ChangeOptions(). This results in this annoying set of nested callbacks.
void MediaVideoEncoderWrapper::UpdateEncoderOptions() {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  num_pending_updates_++;

  auto flush_done_callback =
      CreateCallback(&MediaVideoEncoderWrapper::OnFlushDoneForOptionsUpdate,
                     encoder_version_, options_);
  CallEncoderOnCorrectThread(base::BindOnce(&CallFlush, std::ref(*encoder_),
                                            std::move(flush_done_callback)));
}

void MediaVideoEncoderWrapper::OnFlushDoneForOptionsUpdate(
    int encoder_version,
    media::VideoEncoder::Options options,
    EncoderStatus status) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  if (encoder_version != encoder_version_) {
    return;
  }

  if (!status.is_ok()) {
    OnOptionsUpdated(encoder_version, status);
    return;
  }

  CallEncoderOnCorrectThread(base::BindOnce(
      &CallChangeOptions, std::ref(*encoder_), std::move(options),
      CreateCallback(&MediaVideoEncoderWrapper::OnEncodedFrame,
                     encoder_version),
      CreateCallback(&MediaVideoEncoderWrapper::OnOptionsUpdated,
                     encoder_version)));
}

void MediaVideoEncoderWrapper::CallEncoderOnCorrectThread(
    base::OnceClosure closure) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));

  // If hardware, let the encoder do the post to the accelerator / VIDEO thread
  // on its own.
  if (is_hardware_encoder_) {
    std::move(closure).Run();

    // If software, manually post it to the VIDEO thread to not block MAIN.
  } else {
    cast_environment_->PostTask(CastEnvironment::ThreadId::kVideo, FROM_HERE,
                                std::move(closure));
  }
}

void MediaVideoEncoderWrapper::OnFrameEncodeDone(int encoder_version,
                                                 base::TimeTicks reference_time,
                                                 EncoderStatus status) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  if (encoder_version != encoder_version_) {
    return;
  }
  // An "OK" status is a no-op: the frame is handled in OnEncodedFrame().
  if (status.is_ok()) {
    return;
  }

  // Otherwise notify the VideoSender that we had a failed encode (so that it
  // can appropriately update the duration in flight).
  CachedMetadata& metadata = recent_metadata_.front();
  CHECK(metadata.reference_time == reference_time);
  auto callback = std::move(metadata.frame_encoded_callback);
  recent_metadata_.pop();

  std::move(callback).Run(nullptr);

  OnEncoderStatus(encoder_version, status);
}

void MediaVideoEncoderWrapper::OnOptionsUpdated(int encoder_version,
                                                EncoderStatus status) {
  CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain));
  if (encoder_version != encoder_version_) {
    return;
  }
  --num_pending_updates_;

  if (num_pending_updates_ == 0) {
    for (auto& task : pending_encodes_) {
      CallEncoderOnCorrectThread(std::move(task));
    }
    pending_encodes_.clear();
  }

  OnEncoderStatus(encoder_version, status);
}

}  //  namespace media::cast
