910e62b5创建于 1月15日历史提交
// 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/permission_result.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 CapturedSurfaceControlPermissionStatus =
    PermissionManager::CapturedSurfaceControlPermissionStatus;
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,
    CapturedSurfaceControlPermissionStatus permission_check_result) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (permission_check_result ==
      CapturedSurfaceControlPermissionStatus::kDenied) {
    std::move(reply_callback)
        .Run(CapturedSurfaceControlResult::kNoPermissionError);
    return;
  }

  if (permission_check_result ==
      CapturedSurfaceControlPermissionStatus::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(PermissionManager::CapturedSurfaceControlPermissionStatus)>
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