blob: a27f2eca284e2c4ca87f18530e28e89b75e5a932 [file] [log] [blame]
// 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_INSTANT2(
"gpu.capture", "WebContentsAutoScaler::DetermineMaxScaleOverride",
TRACE_EVENT_SCOPE_THREAD, "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