blob: 625759bb34357f856b9b3293ebe484013092025b [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/feature_list.h"
#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 "components/viz/common/surfaces/video_capture_target.h"
#include "content/browser/gpu/gpu_data_manager_impl.h"
#include "content/browser/media/capture/web_contents_auto_scaler.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/video_util.h"
#include "media/capture/mojom/video_capture_types.mojom.h"
#include "media/capture/video_capture_types.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_ui_types.h"
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
#include "content/browser/media/capture/mouse_cursor_overlay_controller.h"
#endif
namespace content {
namespace {
void SetScaleOverrideForCapture(RenderFrameHost* rfh, float scale_override) {
if (rfh) {
// Inside content, down-casting from the public interface class is safe.
auto* view = static_cast<RenderWidgetHostViewBase*>(rfh->GetView());
if (view) {
view->SetScaleOverrideForCapture(scale_override);
}
}
}
} // namespace
// 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.
std::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 std::nullopt;
}
WebContentsImpl::CaptureTarget GetCaptureTarget() override {
return static_cast<WebContentsImpl*>(contents_)->GetCaptureTarget();
}
void IncrementCapturerCount(const gfx::Size& capture_size) override {
capture_handle_ = contents_->IncrementCapturerCount(
capture_size, /*stay_hidden=*/false,
/*stay_awake=*/true, /*is_activity=*/true);
}
void DecrementCapturerCount() override { capture_handle_.RunAndReset(); }
private:
// WebContentsAutoScaler::Delegate:
void SetCaptureScaleOverride(float scale) override {
if (auto* view = GetCurrentView()) {
view->SetScaleOverrideForCapture(scale);
}
}
float GetCaptureScaleOverride() 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;
}
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_;
};
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))
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
,
cursor_controller_(cursor_controller->GetWeakPtr())
#endif
{
// 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_);
CHECK(device_task_runner_);
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
CHECK(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;
if (is_high_dpi_enabled &&
!GpuDataManagerImpl::GetInstance()->IsGpuCompositingDisabled()) {
auto_scaler_ =
std::make_unique<WebContentsAutoScaler>(*context_, capture_size);
}
context_->IncrementCapturerCount(CalculatePreferredSize(capture_size));
is_capturing_ = true;
}
void WebContentsFrameTracker::DidStopCapturingWebContents() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (web_contents()) {
DCHECK(is_capturing_);
context_->DecrementCapturerCount();
is_capturing_ = false;
if (auto_scaler_) {
UMA_HISTOGRAM_COUNTS_1000("Media.VideoCapture.ScaleOverrideChangeCount",
auto_scaler_->GetScaleOverrideChangeCount());
context_->SetCaptureScaleOverride(1.0f);
auto_scaler_.reset();
}
}
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 (auto_scaler_) {
auto_scaler_->SetCapturedContentSize(content_size);
}
}
// 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 std::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;
}
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);
if (auto_scaler_) {
auto_scaler_->OnUtilizationReport(std::move(feedback));
}
}
void WebContentsFrameTracker::RenderFrameCreated(
RenderFrameHost* render_frame_host) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
OnPossibleTargetChange();
if (auto_scaler_) {
SetScaleOverrideForCapture(render_frame_host,
auto_scaler_->GetDesiredCaptureScaleOverride());
}
}
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();
SetScaleOverrideForCapture(old_host, 1.0f);
if (auto_scaler_) {
SetScaleOverrideForCapture(new_host,
auto_scaler_->GetDesiredCaptureScaleOverride());
}
}
void WebContentsFrameTracker::WebContentsDestroyed() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
is_capturing_ = false;
auto_scaler_.reset();
context_.reset();
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::ApplySubCaptureTarget(
media::mojom::SubCaptureTargetType type,
const base::Token& target_token,
uint32_t sub_capture_target_version,
base::OnceCallback<void(media::mojom::ApplySubCaptureTargetResult)>
callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(callback);
if (sub_capture_target_version_ >= sub_capture_target_version) {
std::move(callback).Run(
media::mojom::ApplySubCaptureTargetResult::kNonIncreasingVersion);
return;
}
sub_capture_target_ =
target_token.is_zero()
? std::nullopt
: std::make_optional<SubCaptureTargetInfo>(type, target_token);
sub_capture_target_version_ = sub_capture_target_version;
// If we don't have a target yet, we can store the sub-capture target,
// but cannot actually apply it yet.
if (!target_frame_sink_id_.is_valid()) {
return;
}
const viz::VideoCaptureTarget target(target_frame_sink_id_,
DeriveSubTarget());
device_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
[](const viz::VideoCaptureTarget& target,
uint32_t sub_capture_target_version,
base::OnceCallback<void(media::mojom::ApplySubCaptureTargetResult)>
callback,
base::WeakPtr<WebContentsVideoCaptureDevice> device) {
if (!device) {
std::move(callback).Run(
media::mojom::ApplySubCaptureTargetResult::kErrorGeneric);
return;
}
device->OnTargetChanged(target, sub_capture_target_version);
std::move(callback).Run(
media::mojom::ApplySubCaptureTargetResult::kSuccess);
},
target, sub_capture_target_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;
}
const WebContentsImpl::CaptureTarget capture_target =
context_ ? context_->GetCaptureTarget()
: WebContentsImpl::CaptureTarget{};
// TODO(crbug.com/40203554): Clear |sub_capture_target_| when
// share-this-tab-instead is clicked.
if (capture_target.sink_id != target_frame_sink_id_) {
target_frame_sink_id_ = capture_target.sink_id;
std::optional<viz::VideoCaptureTarget> target;
if (capture_target.sink_id.is_valid()) {
target =
viz::VideoCaptureTarget(capture_target.sink_id, DeriveSubTarget());
}
// 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), sub_capture_target_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(capture_target.view);
}
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) && !BUILDFLAG(IS_IOS)
if (cursor_controller_) {
cursor_controller_->SetTargetView(view);
}
#endif
}
viz::VideoCaptureSubTarget WebContentsFrameTracker::DeriveSubTarget() const {
if (!sub_capture_target_.has_value()) {
return base::Token();
}
const SubCaptureTargetInfo& sub_capture_target = sub_capture_target_.value();
switch (sub_capture_target.type) {
case media::mojom::SubCaptureTargetType::kCropTarget:
return sub_capture_target.token;
case media::mojom::SubCaptureTargetType::kRestrictionTarget:
return viz::SubtreeCaptureId(sub_capture_target.token);
}
NOTREACHED();
}
} // namespace content