blob: e33bb547fcf31fc9b4917f82e6746c413486aaa4 [file] [log] [blame]
// Copyright 2021 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_frame_tracker.h"
#include <algorithm>
#include <utility>
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "content/browser/media/capture/web_contents_video_capture_device.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_media_capture_id.h"
#include "content/public/browser/web_contents_observer.h"
#include "media/base/media_switches.h"
#include "media/base/video_util.h"
#include "media/capture/video_capture_types.h"
#include "ui/base/layout.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/size_conversions.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/gfx/native_widget_types.h"
#if !BUILDFLAG(IS_ANDROID)
#include "content/browser/media/capture/mouse_cursor_overlay_controller.h"
#endif
namespace content {
namespace {
// A minimum factor of 1.0 means that no DPI scaling is applied.
static constexpr float kMinCaptureScaleOverride = 1.0;
// Note on lifetime: this context is deleted via WebContentsObserver's
// WebContentsDestroyed() method when the WebContents is destroyed.
class WebContentsContext : public WebContentsFrameTracker::Context {
public:
explicit WebContentsContext(WebContents* contents) : contents_(contents) {
DCHECK(contents_);
}
~WebContentsContext() override = default;
// WebContextFrameTracker::Context overrides.
absl::optional<gfx::Rect> GetScreenBounds() override {
if (auto* view = GetCurrentView()) {
// If we know the available size of the screen, we don't want to exceed
// it as it may result in strange capture behavior in some cases.
return view->GetScreenInfo().rect;
}
return absl::nullopt;
}
viz::FrameSinkId GetFrameSinkIdForCapture() override {
return static_cast<WebContentsImpl*>(contents_)->GetCaptureFrameSinkId();
}
void IncrementCapturerCount(const gfx::Size& capture_size) override {
capture_handle_ =
contents_->IncrementCapturerCount(capture_size, /*stay_hidden=*/false,
/*stay_awake=*/true);
}
void DecrementCapturerCount() override { capture_handle_.RunAndReset(); }
void SetScaleOverrideForCapture(float scale) override {
if (auto* view = GetCurrentView()) {
view->SetScaleOverrideForCapture(scale);
}
}
float GetScaleOverrideForCapture() const override {
if (const auto* view = GetCurrentView()) {
return view->GetScaleOverrideForCapture();
}
// Otherwise we can assume it's unset and return the default value.
return 1.0f;
}
private:
RenderWidgetHostViewBase* GetCurrentView() const {
RenderWidgetHostView* view = contents_->GetRenderWidgetHostView();
// Make sure the RWHV is still associated with a RWH before considering the
// view "alive." This is because a null RWH indicates the RWHV has had its
// Destroy() method called.
if (!view || !view->GetRenderWidgetHost()) {
return nullptr;
}
// Inside content, down-casting from the public interface class is safe.
return static_cast<RenderWidgetHostViewBase*>(view);
}
base::ScopedClosureRunner capture_handle_;
// The backing WebContents.
raw_ptr<WebContents, DanglingUntriaged> contents_;
};
} // namespace
// 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 WebContentsFrameTracker::kMaxCaptureScaleOverride = 2.0f;
WebContentsFrameTracker::WebContentsFrameTracker(
scoped_refptr<base::SequencedTaskRunner> device_task_runner,
base::WeakPtr<WebContentsVideoCaptureDevice> device,
MouseCursorOverlayController* cursor_controller)
: device_(std::move(device)),
device_task_runner_(std::move(device_task_runner)) {
// Verify on construction that this object is created on the UI thread. After
// this, depend on the sequence checker to ensure consistent execution.
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(device_task_runner_);
#if !BUILDFLAG(IS_ANDROID)
cursor_controller_ = cursor_controller;
DCHECK(cursor_controller_);
#endif
}
WebContentsFrameTracker::~WebContentsFrameTracker() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (is_capturing_) {
DidStopCapturingWebContents();
}
}
void WebContentsFrameTracker::WillStartCapturingWebContents(
const gfx::Size& capture_size,
bool is_high_dpi_enabled) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!is_capturing_);
if (!web_contents()) {
return;
}
capture_size_ = capture_size;
is_high_dpi_enabled_ = is_high_dpi_enabled;
context_->IncrementCapturerCount(CalculatePreferredSize(capture_size));
is_capturing_ = true;
}
void WebContentsFrameTracker::DidStopCapturingWebContents() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (web_contents()) {
SetCaptureScaleOverride(1.0f);
DCHECK(is_capturing_);
context_->DecrementCapturerCount();
is_capturing_ = false;
UMA_HISTOGRAM_COUNTS_1000("Media.VideoCapture.ScaleOverrideChangeCount",
scale_override_change_count_);
scale_override_change_count_ = 0;
}
DCHECK(!is_capturing_);
}
void WebContentsFrameTracker::SetCapturedContentSize(
const gfx::Size& content_size) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!web_contents())
return;
// For efficiency, this function should only be called when the captured
// content size changes. The caller is responsible for enforcing that.
TRACE_EVENT_INSTANT1(
"gpu.capture", "WebContentsFrameTracker::SetCapturedContentSize",
TRACE_EVENT_SCOPE_THREAD, "content_size", content_size.ToString());
if (base::FeatureList::IsEnabled(media::kWebContentsCaptureHiDpi) &&
is_high_dpi_enabled_) {
// 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 = context_->GetScaleOverrideForCapture();
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);
}
}
// We provide the WebContents with a preferred size override during its capture.
// The preferred size is a strong suggestion to UI layout code to size the view
// such that its physical rendering size matches the exact capture size. This
// helps to eliminate redundant scaling operations during capture. Note that if
// there are multiple capturers, a "first past the post" system is used and
// the first capturer's preferred size is set.
gfx::Size WebContentsFrameTracker::CalculatePreferredSize(
const gfx::Size& capture_size) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (capture_size.IsEmpty()) {
// NOTE: An empty preferred size will cause the WebContents to keep its
// previous size preference.
return {};
}
gfx::Size preferred_size = capture_size;
// If we know the available size of the screen, we don't want to exceed
// it as it may result in strange capture behavior in some cases.
if (context_) {
const absl::optional<gfx::Rect> screen_bounds = context_->GetScreenBounds();
if (screen_bounds) {
if (screen_bounds->size().IsEmpty()) {
return {};
}
// We want to honor the aspect ratio of the capture size request while
// also limiting it to the screen bounds of the view.
// For motivation, see https://crbug.com/1194803.
const double x_ratio = static_cast<double>(capture_size.width()) /
static_cast<double>(screen_bounds->size().width());
const double y_ratio =
static_cast<double>(capture_size.height()) /
static_cast<double>(screen_bounds->size().height());
const double scale_ratio = std::max(x_ratio, y_ratio);
if (scale_ratio > 1.0) {
preferred_size = gfx::ScaleToFlooredSize(
preferred_size, static_cast<float>(1 / scale_ratio));
}
DVLOG(3) << __func__ << ": x_ratio=" << x_ratio << " y_ratio=" << y_ratio
<< " scale_ratio=" << scale_ratio
<< " preferred_size=" << preferred_size.ToString();
}
}
return preferred_size;
}
float WebContentsFrameTracker::CalculatePreferredScaleFactor(
const gfx::Size& current_content_size,
const gfx::Size& unscaled_current_content_size) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(is_high_dpi_enabled_);
// 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;
}
void WebContentsFrameTracker::OnUtilizationReport(
media::VideoCaptureFeedback feedback) {
TRACE_EVENT_INSTANT2(
"gpu.capture", "WebContentsFrameTracker::OnUtilizationReport",
TRACE_EVENT_SCOPE_THREAD, "utilization", feedback.resource_utilization,
"max_pixels", feedback.max_pixels);
capture_feedback_ = std::move(feedback);
// We may not be associated with a web contents when we get a report, but
// it is still valid and potentially interesting.
if (context_) {
SetCaptureScaleOverride(desired_capture_scale_override_);
}
}
void WebContentsFrameTracker::RenderFrameCreated(
RenderFrameHost* render_frame_host) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
OnPossibleTargetChange();
if (desired_capture_scale_override_ != 1.0f) {
if (auto* view = render_frame_host->GetView()) {
// Inside content, down-casting from the public interface class is safe.
static_cast<RenderWidgetHostViewBase*>(view)->SetScaleOverrideForCapture(
desired_capture_scale_override_);
}
}
}
void WebContentsFrameTracker::RenderFrameDeleted(
RenderFrameHost* render_frame_host) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
OnPossibleTargetChange();
}
void WebContentsFrameTracker::RenderFrameHostChanged(
RenderFrameHost* old_host,
RenderFrameHost* new_host) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
OnPossibleTargetChange();
if (desired_capture_scale_override_ != 1.0f) {
// According to WebContentsObserver docs, old_host can be nullptr.
if (old_host) {
if (auto* old_view = old_host->GetView()) {
// Inside content, down-casting from the public interface class is safe.
static_cast<RenderWidgetHostViewBase*>(old_view)
->SetScaleOverrideForCapture(1.0f);
}
}
if (auto* new_view = new_host->GetView()) {
static_cast<RenderWidgetHostViewBase*>(new_view)
->SetScaleOverrideForCapture(desired_capture_scale_override_);
}
}
}
void WebContentsFrameTracker::WebContentsDestroyed() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
is_capturing_ = false;
context_ = nullptr;
Observe(nullptr);
OnPossibleTargetChange();
}
void WebContentsFrameTracker::CaptureTargetChanged() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
OnPossibleTargetChange();
}
void WebContentsFrameTracker::SetWebContentsAndContextFromRoutingId(
const GlobalRenderFrameHostId& id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Observe(WebContents::FromRenderFrameHost(RenderFrameHost::FromID(id)));
if (web_contents()) {
// If the routing ID was invalid, don't set up a context.
context_ = std::make_unique<WebContentsContext>(web_contents());
}
OnPossibleTargetChange();
}
void WebContentsFrameTracker::Crop(
const base::Token& crop_id,
uint32_t crop_version,
base::OnceCallback<void(media::mojom::CropRequestResult)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(callback);
if (crop_version_ >= crop_version) {
// This will trigger a BadMessage from MediaStreamDispatcherHost.
// (MediaStreamDispatcherHost knows the capturer, whereas here we know
// the capturee.)
std::move(callback).Run(
media::mojom::CropRequestResult::kNonIncreasingCropVersion);
return;
}
crop_id_ = crop_id;
crop_version_ = crop_version;
// If we don't have a target yet, we can store the crop ID but cannot actually
// crop yet.
if (!target_frame_sink_id_.is_valid())
return;
const viz::VideoCaptureTarget target(target_frame_sink_id_, crop_id_);
device_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
[](const viz::VideoCaptureTarget& target, uint32_t crop_version,
base::OnceCallback<void(media::mojom::CropRequestResult)> callback,
base::WeakPtr<WebContentsVideoCaptureDevice> device) {
if (!device) {
std::move(callback).Run(
media::mojom::CropRequestResult::kErrorGeneric);
return;
}
device->OnTargetChanged(target, crop_version);
std::move(callback).Run(media::mojom::CropRequestResult::kSuccess);
},
target, crop_version_, std::move(callback), device_));
}
void WebContentsFrameTracker::SetWebContentsAndContextForTesting(
WebContents* web_contents,
std::unique_ptr<WebContentsFrameTracker::Context> context) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Observe(web_contents);
context_ = std::move(context);
OnPossibleTargetChange();
}
void WebContentsFrameTracker::OnPossibleTargetChange() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!web_contents()) {
DCHECK(!context_);
device_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&WebContentsVideoCaptureDevice::OnTargetPermanentlyLost,
device_));
SetTargetView({});
return;
}
viz::FrameSinkId frame_sink_id;
if (context_) {
frame_sink_id = context_->GetFrameSinkIdForCapture();
}
// TODO(crbug.com/1264849): Clear |crop_id_| when share-this-tab-instead
// is clicked.
if (frame_sink_id != target_frame_sink_id_) {
target_frame_sink_id_ = frame_sink_id;
absl::optional<viz::VideoCaptureTarget> target;
if (frame_sink_id.is_valid()) {
target = viz::VideoCaptureTarget(frame_sink_id, crop_id_);
}
// The target may change to an invalid one, but we don't consider it
// permanently lost here yet.
device_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&WebContentsVideoCaptureDevice::OnTargetChanged, device_,
std::move(target), crop_version_));
}
// Note: MouseCursorOverlayController runs on the UI thread. SetTargetView()
// must be called synchronously since the NativeView pointer is not valid
// across task switches, cf. https://crbug.com/818679
SetTargetView(web_contents()->GetNativeView());
}
void WebContentsFrameTracker::SetTargetView(gfx::NativeView view) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (view == target_native_view_)
return;
target_native_view_ = view;
#if !BUILDFLAG(IS_ANDROID)
cursor_controller_->SetTargetView(view);
#endif
}
void WebContentsFrameTracker::SetCaptureScaleOverride(float new_value) {
DCHECK(context_);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// 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 = context_->GetScaleOverrideForCapture();
const float bounded_value = std::min(new_value, DetermineMaxScaleOverride());
if (bounded_value != current_value) {
context_->SetScaleOverrideForCapture(bounded_value);
++scale_override_change_count_;
UMA_HISTOGRAM_CUSTOM_COUNTS("Media.VideoCapture.ScaleOverride",
new_value * 100, kMinCaptureScaleOverride * 100,
kMaxCaptureScaleOverride * 100 + 1, 50);
}
}
float WebContentsFrameTracker::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", "WebContentsFrameTracker::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_;
}
} // namespace content