blob: 5a9dc7a821108c71b501750e1b2a9c78856a92d5 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// 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 <utility>
#include "base/bind.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/threading/thread_task_runner_handle.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_task_traits.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/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/native_widget_types.h"
#if !defined(OS_ANDROID)
#include "content/browser/media/capture/mouse_cursor_overlay_controller.h"
#endif
namespace content {
namespace {
// Note on lifetime: this context should be deleted when the WebContents
// is destroyed.
class WebContentsContext : public WebContentsFrameTracker::Context {
public:
explicit WebContentsContext(WebContents* contents) : contents_(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.
display::ScreenInfo info;
view->GetScreenInfo(&info);
return info.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(); }
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.
WebContents* contents_;
};
} // namespace
WebContentsFrameTracker::WebContentsFrameTracker(
base::WeakPtr<WebContentsVideoCaptureDevice> device,
MouseCursorOverlayController* cursor_controller)
: device_(std::move(device)),
device_task_runner_(base::ThreadTaskRunnerHandle::Get()) {
DCHECK(device_task_runner_);
#if !defined(OS_ANDROID)
cursor_controller_ = cursor_controller;
DCHECK(cursor_controller_);
#endif
}
WebContentsFrameTracker::~WebContentsFrameTracker() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (is_capturing_) {
DidStopCapturingWebContents();
}
}
void WebContentsFrameTracker::WillStartCapturingWebContents(
const gfx::Size& capture_size) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(!is_capturing_);
if (!web_contents()) {
return;
}
const gfx::Size preferred_size = CalculatePreferredSize(capture_size);
context_->IncrementCapturerCount(preferred_size);
is_capturing_ = true;
}
void WebContentsFrameTracker::DidStopCapturingWebContents() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (web_contents()) {
DCHECK(is_capturing_);
context_->DecrementCapturerCount();
is_capturing_ = false;
}
DCHECK(!is_capturing_);
}
// 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) {
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));
}
}
}
return preferred_size;
}
void WebContentsFrameTracker::RenderFrameCreated(
RenderFrameHost* render_frame_host) {
OnPossibleTargetChange();
}
void WebContentsFrameTracker::RenderFrameDeleted(
RenderFrameHost* render_frame_host) {
OnPossibleTargetChange();
}
void WebContentsFrameTracker::RenderFrameHostChanged(
RenderFrameHost* old_host,
RenderFrameHost* new_host) {
OnPossibleTargetChange();
}
void WebContentsFrameTracker::WebContentsDestroyed() {
is_capturing_ = false;
Observe(nullptr);
OnPossibleTargetChange();
}
void WebContentsFrameTracker::CaptureTargetChanged() {
OnPossibleTargetChange();
}
void WebContentsFrameTracker::SetWebContentsAndContextFromRoutingId(
const GlobalRenderFrameHostId& id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
Observe(WebContents::FromRenderFrameHost(RenderFrameHost::FromID(id)));
context_ = std::make_unique<WebContentsContext>(web_contents());
OnPossibleTargetChange();
}
void WebContentsFrameTracker::SetWebContentsAndContextForTesting(
WebContents* web_contents,
std::unique_ptr<WebContentsFrameTracker::Context> context) {
Observe(web_contents);
context_ = std::move(context);
OnPossibleTargetChange();
}
void WebContentsFrameTracker::OnPossibleTargetChange() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!web_contents()) {
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();
}
if (frame_sink_id != target_frame_sink_id_) {
target_frame_sink_id_ = frame_sink_id;
device_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
&WebContentsVideoCaptureDevice::OnTargetChanged, device_,
FrameSinkVideoCaptureDevice::VideoCaptureTarget{frame_sink_id}));
}
SetTargetView(web_contents()->GetNativeView());
}
// Note: MouseCursorOverlayController runs on the UI thread. It's also
// important that SetTargetView() be called in the current stack while
// |view| is known to be a valid pointer. http://crbug.com/818679
void WebContentsFrameTracker::SetTargetView(gfx::NativeView view) {
if (view == target_native_view_)
return;
target_native_view_ = view;
#if !defined(OS_ANDROID)
cursor_controller_->SetTargetView(view);
#endif
}
} // namespace content