blob: 87f06d4a1a571317d69966aa4754da6a05d3d5b4 [file] [log] [blame]
// Copyright 2023 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/captured_surface_controller.h"
#include <cmath>
#include "base/task/bind_post_task.h"
#include "content/browser/media/captured_surface_control_permission_manager.h"
#include "content/browser/media/media_stream_web_contents_observer.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/host_zoom_map.h"
#include "content/public/browser/web_contents_media_capture_id.h"
#include "third_party/blink/public/common/input/synthetic_web_input_event_builders.h"
#include "third_party/blink/public/common/page/page_zoom.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
#include "ui/events/types/scroll_types.h"
#include "ui/gfx/geometry/size.h"
namespace content {
namespace {
using ::blink::kPresetBrowserZoomFactors;
using ::blink::ZoomFactorToZoomLevel;
using ::blink::ZoomLevelToZoomFactor;
using ::blink::ZoomValuesEqual;
using ::blink::mojom::CapturedSurfaceControlResult;
using ::blink::mojom::ZoomLevelAction;
using PermissionManager = CapturedSurfaceControlPermissionManager;
using PermissionResult = PermissionManager::PermissionResult;
using CapturedSurfaceInfo = CapturedSurfaceController::CapturedSurfaceInfo;
void OnZoomLevelChangeOnUI(
base::RepeatingCallback<void(int)> on_zoom_level_change_callback,
base::WeakPtr<WebContents> captured_wc,
const HostZoomMap::ZoomLevelChange& change) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!captured_wc) {
return;
}
int zoom_level =
std::round(100 * ZoomLevelToZoomFactor(
HostZoomMap::GetZoomLevel(captured_wc.get())));
on_zoom_level_change_callback.Run(zoom_level);
}
std::optional<CapturedSurfaceInfo> ResolveCapturedSurfaceOnUI(
WebContentsMediaCaptureId wc_id,
int subscription_version,
base::RepeatingCallback<void(int)> on_zoom_level_change_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (wc_id.is_null()) {
return std::nullopt;
}
WebContents* const wc =
WebContents::FromRenderFrameHost(RenderFrameHost::FromID(
wc_id.render_process_id, wc_id.main_render_frame_id));
if (!wc) {
return std::nullopt;
}
HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(wc);
if (!host_zoom_map) {
return std::nullopt;
}
int initial_zoom_level =
std::round(100 * ZoomLevelToZoomFactor(HostZoomMap::GetZoomLevel(wc)));
std::unique_ptr<base::CallbackListSubscription,
BrowserThread::DeleteOnUIThread>
subscription_ptr(new base::CallbackListSubscription());
base::WeakPtr<WebContents> wc_weak_ptr = wc->GetWeakPtr();
*subscription_ptr =
host_zoom_map->AddZoomLevelChangedCallback(base::BindRepeating(
&OnZoomLevelChangeOnUI, on_zoom_level_change_callback, wc_weak_ptr));
return CapturedSurfaceInfo(wc_weak_ptr, std::move(subscription_ptr),
subscription_version, initial_zoom_level);
}
// Deliver a synthetic MouseWheel action on `captured_wc` with the parameters
// described by the values in `action`.
//
// Return `CapturedSurfaceControlResult` to be reported back to the renderer,
// indicating success or failure (with reason).
CapturedSurfaceControlResult DoSendWheel(
GlobalRenderFrameHostId capturer_rfh_id,
base::WeakPtr<WebContents> captured_wc,
blink::mojom::CapturedWheelActionPtr action) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
WebContentsImpl* const capturer_wci =
WebContentsImpl::FromRenderFrameHostImpl(
RenderFrameHostImpl::FromID(capturer_rfh_id));
if (!capturer_wci) {
// The capturing frame or tab appears to have closed asynchronously.
return CapturedSurfaceControlResult::kCapturerNotFoundError;
}
RenderFrameHost* const captured_rfh =
captured_wc ? captured_wc->GetPrimaryMainFrame() : nullptr;
if (!captured_rfh) {
return CapturedSurfaceControlResult::kCapturedSurfaceNotFoundError;
}
RenderFrameHostImpl* const captured_rfhi =
RenderFrameHostImpl::FromID(captured_rfh->GetGlobalId());
RenderWidgetHostImpl* const captured_rwhi =
captured_rfhi ? captured_rfhi->GetRenderWidgetHost() : nullptr;
if (!captured_rwhi) {
return CapturedSurfaceControlResult::kCapturedSurfaceNotFoundError;
}
if (capturer_wci == captured_wc.get()) {
return CapturedSurfaceControlResult::kDisallowedForSelfCaptureError;
}
// Scale (x, y).
const gfx::Size captured_viewport_size =
captured_rwhi->GetRenderInputRouter()->GetRootWidgetViewportSize();
if (captured_viewport_size.width() < 1 ||
captured_viewport_size.height() < 1) {
return CapturedSurfaceControlResult::kUnknownError;
}
const double x =
std::floor(action->relative_x * captured_viewport_size.width());
const double y =
std::floor(action->relative_y * captured_viewport_size.height());
// Clamp deltas.
// Note that `action->wheel_delta_x` and `action->wheel_delta_y` are
// `int32_t`s, but `blink::SyntheticWebMouseWheelEventBuilder::Build()`
// receives `float`s.
const float wheel_delta_x =
std::min(CapturedSurfaceController::kMaxWheelDeltaMagnitude,
std::max(action->wheel_delta_x,
-CapturedSurfaceController::kMaxWheelDeltaMagnitude));
const float wheel_delta_y =
std::min(CapturedSurfaceController::kMaxWheelDeltaMagnitude,
std::max(action->wheel_delta_y,
-CapturedSurfaceController::kMaxWheelDeltaMagnitude));
// Produce the wheel event on the captured surface.
{
blink::WebMouseWheelEvent event =
blink::SyntheticWebMouseWheelEventBuilder::Build(
x, y, wheel_delta_x, wheel_delta_y,
blink::WebInputEvent::kNoModifiers,
ui::ScrollGranularity::kScrollByPixel);
event.phase = blink::WebMouseWheelEvent::Phase::kPhaseBegan;
captured_rwhi->ForwardWheelEvent(event);
}
// Close the loop by producing an event at the same location with zero deltas
// and with the phase set to kPhaseEnded.
{
blink::WebMouseWheelEvent event =
blink::SyntheticWebMouseWheelEventBuilder::Build(
x, y, /*dx=*/0, /*dy=*/0, blink::WebInputEvent::kNoModifiers,
ui::ScrollGranularity::kScrollByPixel);
event.phase = blink::WebMouseWheelEvent::Phase::kPhaseEnded;
captured_rwhi->ForwardWheelEvent(event);
}
capturer_wci->DidCapturedSurfaceControl();
return CapturedSurfaceControlResult::kSuccess;
}
// We use this helper to check if a given zoom factor is within [closed, open),
// or within (open, closed], depending on which of the two is greater.
// We allow for some small epsilon in either direction using ZoomValuesEqual().
bool IsFactorWithinHalfOpenInterval(double val, double closed, double open) {
CHECK_NE(closed, open);
if (ZoomValuesEqual(val, closed)) {
return true;
}
const double min = std::min(closed, open);
const double max = std::max(closed, open);
return val > min && val < max && !ZoomValuesEqual(val, open);
}
base::expected<double, CapturedSurfaceControlResult> GetNewZoomLevel(
WebContents* wc,
ZoomLevelAction action) {
if (action == ZoomLevelAction::kReset) {
return ZoomFactorToZoomLevel(1);
}
CHECK(action == ZoomLevelAction::kIncrease ||
action == ZoomLevelAction::kDecrease);
const bool is_increase = (action == ZoomLevelAction::kIncrease);
const double factor = ZoomLevelToZoomFactor(HostZoomMap::GetZoomLevel(wc));
CHECK_GE(kPresetBrowserZoomFactors.size(), 2u);
for (size_t i = 1; i < kPresetBrowserZoomFactors.size(); ++i) {
const double prev = kPresetBrowserZoomFactors[i - 1];
const double next = kPresetBrowserZoomFactors[i];
if (is_increase) {
if (IsFactorWithinHalfOpenInterval(factor, prev, next)) {
return ZoomFactorToZoomLevel(next);
}
} else { // !is_increase
if (IsFactorWithinHalfOpenInterval(factor, next, prev)) {
return ZoomFactorToZoomLevel(prev);
}
}
}
return base::unexpected(action == ZoomLevelAction::kIncrease
? CapturedSurfaceControlResult::kMaxZoomLevel
: CapturedSurfaceControlResult::kMinZoomLevel);
}
// Update the zoom level of the tab indicated by `captured_wc`,
// either increasing, decreasing or resetting the zoom level,
// according to the desired change indicated by `action`.
//
// Return `CapturedSurfaceControlResult` to be reported back to the renderer,
// indicating success or failure (with reason).
CapturedSurfaceControlResult DoUpdateZoomLevel(
GlobalRenderFrameHostId capturer_rfh_id,
base::WeakPtr<WebContents> captured_wc,
ZoomLevelAction action) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
WebContentsImpl* const capturer_wci =
WebContentsImpl::FromRenderFrameHostImpl(
RenderFrameHostImpl::FromID(capturer_rfh_id));
if (!capturer_wci) {
// The capturing frame or tab appears to have closed asynchronously.
return CapturedSurfaceControlResult::kCapturerNotFoundError;
}
if (!captured_wc) {
return CapturedSurfaceControlResult::kCapturedSurfaceNotFoundError;
}
if (capturer_wci == captured_wc.get()) {
return CapturedSurfaceControlResult::kDisallowedForSelfCaptureError;
}
HostZoomMap* const host_zoom_map =
HostZoomMap::GetForWebContents(captured_wc.get());
if (!host_zoom_map) {
return CapturedSurfaceControlResult::kUnknownError;
}
const base::expected<double, CapturedSurfaceControlResult> new_zoom_level =
GetNewZoomLevel(captured_wc.get(), action);
if (!new_zoom_level.has_value()) {
return new_zoom_level.error();
}
host_zoom_map->SetTemporaryZoomLevel(
captured_wc->GetPrimaryMainFrame()->GetGlobalId(),
new_zoom_level.value());
capturer_wci->DidCapturedSurfaceControl();
return CapturedSurfaceControlResult::kSuccess;
}
// Return success if all conditions for CSC apply, otherwise fail with the
// appropriate error code.
CapturedSurfaceControlResult FinalizeRequestPermission(
GlobalRenderFrameHostId capturer_rfh_id,
base::WeakPtr<WebContents> captured_wc) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
WebContentsImpl* const capturer_wci =
WebContentsImpl::FromRenderFrameHostImpl(
RenderFrameHostImpl::FromID(capturer_rfh_id));
if (!capturer_wci) {
// The capturing frame or tab appears to have closed asynchronously.
return CapturedSurfaceControlResult::kCapturerNotFoundError;
}
RenderFrameHost* const captured_rfh =
captured_wc ? captured_wc->GetPrimaryMainFrame() : nullptr;
if (!captured_rfh) {
return CapturedSurfaceControlResult::kCapturedSurfaceNotFoundError;
}
RenderFrameHostImpl* const captured_rfhi =
RenderFrameHostImpl::FromID(captured_rfh->GetGlobalId());
RenderWidgetHostImpl* const captured_rwhi =
captured_rfhi ? captured_rfhi->GetRenderWidgetHost() : nullptr;
if (!captured_rwhi) {
return CapturedSurfaceControlResult::kCapturedSurfaceNotFoundError;
}
if (capturer_wci == captured_wc.get()) {
return CapturedSurfaceControlResult::kDisallowedForSelfCaptureError;
}
capturer_wci->DidCapturedSurfaceControl();
return CapturedSurfaceControlResult::kSuccess;
}
void OnPermissionCheckResult(
base::OnceCallback<CapturedSurfaceControlResult()> action_callback,
base::OnceCallback<void(CapturedSurfaceControlResult)> reply_callback,
PermissionResult permission_check_result) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (permission_check_result == PermissionResult::kDenied) {
std::move(reply_callback)
.Run(CapturedSurfaceControlResult::kNoPermissionError);
return;
}
if (permission_check_result == PermissionResult::kError) {
std::move(reply_callback).Run(CapturedSurfaceControlResult::kUnknownError);
return;
}
const CapturedSurfaceControlResult result = std::move(action_callback).Run();
std::move(reply_callback).Run(result);
}
// Given:
// 1. A callback that will attempt to perform an action if permitted.
// 2. A callback that will report to the renderer process whether the
// action succeeded, failed or was not permitted.
//
// Return:
// A callback that composes these two into a single callback that,
// after the permission manager has checked for permission, runs the
// action callback if it is permitted, and reports the result to the renderer.
//
// It is assumed that `action_callback` runs on the UI thread.
base::OnceCallback<void(PermissionResult)> ComposeCallbacks(
base::OnceCallback<CapturedSurfaceControlResult(void)> action_callback,
base::OnceCallback<void(CapturedSurfaceControlResult)> reply_callback) {
// Callback for reporting result of both permission-prompt as well as action
// (if permitted) to the renderer.
base::OnceCallback<void(CapturedSurfaceControlResult)> reply_callback_io =
base::BindPostTask(GetIOThreadTaskRunner({}), std::move(reply_callback));
return base::BindPostTask(
GetUIThreadTaskRunner({}),
base::BindOnce(&OnPermissionCheckResult, std::move(action_callback),
std::move(reply_callback_io)));
}
} // namespace
CapturedSurfaceInfo::CapturedSurfaceInfo(
base::WeakPtr<WebContents> captured_wc,
std::unique_ptr<base::CallbackListSubscription,
BrowserThread::DeleteOnUIThread> subscription,
int subscription_version,
int initial_zoom_level)
: captured_wc(captured_wc),
subscription(std::move(subscription)),
subscription_version(subscription_version),
initial_zoom_level(initial_zoom_level) {}
CapturedSurfaceInfo::CapturedSurfaceInfo(CapturedSurfaceInfo&& other) = default;
CapturedSurfaceInfo& CapturedSurfaceInfo::operator=(
CapturedSurfaceInfo&& other) = default;
CapturedSurfaceInfo::~CapturedSurfaceInfo() = default;
std::unique_ptr<CapturedSurfaceController>
CapturedSurfaceController::CreateForTesting(
GlobalRenderFrameHostId capturer_rfh_id,
WebContentsMediaCaptureId captured_wc_id,
std::unique_ptr<PermissionManager> permission_manager,
base::RepeatingCallback<void(int)> on_zoom_level_change_callback,
base::RepeatingCallback<void(base::WeakPtr<WebContents>)>
wc_resolution_callback) {
return base::WrapUnique(new CapturedSurfaceController(
capturer_rfh_id, captured_wc_id, std::move(permission_manager),
on_zoom_level_change_callback, std::move(wc_resolution_callback)));
}
CapturedSurfaceController::CapturedSurfaceController(
GlobalRenderFrameHostId capturer_rfh_id,
WebContentsMediaCaptureId captured_wc_id,
base::RepeatingCallback<void(int)> on_zoom_level_change_callback)
: CapturedSurfaceController(
capturer_rfh_id,
captured_wc_id,
std::make_unique<PermissionManager>(capturer_rfh_id),
std::move(on_zoom_level_change_callback),
/*wc_resolution_callback=*/base::DoNothing()) {}
CapturedSurfaceController::CapturedSurfaceController(
GlobalRenderFrameHostId capturer_rfh_id,
WebContentsMediaCaptureId captured_wc_id,
std::unique_ptr<PermissionManager> permission_manager,
base::RepeatingCallback<void(int)> on_zoom_level_change_callback,
base::RepeatingCallback<void(base::WeakPtr<WebContents>)>
wc_resolution_callback)
: capturer_rfh_id_(capturer_rfh_id),
permission_manager_(std::move(permission_manager)),
wc_resolution_callback_(std::move(wc_resolution_callback)),
on_zoom_level_change_callback_(std::move(on_zoom_level_change_callback)) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
ResolveCapturedSurface(captured_wc_id);
}
CapturedSurfaceController::~CapturedSurfaceController() = default;
void CapturedSurfaceController::UpdateCaptureTarget(
WebContentsMediaCaptureId captured_wc_id) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
ResolveCapturedSurface(captured_wc_id);
}
void CapturedSurfaceController::SendWheel(
blink::mojom::CapturedWheelActionPtr action,
base::OnceCallback<void(CapturedSurfaceControlResult)> reply_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (!captured_wc_.has_value()) {
std::move(reply_callback)
.Run(CapturedSurfaceControlResult::kCapturedSurfaceNotFoundError);
return;
}
// Action to be performed on the UI thread if permitted.
base::OnceCallback<CapturedSurfaceControlResult(void)> action_callback =
base::BindOnce(&DoSendWheel, capturer_rfh_id_, captured_wc_.value(),
std::move(action));
permission_manager_->CheckPermission(
ComposeCallbacks(std::move(action_callback), std::move(reply_callback)));
}
void CapturedSurfaceController::UpdateZoomLevel(
ZoomLevelAction action,
base::OnceCallback<void(CapturedSurfaceControlResult)> reply_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (!captured_wc_.has_value()) {
std::move(reply_callback)
.Run(CapturedSurfaceControlResult::kCapturedSurfaceNotFoundError);
return;
}
// Action to be performed on the UI thread if permitted.
base::OnceCallback<CapturedSurfaceControlResult(void)> action_callback =
base::BindOnce(&DoUpdateZoomLevel, capturer_rfh_id_, captured_wc_.value(),
action);
permission_manager_->CheckPermission(
ComposeCallbacks(std::move(action_callback), std::move(reply_callback)));
}
void CapturedSurfaceController::RequestPermission(
base::OnceCallback<void(CapturedSurfaceControlResult)> reply_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (!captured_wc_.has_value()) {
std::move(reply_callback)
.Run(CapturedSurfaceControlResult::kCapturedSurfaceNotFoundError);
return;
}
// If the permission check is successful, just return success.
base::OnceCallback<CapturedSurfaceControlResult(void)> action_callback =
base::BindOnce(&FinalizeRequestPermission, capturer_rfh_id_,
captured_wc_.value());
permission_manager_->CheckPermission(
ComposeCallbacks(std::move(action_callback), std::move(reply_callback)));
}
void CapturedSurfaceController::ResolveCapturedSurface(
WebContentsMediaCaptureId captured_wc_id) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// Avoid posting new tasks (DoSendWheel/DoUpdateZoomLevel) with the old target
// while pending resolution.
captured_wc_ = std::nullopt;
zoom_level_subscription_.reset();
// Ensure that, in the unlikely case that multiple resolutions are pending at
// the same time, only the resolution of the last one will set `captured_wc_`
// back to a concrete value.
++pending_wc_resolutions_;
base::RepeatingCallback on_zoom_level_change_callback =
base::BindRepeating(&CapturedSurfaceController::OnZoomLevelChange,
weak_factory_.GetWeakPtr(), ++subscription_version_);
GetUIThreadTaskRunner({})->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(
&ResolveCapturedSurfaceOnUI, captured_wc_id, subscription_version_,
base::BindPostTask(GetIOThreadTaskRunner({}),
std::move(on_zoom_level_change_callback))),
base::BindOnce(&CapturedSurfaceController::OnCapturedSurfaceResolved,
weak_factory_.GetWeakPtr()));
}
void CapturedSurfaceController::OnCapturedSurfaceResolved(
std::optional<CapturedSurfaceInfo> captured_surface_info) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK_GE(pending_wc_resolutions_, 1);
if (--pending_wc_resolutions_ > 0) {
return;
}
if (!captured_surface_info) {
return;
}
captured_wc_ = captured_surface_info->captured_wc;
zoom_level_subscription_ = std::move(captured_surface_info->subscription);
OnZoomLevelChange(captured_surface_info->subscription_version,
captured_surface_info->initial_zoom_level);
wc_resolution_callback_.Run(captured_surface_info->captured_wc);
}
void CapturedSurfaceController::OnZoomLevelChange(
int zoom_level_subscription_version,
int zoom_level) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// Only propagate zoom-level updates if they are sent with the current
// zoom-level subscription version.
if (zoom_level_subscription_version != subscription_version_) {
return;
}
on_zoom_level_change_callback_.Run(zoom_level);
}
} // namespace content