// 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 "content/browser/media/capture/web_contents_auto_scaler.h"

#include <algorithm>
#include <utility>

#include "base/check.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/trace_event/trace_event.h"
#include "media/base/video_util.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/vector2d_f.h"

namespace content {

namespace {
// A minimum factor of 1.0 means that no DPI scaling is applied.
static constexpr float kMinCaptureScaleOverride = 1.0;
}  // namespace

WebContentsAutoScaler::Delegate::Delegate() = default;
WebContentsAutoScaler::Delegate::~Delegate() = default;

WebContentsAutoScaler::WebContentsAutoScaler(Delegate& delegate,
                                             const gfx::Size& capture_size)
    : delegate_(delegate), capture_size_(capture_size) {}
WebContentsAutoScaler::~WebContentsAutoScaler() = default;

void WebContentsAutoScaler::SetCapturedContentSize(
    const gfx::Size& content_size) {
  // Now that we have a new content size, reset some related values.
  content_size_ = content_size;
  max_capture_scale_override_ = kMaxCaptureScaleOverride;

  // The unscaled content size can be determined by removing the scale factor
  // from the |content_size|.
  const float scale_override = delegate_->GetCaptureScaleOverride();
  DCHECK_NE(0.0f, scale_override);
  const gfx::Size unscaled_content_size =
      gfx::ScaleToCeiledSize(content_size, 1.0f / scale_override);

  // Check if the capture scale needs to be modified. The content_size
  // provided here is the final pixel size, with all scale factors such as the
  // device scale factor and HiDPI capture scale already applied.
  //
  // The initial content_size received here corresponds to the size of the
  // browser tab. If region capture is active, there will be an additional
  // call providing the region size. Lastly, if the scale was modified, there
  // will be another call with the upscaled size.
  const float factor =
      CalculatePreferredScaleFactor(content_size, unscaled_content_size);
  SetCaptureScaleOverride(factor);
}

void WebContentsAutoScaler::OnUtilizationReport(
    media::VideoCaptureFeedback feedback) {
  capture_feedback_ = std::move(feedback);
  // Does not actually update the desired value (which is based on the content
  // size and capture size), but may lower the current scale factor by capping
  // it to a lower `max_capture_scale_override_` after processing the feedback.
  SetCaptureScaleOverride(desired_capture_scale_override_);
}

float WebContentsAutoScaler::GetDesiredCaptureScaleOverride() const {
  return desired_capture_scale_override_;
}

int WebContentsAutoScaler::GetScaleOverrideChangeCount() const {
  return scale_override_change_count_;
}

void WebContentsAutoScaler::SetCaptureScaleOverride(float new_value) {
  // First, record the desired value for future lookup.
  desired_capture_scale_override_ = new_value;

  // Then, if the value adjusted by max is not the same as the current value,
  // apply it to the context.
  const float current_value = delegate_->GetCaptureScaleOverride();
  const float bounded_value = std::min(new_value, DetermineMaxScaleOverride());
  if (bounded_value != current_value) {
    delegate_->SetCaptureScaleOverride(bounded_value);

    ++scale_override_change_count_;
    UMA_HISTOGRAM_CUSTOM_COUNTS("Media.VideoCapture.ScaleOverride",
                                new_value * 100, kMinCaptureScaleOverride * 100,
                                kMaxCaptureScaleOverride * 100 + 1, 50);
  }
}

float WebContentsAutoScaler::CalculatePreferredScaleFactor(
    const gfx::Size& current_content_size,
    const gfx::Size& unscaled_current_content_size) {
  // The content size does not include letterboxing, meaning that there may
  // be an aspect ratio difference between the content size and the final
  // capture size. For example, if the video frame consumer requests a 1080P
  // video stream and the web contents has a size of 960x720 (ratio of 4:3), the
  // letterboxed size here will be 1440x1080 (still 4:3). Graphically:
  //
  //    |capture_size_|
  //    |----------------------------------------------------|
  //    |    | |letterbox_size|                         | .  |
  //    |    |     |-------------------------------|    |    |
  //    |    |     | |content_size|                |    |    |
  //    |    |     |-------------------------------|    |    |
  //    |    |                                          |    |
  //    |----------------------------------------------------|
  //
  // In order to preserve the aspect ratio of the web contents, we use this
  // letterboxed size with the same aspect ratio instead of the requested
  // capture size's aspect ratio.
  gfx::Size letterbox_size =
      media::ComputeLetterboxRegion(gfx::Rect(capture_size_),
                                    unscaled_current_content_size)
          .size();

  // Ideally the |current_content_size| should be the same as |letterbox_size|,
  // so if we are achieving that with current settings we can exit early. Since
  // we only scale by factors of 1/4, we accept a difference here of up to 1/8th
  // of the letterboxed size, meaning that this scale factor would have been a
  // more appropriate fit that a neighboring factor.
  if (std::abs(current_content_size.width() - letterbox_size.width()) <=
          (letterbox_size.width() / 8) &&
      std::abs(current_content_size.height() - letterbox_size.height()) <=
          (letterbox_size.height() / 8)) {
    return desired_capture_scale_override_;
  }

  // Next, determine what the ideal scale factors in each direction would have
  // been for this frame. Since we are using the letterboxed size here, the
  // factors should be almost identical.
  DCHECK_NE(0.0f, unscaled_current_content_size.width());
  DCHECK_NE(0.0f, unscaled_current_content_size.height());
  const gfx::Vector2dF factors(static_cast<float>(letterbox_size.width()) /
                                   unscaled_current_content_size.width(),
                               static_cast<float>(letterbox_size.height()) /
                                   unscaled_current_content_size.height());

  // We prefer to err on the side of having to downscale in one direction rather
  // than upscale in the other direction, so we use the largest scale factor.
  const float largest_factor = std::max(factors.x(), factors.y());

  // Finally, we return a value bounded by [kMinCaptureScaleOverride,
  // kMaxCaptureScaleOverride] rounded to the nearest quarter.
  const float preferred_factor =
      std::clamp(std::round(largest_factor * 4) / 4, kMinCaptureScaleOverride,
                 kMaxCaptureScaleOverride);

  DVLOG(3) << __func__ << ":" << " capture_size_=" << capture_size_.ToString()
           << ", letterbox_size=" << letterbox_size.ToString()
           << ", current_content_size=" << current_content_size.ToString()
           << ", unscaled_current_content_size="
           << unscaled_current_content_size.ToString()
           << ", factors.x()=" << factors.x() << " factors.y()=" << factors.y()
           << ", largest_factor=" << largest_factor
           << ", preferred factor=" << preferred_factor;
  return preferred_factor;
}

float WebContentsAutoScaler::DetermineMaxScaleOverride() {
  // If we have no feedback or don't want to apply a scale factor, leave it
  // unchanged.
  if (!capture_feedback_ || !content_size_) {
    return max_capture_scale_override_;
  }

  // First, determine if we need to lower the max scale override.
  // Clue 1: we are above 80% resource utilization.
  bool should_decrease_override =
      capture_feedback_->resource_utilization > 0.8f;

  // Clue 2: we are using too many pixels.
  if (content_size_) {
    should_decrease_override |=
        content_size_->width() * content_size_->height() >
        capture_feedback_->max_pixels;
  }

  if (should_decrease_override) {
    max_capture_scale_override_ =
        std::max(kMinCaptureScaleOverride, max_capture_scale_override_ - 0.25f);
  }

  // Second, determine if conditions have gotten better to the point where
  // we can increase the maximum scale override.
  if (!should_decrease_override &&
      max_capture_scale_override_ < kMaxCaptureScaleOverride) {
    // Clue A: using less than 40% of resources.
    bool should_increase_override =
        capture_feedback_->resource_utilization < 0.5f;

    // Clue B: we are ALSO significantly below the max pixels.
    should_increase_override &=
        content_size_->width() * content_size_->height() <
        capture_feedback_->max_pixels * 0.8;

    if (should_increase_override) {
      max_capture_scale_override_ = std::min(
          kMaxCaptureScaleOverride, max_capture_scale_override_ + 0.25f);
    }
  }

  TRACE_EVENT_INSTANT(
      "gpu.capture", "WebContentsAutoScaler::DetermineMaxScaleOverride",
      "max_scale_override", max_capture_scale_override_, "constraints",
      base::StrCat(
          {"max_pixels=", base::NumberToString(capture_feedback_->max_pixels),
           ", utilization=",
           base::NumberToString(capture_feedback_->resource_utilization)}));
  return max_capture_scale_override_;
}

// A max factor above 2.0 would cause a quality degradation for local
// rendering. The downscaling used by the compositor uses a linear filter
// which only looks at 4 source pixels, so rendering more than 4 pixels per
// destination pixel would result in information loss.
// static
const float WebContentsAutoScaler::kMaxCaptureScaleOverride = 2.0f;

}  // namespace content
