chromium/chromeos/ash/services/recording/video_capture_params.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/services/recording/video_capture_params.h"

#include "base/check.h"
#include "chromeos/ash/services/recording/recording_service_constants.h"
#include "components/viz/common/surfaces/subtree_capture_id.h"
#include "media/base/video_types.h"
#include "services/viz/privileged/mojom/compositing/frame_sink_video_capture.mojom.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/size_conversions.h"

namespace recording {

namespace {

// Returns a rect that is the result of intersecting the given two rects.
gfx::Rect GetIntersectionRect(const gfx::Rect& rect_a,
                              const gfx::Rect& rect_b) {
  auto result = rect_a;
  result.Intersect(rect_b);
  return result;
}

inline gfx::Size GetSizeInPixels(const gfx::Size& size_dips, float dsf) {
  return gfx::ToFlooredSize(gfx::ConvertSizeToPixels(size_dips, dsf));
}

// -----------------------------------------------------------------------------
// FullscreenCaptureParams:

class FullscreenCaptureParams : public VideoCaptureParams {
 public:
  FullscreenCaptureParams(viz::FrameSinkId frame_sink_id,
                          const gfx::Size& frame_sink_size_dip,
                          float device_scale_factor)
      : VideoCaptureParams(frame_sink_id,
                           viz::SubtreeCaptureId(),
                           frame_sink_size_dip,
                           device_scale_factor),
        initial_frame_sink_size_pixels_(current_frame_sink_size_pixels_) {}
  FullscreenCaptureParams(const FullscreenCaptureParams&) = delete;
  FullscreenCaptureParams& operator=(const FullscreenCaptureParams&) = delete;
  ~FullscreenCaptureParams() override = default;

  // VideoCaptureParams:
  gfx::Size GetVideoSize() const override {
    return initial_frame_sink_size_pixels_;
  }

 protected:
  void SetCapturerResolutionConstraints(
      mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer)
      const override {
    DCHECK(capturer);

    capturer->SetResolutionConstraints(
        /*min_size=*/initial_frame_sink_size_pixels_,
        /*max_size=*/initial_frame_sink_size_pixels_,
        /*use_fixed_aspect_ratio=*/true);
  }

  bool OnVideoSizeMayHaveChanged(
      mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer) override {
    // We override the default behavior, as we want the video size to remain at
    // the original requested size. This gives a nice indication of display
    // rotations. The new video frames will letterbox to adhere to
    // the original requested resolution constraints.
    return false;
  }

 private:
  // We chose not to change the video size for fullscreen recording and let the
  // capturer letterbox the content in the initial size we gave it. This gives
  // a nice effect especially when rotating the display. See
  // OnVideoSizeMayHaveChanged() above.
  const gfx::Size initial_frame_sink_size_pixels_;
};

// -----------------------------------------------------------------------------
// WindowCaptureParams:

class WindowCaptureParams : public VideoCaptureParams {
 public:
  WindowCaptureParams(viz::FrameSinkId frame_sink_id,
                      viz::SubtreeCaptureId subtree_capture_id,
                      const gfx::Size& frame_sink_size_dip,
                      float device_scale_factor,
                      const gfx::Size& window_size_dip)
      : VideoCaptureParams(frame_sink_id,
                           subtree_capture_id,
                           frame_sink_size_dip,
                           device_scale_factor),
        current_window_size_dips_(window_size_dip),
        current_window_size_pixels_(CalculateWindowSizeInPixels()) {}
  WindowCaptureParams(const WindowCaptureParams&) = delete;
  WindowCaptureParams& operator=(const WindowCaptureParams&) = delete;
  ~WindowCaptureParams() override = default;

  // VideoCaptureParams:
  gfx::Rect GetVideoFrameVisibleRect(
      const gfx::Rect& original_frame_visible_rect_pixels) const override {
    return GetIntersectionRect(original_frame_visible_rect_pixels,
                               gfx::Rect(current_window_size_pixels_));
  }

  gfx::Size GetVideoSize() const override {
    return current_window_size_pixels_;
  }

  bool OnRecordedWindowChangingRoot(
      mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer,
      viz::FrameSinkId new_frame_sink_id,
      const gfx::Size& new_frame_sink_size_dip,
      float new_device_scale_factor) override {
    DCHECK(new_frame_sink_id.is_valid());
    DCHECK_NE(frame_sink_id_, new_frame_sink_id);

    frame_sink_id_ = new_frame_sink_id;
    capturer->ChangeTarget(
        viz::VideoCaptureTarget(frame_sink_id_, subtree_capture_id_),
        /*sub_capture_target_version=*/0);

    // If the movement to another display results in changes in the frame sink
    // size or DSF, OnVideoSizeMayHaveChanged() will be called by the below
    // OnFrameSinkSizeChanged().
    return OnFrameSinkSizeChanged(capturer, new_frame_sink_size_dip,
                                  new_device_scale_factor);
  }

  bool OnRecordedWindowSizeChanged(
      mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer,
      const gfx::Size& new_window_size_dip) override {
    current_window_size_dips_ = new_window_size_dip;
    return OnVideoSizeMayHaveChanged(capturer);
  }

 protected:
  void SetCapturerResolutionConstraints(
      mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer)
      const override {
    DCHECK(capturer);

    // To avoid receiving letterboxed video frames from the capturer, we ask it
    // to give us an exact resolution matching the window's size in pixels.
    capturer->SetResolutionConstraints(/*min_size=*/current_window_size_pixels_,
                                       /*max_size=*/current_window_size_pixels_,
                                       /*use_fixed_aspect_ratio=*/true);
  }

  bool OnVideoSizeMayHaveChanged(
      mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer) override {
    const auto new_window_size_pixels = CalculateWindowSizeInPixels();
    if (new_window_size_pixels == current_window_size_pixels_)
      return false;
    current_window_size_pixels_ = new_window_size_pixels;
    SetCapturerResolutionConstraints(capturer);
    return true;
  }

 private:
  gfx::Size CalculateWindowSizeInPixels() const {
    return GetSizeInPixels(current_window_size_dips_,
                           current_device_scale_factor_);
  }

  gfx::Size current_window_size_dips_;
  gfx::Size current_window_size_pixels_;
};

// -----------------------------------------------------------------------------
// RegionCaptureParams:

class RegionCaptureParams : public VideoCaptureParams {
 public:
  RegionCaptureParams(viz::FrameSinkId frame_sink_id,
                      const gfx::Size& frame_sink_size_dip,
                      float device_scale_factor,
                      const gfx::Rect& crop_region_dip)
      : VideoCaptureParams(frame_sink_id,
                           viz::SubtreeCaptureId(),
                           frame_sink_size_dip,
                           device_scale_factor),
        crop_region_dips_(crop_region_dip),
        crop_region_pixels_(CalculateCropRegionInPixels()) {
    DCHECK(!crop_region_dips_.IsEmpty());
    DCHECK(!crop_region_pixels_.IsEmpty());
  }
  RegionCaptureParams(const RegionCaptureParams&) = delete;
  RegionCaptureParams& operator=(const RegionCaptureParams&) = delete;
  ~RegionCaptureParams() override = default;

  // VideoCaptureParams:
  gfx::Rect GetVideoFrameVisibleRect(
      const gfx::Rect& original_frame_visible_rect_pixels) const override {
    // We can't crop the video frame by an invalid bounds. The crop bounds must
    // be contained within the original frame bounds.
    gfx::Rect visible_rect = crop_region_pixels_;
    visible_rect.AdjustToFit(original_frame_visible_rect_pixels);
    return visible_rect;
  }

  gfx::Size GetVideoSize() const override {
    return GetVideoFrameVisibleRect(gfx::Rect(current_frame_sink_size_pixels_))
        .size();
  }

 protected:
  void SetCapturerResolutionConstraints(
      mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer)
      const override {
    DCHECK(capturer);

    capturer->SetResolutionConstraints(
        /*min_size=*/current_frame_sink_size_pixels_,
        /*max_size=*/current_frame_sink_size_pixels_,
        /*use_fixed_aspect_ratio=*/true);
  }

  bool OnVideoSizeMayHaveChanged(
      mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer) override {
    SetCapturerResolutionConstraints(capturer);
    const auto new_crop_region_pixels = CalculateCropRegionInPixels();
    if (new_crop_region_pixels == crop_region_pixels_)
      return false;
    crop_region_pixels_ = new_crop_region_pixels;
    return true;
  }

 private:
  // Computes and returns the crop region bounds in pixels, according to the
  // |crop_region_dips_| and the |current_device_scale_factor_|.
  gfx::Rect CalculateCropRegionInPixels() const {
    return gfx::ToRoundedRect(gfx::ConvertRectToPixels(
        crop_region_dips_, current_device_scale_factor_));
  }

  const gfx::Rect crop_region_dips_;
  gfx::Rect crop_region_pixels_;
};

}  // namespace

// -----------------------------------------------------------------------------
// VideoCaptureParams:

// static
std::unique_ptr<VideoCaptureParams>
VideoCaptureParams::CreateForFullscreenCapture(
    viz::FrameSinkId frame_sink_id,
    const gfx::Size& frame_sink_size_dip,
    float device_scale_factor) {
  return std::make_unique<FullscreenCaptureParams>(
      frame_sink_id, frame_sink_size_dip, device_scale_factor);
}

// static
std::unique_ptr<VideoCaptureParams> VideoCaptureParams::CreateForWindowCapture(
    viz::FrameSinkId frame_sink_id,
    viz::SubtreeCaptureId subtree_capture_id,
    const gfx::Size& frame_sink_size_dip,
    float device_scale_factor,
    const gfx::Size& window_size_dip) {
  return std::make_unique<WindowCaptureParams>(
      frame_sink_id, subtree_capture_id, frame_sink_size_dip,
      device_scale_factor, window_size_dip);
}

// static
std::unique_ptr<VideoCaptureParams> VideoCaptureParams::CreateForRegionCapture(
    viz::FrameSinkId frame_sink_id,
    const gfx::Size& full_capture_size,
    float device_scale_factor,
    const gfx::Rect& crop_region_dip) {
  return std::make_unique<RegionCaptureParams>(
      frame_sink_id, full_capture_size, device_scale_factor, crop_region_dip);
}

void VideoCaptureParams::InitializeVideoCapturer(
    mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer,
    media::VideoPixelFormat supported_pixel_format) const {
  DCHECK(capturer);

  capturer->SetMinCapturePeriod(kMinCapturePeriod);
  capturer->SetMinSizeChangePeriod(kMinPeriodForResizeThrottling);
  SetCapturerResolutionConstraints(capturer);
  capturer->SetAutoThrottlingEnabled(false);
  // TODO(afakhry): Discuss with //media/ team the implications of color space
  // conversions.
  capturer->SetFormat(supported_pixel_format);
  capturer->ChangeTarget(
      viz::VideoCaptureTarget(frame_sink_id_, subtree_capture_id_),
      /*sub_capture_target_version=*/0);
}

gfx::Rect VideoCaptureParams::GetVideoFrameVisibleRect(
    const gfx::Rect& original_frame_visible_rect_pixels) const {
  return original_frame_visible_rect_pixels;
}

bool VideoCaptureParams::OnRecordedWindowChangingRoot(
    mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer,
    viz::FrameSinkId new_frame_sink_id,
    const gfx::Size& new_frame_sink_size_dip,
    float new_device_scale_factor) {
  CHECK(false) << "This can only be called when recording a window";
  return false;
}

bool VideoCaptureParams::OnRecordedWindowSizeChanged(
    mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer,
    const gfx::Size& new_window_size_dip) {
  CHECK(false) << "This can only be called when recording a window";
  return false;
}

bool VideoCaptureParams::OnFrameSinkSizeChanged(
    mojo::Remote<viz::mojom::FrameSinkVideoCapturer>& capturer,
    const gfx::Size& new_frame_sink_size_dip,
    float new_device_scale_factor) {
  if (current_frame_sink_size_dips_ == new_frame_sink_size_dip &&
      current_device_scale_factor_ == new_device_scale_factor) {
    return false;
  }

  current_frame_sink_size_dips_ = new_frame_sink_size_dip;
  current_device_scale_factor_ = new_device_scale_factor;
  current_frame_sink_size_pixels_ = CalculateFrameSinkSizeInPixels();

  return OnVideoSizeMayHaveChanged(capturer);
}

VideoCaptureParams::VideoCaptureParams(viz::FrameSinkId frame_sink_id,
                                       viz::SubtreeCaptureId subtree_capture_id,
                                       const gfx::Size& current_frame_sink_size,
                                       float device_scale_factor)
    : frame_sink_id_(frame_sink_id),
      subtree_capture_id_(subtree_capture_id),
      current_frame_sink_size_dips_(current_frame_sink_size),
      current_device_scale_factor_(device_scale_factor),
      current_frame_sink_size_pixels_(CalculateFrameSinkSizeInPixels()) {
  DCHECK(frame_sink_id_.is_valid());
}

gfx::Size VideoCaptureParams::CalculateFrameSinkSizeInPixels() const {
  return GetSizeInPixels(current_frame_sink_size_dips_,
                         current_device_scale_factor_);
}

}  // namespace recording