Add viz::VideoCaptureOverlay to support mouse cursor rendering VIZ-side.
VideoCaptureOverlay provides the post-GPU-readback rendering of small
SkBitmaps onto the VideoFrames captured by viz::FrameSinkVideoCapturer.
This will be used to move mouse cursor rendering out of the browser
process, and into VIZ, as was intended in the original design. More
details in crbug/810133.
A soon-upcoming series of changes will provide mojo IDL definition as
well as integration of VideoCaptureOverlay with FrameSinkVideoCapturer.
Bug: 810133
Cq-Include-Trybots: luci.chromium.try:android_optional_gpu_tests_rel
Change-Id: I16897dbd361af153d10a1223339d9fb5f7e5f05a
Reviewed-on: https://chromium-review.googlesource.com/1132318
Commit-Queue: Yuri Wiitala <miu@chromium.org>
Reviewed-by: Xiangjun Zhang <xjz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#574480}diff --git a/components/viz/service/BUILD.gn b/components/viz/service/BUILD.gn
index 826d73f..166f325 100644
--- a/components/viz/service/BUILD.gn
+++ b/components/viz/service/BUILD.gn
@@ -144,6 +144,8 @@
"frame_sinks/video_capture/in_flight_frame_delivery.h",
"frame_sinks/video_capture/interprocess_frame_pool.cc",
"frame_sinks/video_capture/interprocess_frame_pool.h",
+ "frame_sinks/video_capture/video_capture_overlay.cc",
+ "frame_sinks/video_capture/video_capture_overlay.h",
"frame_sinks/video_detector.cc",
"frame_sinks/video_detector.h",
"gl/gpu_service_impl.cc",
@@ -319,6 +321,7 @@
"frame_sinks/surface_synchronization_unittest.cc",
"frame_sinks/video_capture/frame_sink_video_capturer_impl_unittest.cc",
"frame_sinks/video_capture/interprocess_frame_pool_unittest.cc",
+ "frame_sinks/video_capture/video_capture_overlay_unittest.cc",
"frame_sinks/video_detector_unittest.cc",
"gl/gpu_service_impl_unittest.cc",
"hit_test/hit_test_aggregator_unittest.cc",
diff --git a/components/viz/service/frame_sinks/video_capture/video_capture_overlay.cc b/components/viz/service/frame_sinks/video_capture/video_capture_overlay.cc
new file mode 100644
index 0000000..75be2cb
--- /dev/null
+++ b/components/viz/service/frame_sinks/video_capture/video_capture_overlay.cc
@@ -0,0 +1,574 @@
+// Copyright 2018 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 "components/viz/service/frame_sinks/video_capture/video_capture_overlay.h"
+
+#include <algorithm>
+#include <cmath>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/numerics/safe_conversions.h"
+#include "base/trace_event/trace_event.h"
+#include "media/base/limits.h"
+#include "media/base/video_frame.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "third_party/skia/include/core/SkFilterQuality.h"
+#include "third_party/skia/include/core/SkImageInfo.h"
+#include "ui/gfx/geometry/point.h"
+#include "ui/gfx/geometry/rect_conversions.h"
+
+using media::VideoFrame;
+using media::VideoPixelFormat;
+
+namespace viz {
+
+VideoCaptureOverlay::FrameSource::~FrameSource() = default;
+
+VideoCaptureOverlay::VideoCaptureOverlay(FrameSource* frame_source)
+ : frame_source_(frame_source) {
+ DCHECK(frame_source_);
+}
+
+VideoCaptureOverlay::~VideoCaptureOverlay() = default;
+
+void VideoCaptureOverlay::SetImageAndBounds(const SkBitmap& image,
+ const gfx::RectF& bounds) {
+ const gfx::Rect old_rect = ComputeSourceMutationRect();
+
+ image_ = image;
+ bounds_ = bounds;
+
+ // Reset the cached sprite since the source image has been changed.
+ sprite_ = nullptr;
+
+ const gfx::Rect new_rect = ComputeSourceMutationRect();
+ if (!new_rect.IsEmpty() || !old_rect.IsEmpty()) {
+ frame_source_->InvalidateRect(old_rect);
+ frame_source_->InvalidateRect(new_rect);
+ frame_source_->RequestRefreshFrame();
+ }
+}
+
+void VideoCaptureOverlay::SetBounds(const gfx::RectF& bounds) {
+ if (bounds_ != bounds) {
+ const gfx::Rect old_rect = ComputeSourceMutationRect();
+ bounds_ = bounds;
+ const gfx::Rect new_rect = ComputeSourceMutationRect();
+ if (!new_rect.IsEmpty() || !old_rect.IsEmpty()) {
+ frame_source_->InvalidateRect(old_rect);
+ frame_source_->InvalidateRect(new_rect);
+ frame_source_->RequestRefreshFrame();
+ }
+ }
+}
+
+namespace {
+
+// Scales a |relative| rect having coordinates in the range [0.0,1.0) by the
+// given |span|, snapping all coordinates to even numbers.
+gfx::Rect ToAbsoluteBoundsForI420(const gfx::RectF& relative,
+ const gfx::Rect& span) {
+ const float absolute_left = std::fma(relative.x(), span.width(), span.x());
+ const float absolute_top = std::fma(relative.y(), span.height(), span.y());
+ const float absolute_right =
+ std::fma(relative.right(), span.width(), span.x());
+ const float absolute_bottom =
+ std::fma(relative.bottom(), span.height(), span.y());
+
+ // Compute the largest I420-friendly Rect that is fully-enclosed by the
+ // absolute rect. Use saturated_cast<> to restrict all extreme results [and
+ // Inf and NaN] to a safe range of integers.
+ const int snapped_left =
+ base::saturated_cast<int16_t>(std::ceil(absolute_left / 2.0f)) * 2;
+ const int snapped_top =
+ base::saturated_cast<int16_t>(std::ceil(absolute_top / 2.0f)) * 2;
+ const int snapped_right =
+ base::saturated_cast<int16_t>(std::floor(absolute_right / 2.0f)) * 2;
+ const int snapped_bottom =
+ base::saturated_cast<int16_t>(std::floor(absolute_bottom / 2.0f)) * 2;
+ return gfx::Rect(snapped_left, snapped_top,
+ std::max(0, snapped_right - snapped_left),
+ std::max(0, snapped_bottom - snapped_top));
+}
+
+// Shrinks the given |rect| by the minimum amount necessary to align its corners
+// to even-numbered coordinates. |rect| is assumed to have non-negative values
+// for its coordinates.
+gfx::Rect MinimallyShrinkRectForI420(const gfx::Rect& rect) {
+ DCHECK(gfx::Rect(0, 0, media::limits::kMaxDimension,
+ media::limits::kMaxDimension)
+ .Contains(rect));
+ const int left = rect.x() + (rect.x() % 2);
+ const int top = rect.y() + (rect.y() % 2);
+ const int right = rect.right() - (rect.right() % 2);
+ const int bottom = rect.bottom() - (rect.bottom() % 2);
+ return gfx::Rect(left, top, std::max(0, right - left),
+ std::max(0, bottom - top));
+}
+
+} // namespace
+
+VideoCaptureOverlay::OnceRenderer VideoCaptureOverlay::MakeRenderer(
+ const gfx::Rect& region_in_frame,
+ const VideoPixelFormat frame_format,
+ const gfx::ColorSpace& frame_color_space) {
+ // If there's no image set yet, punt.
+ if (image_.drawsNothing()) {
+ return VideoCaptureOverlay::OnceRenderer();
+ }
+
+ // Determine the bounds of the sprite to be blitted onto the video frame. The
+ // calculations here align to the 2x2 pixel-quads, since dealing with
+ // fractions or partial I420 chroma plane alpha-blending would greatly
+ // complexify the blitting algorithm later on. This introduces a little
+ // inaccuracy in the size and position of the overlay in the final result, but
+ // should be an acceptable trade-off for all use cases.
+ const gfx::Rect bounds_in_frame =
+ ToAbsoluteBoundsForI420(bounds_, region_in_frame);
+ // If the sprite's size will be unreasonably large, punt.
+ if (bounds_in_frame.width() > media::limits::kMaxDimension ||
+ bounds_in_frame.height() > media::limits::kMaxDimension) {
+ return VideoCaptureOverlay::OnceRenderer();
+ }
+
+ // Compute the blit rect: the region of the frame to be modified by future
+ // Sprite::Blit() calls. First, |region_in_frame| must be shrunk to have
+ // even-valued coordinates to ensure the final blit rect is I420-friendly.
+ // Then, the shrunk |region_in_frame| is used to clip |bounds_in_frame|.
+ gfx::Rect blit_rect = MinimallyShrinkRectForI420(region_in_frame);
+ blit_rect.Intersect(bounds_in_frame);
+ // If the two rects didn't intersect at all (i.e., everything has been
+ // clipped), punt.
+ if (blit_rect.IsEmpty()) {
+ return VideoCaptureOverlay::OnceRenderer();
+ }
+
+ // If the cached sprite does not match the computed scaled size, pixel format,
+ // and/or color space, create a new instance for this (and future) renderers.
+ if (!sprite_ || sprite_->size() != bounds_in_frame.size() ||
+ sprite_->format() != frame_format ||
+ sprite_->color_space() != frame_color_space) {
+ sprite_ = base::MakeRefCounted<Sprite>(image_, bounds_in_frame.size(),
+ frame_format, frame_color_space);
+ }
+
+ return base::BindOnce(&Sprite::Blit, sprite_, bounds_in_frame.origin(),
+ blit_rect);
+}
+
+// static
+VideoCaptureOverlay::OnceRenderer VideoCaptureOverlay::MakeCombinedRenderer(
+ const std::vector<std::unique_ptr<VideoCaptureOverlay>>& overlays,
+ const gfx::Rect& region_in_frame,
+ const VideoPixelFormat frame_format,
+ const gfx::ColorSpace& frame_color_space) {
+ if (overlays.empty()) {
+ return VideoCaptureOverlay::OnceRenderer();
+ }
+
+ std::vector<OnceRenderer> renderers;
+ for (const std::unique_ptr<VideoCaptureOverlay>& overlay : overlays) {
+ renderers.emplace_back(overlay->MakeRenderer(region_in_frame, frame_format,
+ frame_color_space));
+ if (renderers.back().is_null()) {
+ renderers.pop_back();
+ }
+ }
+
+ if (renderers.empty()) {
+ return VideoCaptureOverlay::OnceRenderer();
+ }
+
+ return base::BindOnce(
+ [](std::vector<OnceRenderer> renderers, VideoFrame* frame) {
+ for (OnceRenderer& renderer : renderers) {
+ std::move(renderer).Run(frame);
+ }
+ },
+ std::move(renderers));
+}
+
+gfx::Rect VideoCaptureOverlay::ComputeSourceMutationRect() const {
+ if (!image_.drawsNothing() && !bounds_.IsEmpty()) {
+ const gfx::Size& source_size = frame_source_->GetSourceSize();
+ gfx::Rect result = gfx::ToEnclosingRect(
+ gfx::ScaleRect(bounds_, source_size.width(), source_size.height()));
+ result.Intersect(gfx::Rect(source_size));
+ return result;
+ }
+ return gfx::Rect();
+}
+
+VideoCaptureOverlay::Sprite::Sprite(const SkBitmap& image,
+ const gfx::Size& size,
+ const VideoPixelFormat format,
+ const gfx::ColorSpace& color_space)
+ : image_(image), size_(size), format_(format), color_space_(color_space) {
+ DCHECK(!image_.isNull());
+}
+
+VideoCaptureOverlay::Sprite::~Sprite() {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+}
+
+namespace {
+
+// Returns the pointer to the element at the |offset| position, given a pointer
+// to the element for (0,0) in a row-major image plane.
+template <typename Pointer>
+Pointer PositionPointerInPlane(Pointer plane_begin,
+ int stride,
+ const gfx::Point& offset) {
+ return plane_begin + (offset.y() * stride) + offset.x();
+}
+
+// Returns the pointer to the element at the |offset| position, given a pointer
+// to the element for (0,0) in a row-major bitmap with 4 elements per pixel.
+template <typename Pointer>
+Pointer PositionPointerARGB(Pointer pixels_begin,
+ int stride,
+ const gfx::Point& offset) {
+ return pixels_begin + (offset.y() * stride) + (4 * offset.x());
+}
+
+// Transforms the lower 8 bits of |value| from the [0,255] range to the
+// normalized floating-point [0.0,1.0] range.
+float From255(uint8_t value) {
+ return value / 255.0f;
+}
+
+// Transforms the value from the normalized floating-point [0.0,1.0] range to an
+// unsigned int in the [0,255] range, capping any out-of-range values.
+uint32_t ToClamped255(float value) {
+ value = std::fma(value, 255.0f, 0.5f /* rounding */);
+ return base::saturated_cast<uint8_t>(value);
+}
+
+} // namespace
+
+void VideoCaptureOverlay::Sprite::Blit(const gfx::Point& position,
+ const gfx::Rect& blit_rect,
+ VideoFrame* frame) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ DCHECK(frame);
+ DCHECK_EQ(format_, frame->format());
+ DCHECK(frame->visible_rect().Contains(blit_rect));
+
+ TRACE_EVENT2("gpu.capture", "VideoCaptureOverlay::Sprite::Blit", "x",
+ position.x(), "y", position.y());
+
+ if (!transformed_image_) {
+ TransformImageOnce();
+ }
+
+ // Compute the left-most and top-most pixel to source from the transformed
+ // image. This is usually (0,0) unless only part of the sprite is being
+ // blitted (i.e., cropped at the edge(s) of the video frame).
+ gfx::Point src_origin = blit_rect.origin() - position.OffsetFromOrigin();
+ DCHECK(gfx::Rect(size_).Contains(gfx::Rect(src_origin, blit_rect.size())));
+
+ // Blit the sprite (src) onto the video frame (dest). One of two algorithms is
+ // used, depending on the video frame's format, as the blending calculations
+ // and data layout/format are different.
+ switch (frame->format()) {
+ case media::PIXEL_FORMAT_I420: {
+ // Core assumption: All coordinates are aligned to even-numbered
+ // coordinates.
+ DCHECK_EQ(src_origin.x() % 2, 0);
+ DCHECK_EQ(src_origin.y() % 2, 0);
+ DCHECK_EQ(blit_rect.x() % 2, 0);
+ DCHECK_EQ(blit_rect.y() % 2, 0);
+ DCHECK_EQ(blit_rect.width() % 2, 0);
+ DCHECK_EQ(blit_rect.height() % 2, 0);
+
+ // Helper function to execute a "SrcOver" blit from |src| to |dst|, and
+ // store the results back in |dst|.
+ const auto BlitOntoPlane = [](const gfx::Size& blit_size, int src_stride,
+ const float* src, const float* under_weight,
+ int dst_stride, uint8_t* dst) {
+ for (int row = 0; row < blit_size.height(); ++row, src += src_stride,
+ under_weight += src_stride, dst += dst_stride) {
+ for (int col = 0; col < blit_size.width(); ++col) {
+ dst[col] = ToClamped255(
+ std::fma(From255(dst[col]), under_weight[col], src[col]));
+ }
+ }
+ };
+
+ // Blit the Y plane: |src| points to the pre-multiplied luma values, while
+ // |under_weight| points to the "one minus src alpha" values. Both have
+ // the same stride, |src_stride|.
+ int src_stride = size_.width();
+ const float* under_weight = PositionPointerInPlane(
+ transformed_image_.get(), src_stride, src_origin);
+ const int num_pixels = size_.GetArea();
+ const float* src = under_weight + num_pixels;
+ // Likewise, start |dst| at the upper-left-most pixel within the video
+ // frame's Y plane that will be SrcOver'ed.
+ int dst_stride = frame->stride(VideoFrame::kYPlane);
+ uint8_t* dst =
+ PositionPointerInPlane(frame->visible_data(VideoFrame::kYPlane),
+ dst_stride, blit_rect.origin());
+ BlitOntoPlane(blit_rect.size(), src_stride, src, under_weight, dst_stride,
+ dst);
+
+ // Blit the U and V planes similarly to the Y plane, but reduce all
+ // coordinates by 2x2.
+ src_stride = size_.width() / 2;
+ src_origin = gfx::Point(src_origin.x() / 2, src_origin.y() / 2);
+ under_weight = PositionPointerInPlane(
+ transformed_image_.get() + 2 * num_pixels, src_stride, src_origin);
+ const int num_chroma_pixels = size_.GetArea() / 4;
+ src = under_weight + num_chroma_pixels;
+ dst_stride = frame->stride(VideoFrame::kUPlane);
+ const gfx::Rect chroma_blit_rect(blit_rect.x() / 2, blit_rect.y() / 2,
+ blit_rect.width() / 2,
+ blit_rect.height() / 2);
+ dst = PositionPointerInPlane(frame->visible_data(VideoFrame::kUPlane),
+ dst_stride, chroma_blit_rect.origin());
+ BlitOntoPlane(chroma_blit_rect.size(), src_stride, src, under_weight,
+ dst_stride, dst);
+ src += num_chroma_pixels;
+ dst_stride = frame->stride(VideoFrame::kVPlane);
+ dst = PositionPointerInPlane(frame->visible_data(VideoFrame::kVPlane),
+ dst_stride, chroma_blit_rect.origin());
+ BlitOntoPlane(chroma_blit_rect.size(), src_stride, src, under_weight,
+ dst_stride, dst);
+
+ break;
+ }
+
+ case media::PIXEL_FORMAT_ARGB: {
+ // Start |src| at the upper-left-most pixel within |transformed_image_|
+ // that will be blitted.
+ const int src_stride = size_.width() * 4;
+ const float* src =
+ PositionPointerARGB(transformed_image_.get(), src_stride, src_origin);
+
+ // Likewise, start |dst| at the upper-left-most pixel within the video
+ // frame that will be SrcOver'ed.
+ const int dst_stride = frame->stride(VideoFrame::kARGBPlane);
+ DCHECK_EQ(dst_stride % sizeof(uint32_t), 0u);
+ uint8_t* dst =
+ PositionPointerARGB(frame->visible_data(VideoFrame::kARGBPlane),
+ dst_stride, blit_rect.origin());
+ DCHECK_EQ((dst - frame->visible_data(VideoFrame::kARGBPlane)) %
+ sizeof(uint32_t),
+ 0u);
+
+ // Blend each sprite pixel over the corresponding pixel in the video
+ // frame, and store the result back in the video frame. Note that the
+ // video frame format does NOT have color values pre-multiplied by the
+ // alpha.
+ for (int row = 0; row < blit_rect.height();
+ ++row, src += src_stride, dst += dst_stride) {
+ uint32_t* dst_pixel = reinterpret_cast<uint32_t*>(dst);
+ for (int col = 0; col < blit_rect.width(); ++col) {
+ const int src_idx = 4 * col;
+ const float src_alpha = src[src_idx];
+ const float dst_weight =
+ From255(dst_pixel[col] >> 24) * (1.0f - src_alpha);
+ const float out_alpha = src_alpha + dst_weight;
+ float out_red = std::fma(From255(dst_pixel[col] >> 16), dst_weight,
+ src[src_idx + 1]);
+ float out_green = std::fma(From255(dst_pixel[col] >> 8), dst_weight,
+ src[src_idx + 2]);
+ float out_blue = std::fma(From255(dst_pixel[col] >> 0), dst_weight,
+ src[src_idx + 3]);
+ if (out_alpha != 0.0f) {
+ out_red /= out_alpha;
+ out_green /= out_alpha;
+ out_blue /= out_alpha;
+ }
+ dst_pixel[col] =
+ ((ToClamped255(out_alpha) << 24) | (ToClamped255(out_red) << 16) |
+ (ToClamped255(out_green) << 8) | (ToClamped255(out_blue) << 0));
+ }
+ }
+
+ break;
+ }
+
+ default:
+ NOTREACHED();
+ break;
+ }
+}
+
+void VideoCaptureOverlay::Sprite::TransformImageOnce() {
+ TRACE_EVENT2("gpu.capture", "VideoCaptureOverlay::Sprite::TransformImageOnce",
+ "width", size_.width(), "height", size_.height());
+
+ // Scale the source |image_| to match the format and size required. For the
+ // purposes of color space conversion, the alpha must not be pre-multiplied.
+ const SkImageInfo scaled_image_format =
+ SkImageInfo::Make(size_.width(), size_.height(), kN32_SkColorType,
+ kUnpremul_SkAlphaType, image_.refColorSpace());
+ SkBitmap scaled_image;
+ if (image_.info() == scaled_image_format) {
+ scaled_image = image_;
+ } else {
+ if (!scaled_image.tryAllocPixels(scaled_image_format) ||
+ !image_.pixmap().scalePixels(scaled_image.pixmap(),
+ kMedium_SkFilterQuality)) {
+ // If the allocation, format conversion and/or scaling failed, just reset
+ // the |scaled_image|. This will be checked below.
+ scaled_image.reset();
+ }
+ }
+
+ gfx::ColorSpace image_color_space;
+ if (scaled_image.colorSpace()) {
+ image_color_space = gfx::ColorSpace(*scaled_image.colorSpace());
+ DCHECK(image_color_space.IsValid());
+ } else {
+ // Assume a default linear color space, if no color space was provided.
+ DCHECK(!image_.colorSpace()); // Skia should have set it!
+ image_color_space = gfx::ColorSpace(
+ gfx::ColorSpace::PrimaryID::BT709, gfx::ColorSpace::TransferID::LINEAR,
+ gfx::ColorSpace::MatrixID::RGB, gfx::ColorSpace::RangeID::FULL);
+ }
+
+ // The source image is no longer needed. Reset it to dereference pixel memory.
+ image_.reset();
+
+ // Populate |colors| and |alphas| from the |scaled_image|. If the image
+ // scaling operation failed, this sprite should draw nothing, and so fully
+ // transparent pixels will be generated instead.
+ const int num_pixels = size_.GetArea();
+ std::unique_ptr<float[]> alphas(new float[num_pixels]);
+ std::unique_ptr<gfx::ColorTransform::TriStim[]> colors(
+ new gfx::ColorTransform::TriStim[num_pixels]);
+ if (scaled_image.drawsNothing()) {
+ std::fill(alphas.get(), alphas.get() + num_pixels, 0.0f);
+ std::fill(colors.get(), colors.get() + num_pixels,
+ gfx::ColorTransform::TriStim());
+ } else {
+ int pos = 0;
+ for (int y = 0; y < size_.height(); ++y) {
+ const uint32_t* src = scaled_image.getAddr32(0, y);
+ for (int x = 0; x < size_.width(); ++x) {
+ const uint32_t pixel = src[x];
+ alphas[pos] = ((pixel >> SK_A32_SHIFT) & 0xff) / 255.0f;
+ colors[pos].SetPoint(((pixel >> SK_R32_SHIFT) & 0xff) / 255.0f,
+ ((pixel >> SK_G32_SHIFT) & 0xff) / 255.0f,
+ ((pixel >> SK_B32_SHIFT) & 0xff) / 255.0f);
+ ++pos;
+ }
+ }
+ }
+
+ // Transform the colors, if needed. This may perform RGB→YUV conversion.
+ if (image_color_space != color_space_) {
+ const auto color_transform = gfx::ColorTransform::NewColorTransform(
+ image_color_space, color_space_,
+ gfx::ColorTransform::Intent::INTENT_ABSOLUTE);
+ color_transform->Transform(colors.get(), num_pixels);
+ }
+
+ switch (format_) {
+ case media::PIXEL_FORMAT_I420: {
+ // Produce 5 planes of data: The "one minus alpha" plane, the Y plane, the
+ // subsampled "one minus alpha" plane, the U plane, and the V plane.
+ // Pre-multiply the colors by the alpha to prevent extra work in multiple
+ // later Blit() calls.
+ DCHECK_EQ(size_.width() % 2, 0);
+ DCHECK_EQ(size_.height() % 2, 0);
+ const int num_chroma_pixels = size_.GetArea() / 4;
+ transformed_image_.reset(
+ new float[num_pixels * 2 + num_chroma_pixels * 3]);
+
+ // Copy the alpha values, and pre-multiply the luma values by the alpha.
+ float* out_1_minus_alpha = transformed_image_.get();
+ float* out_luma = out_1_minus_alpha + num_pixels;
+ for (int i = 0; i < num_pixels; ++i) {
+ const float alpha = alphas[i];
+ out_1_minus_alpha[i] = 1.0f - alpha;
+ out_luma[i] = colors[i].x() * alpha;
+ }
+
+ // Downscale the alpha, U, and V planes by 2x2, and pre-multiply the
+ // chroma values by the alpha.
+ float* out_uv_1_minus_alpha = out_luma + num_pixels;
+ float* out_u = out_uv_1_minus_alpha + num_chroma_pixels;
+ float* out_v = out_u + num_chroma_pixels;
+ const float* alpha_row0 = alphas.get();
+ const float* const alpha_row_end = alpha_row0 + num_pixels;
+ const gfx::ColorTransform::TriStim* color_row0 = colors.get();
+ while (alpha_row0 < alpha_row_end) {
+ const float* alpha_row1 = alpha_row0 + size_.width();
+ const gfx::ColorTransform::TriStim* color_row1 =
+ color_row0 + size_.width();
+ for (int col = 0; col < size_.width(); col += 2) {
+ // First, the downscaled alpha is the average of the four original
+ // alpha values:
+ //
+ // sum_of_alphas = a[r,c] + a[r,c+1] + a[r+1,c] + a[r+1,c+1];
+ // average_alpha = sum_of_alphas / 4
+ *(out_uv_1_minus_alpha++) =
+ std::fma(alpha_row0[col] + alpha_row0[col + 1] + alpha_row1[col] +
+ alpha_row1[col + 1],
+ -1.0f / 4.0f, 1.0f);
+ // Then, the downscaled chroma values are the weighted average of the
+ // four original chroma values (weighed by alpha):
+ //
+ // weighted_sum_of_chromas =
+ // c[r,c]*a[r,c] + c[r,c+1]*a[r,c+1] +
+ // c[r+1,c]*a[r+1,c] + c[r+1,c+1]*a[r+1,c+1]
+ // sum_of_weights = sum_of_alphas;
+ // average_chroma = weighted_sum_of_chromas / sum_of_weights
+ //
+ // But then, because the chroma is to be pre-multiplied by the alpha,
+ // the calculations simplify, as follows:
+ //
+ // premul_chroma = average_chroma * average_alpha
+ // = (weighted_sum_of_chromas / sum_of_alphas) *
+ // (sum_of_alphas / 4)
+ // = weighted_sum_of_chromas / 4
+ //
+ // This also automatically solves a special case, when sum_of_alphas
+ // is zero: With the simplified calculations, there is no longer a
+ // "divide-by-zero guard" needed; and the result in this case will be
+ // a zero chroma, which is perfectly acceptable behavior.
+ *(out_u++) = ((color_row0[col].y() * alpha_row0[col]) +
+ (color_row0[col + 1].y() * alpha_row0[col + 1]) +
+ (color_row1[col].y() * alpha_row1[col]) +
+ (color_row1[col + 1].y() * alpha_row1[col + 1])) /
+ 4.0f;
+ *(out_v++) = ((color_row0[col].z() * alpha_row0[col]) +
+ (color_row0[col + 1].z() * alpha_row0[col + 1]) +
+ (color_row1[col].z() * alpha_row1[col]) +
+ (color_row1[col + 1].z() * alpha_row1[col + 1])) /
+ 4.0f;
+ }
+ alpha_row0 = alpha_row1 + size_.width();
+ color_row0 = color_row1 + size_.width();
+ }
+
+ break;
+ }
+
+ case media::PIXEL_FORMAT_ARGB: {
+ // Produce ARGB pixels from |colors| and |alphas|. Pre-multiply the colors
+ // by the alpha to prevent extra work in multiple later Blit() calls.
+ transformed_image_.reset(new float[num_pixels * 4]);
+ float* out = transformed_image_.get();
+ for (int i = 0; i < num_pixels; ++i) {
+ const float alpha = alphas[i];
+ *(out++) = alpha;
+ *(out++) = colors[i].x() * alpha;
+ *(out++) = colors[i].y() * alpha;
+ *(out++) = colors[i].z() * alpha;
+ }
+ break;
+ }
+
+ default:
+ NOTREACHED();
+ break;
+ }
+}
+
+} // namespace viz
diff --git a/components/viz/service/frame_sinks/video_capture/video_capture_overlay.h b/components/viz/service/frame_sinks/video_capture/video_capture_overlay.h
new file mode 100644
index 0000000..1d39a15
--- /dev/null
+++ b/components/viz/service/frame_sinks/video_capture/video_capture_overlay.h
@@ -0,0 +1,186 @@
+// Copyright 2018 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.
+
+#ifndef COMPONENTS_VIZ_SERVICE_FRAME_SINKS_VIDEO_CAPTURE_VIDEO_CAPTURE_OVERLAY_H_
+#define COMPONENTS_VIZ_SERVICE_FRAME_SINKS_VIDEO_CAPTURE_VIDEO_CAPTURE_OVERLAY_H_
+
+#include <stdint.h>
+
+#include <memory>
+#include <vector>
+
+#include "base/callback.h"
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/sequence_checker.h"
+#include "components/viz/service/viz_service_export.h"
+#include "media/base/video_types.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "ui/gfx/color_space.h"
+#include "ui/gfx/color_transform.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/rect_f.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace media {
+class VideoFrame;
+}
+
+namespace viz {
+
+// An overlay image to be blitted onto video frames. A mojo client sets the
+// image, position, and size of the overlay in the video frame; and then this
+// VideoCaptureOverlay scales the image and maps its color space to match that
+// of the video frame before the blitting.
+//
+// As an optimization, the client's bitmap image is transformed (scaled, color
+// space converted, and pre-multiplied by alpha), and then this cached Sprite is
+// re-used for blitting to all successive video frames until some change
+// requires a different transformation. MakeRenderer() produces a Renderer
+// callback that holds a reference to an existing Sprite, or will create a new
+// one if necessary. The Renderer callback can then be run at any point in the
+// future, unaffected by later image, size, or color space settings changes.
+//
+// The blit algorithm uses naive linear blending. Thus, the use of non-linear
+// color spaces will cause loses in color accuracy.
+//
+// TODO(crbug.com/810133): Override the mojom::FrameSinkVideoCaptureOverlay
+// interface.
+class VIZ_SERVICE_EXPORT VideoCaptureOverlay {
+ public:
+ // Interface for notifying the frame source when changes to the overlay's
+ // state occur.
+ class VIZ_SERVICE_EXPORT FrameSource {
+ public:
+ // Returns the current size of the source, or empty if unknown.
+ virtual gfx::Size GetSourceSize() = 0;
+
+ // Notifies the FrameSource that the given source |rect| needs to be
+ // re-captured soon. One or more calls to this method will be followed-up
+ // with a call to RequestRefreshFrame().
+ virtual void InvalidateRect(const gfx::Rect& rect) = 0;
+
+ // Notifies the FrameSource that another frame should be captured and have
+ // its VideoCaptureOverlay re-rendered soon to reflect an updated overlay
+ // image and/or position.
+ virtual void RequestRefreshFrame() = 0;
+
+ protected:
+ virtual ~FrameSource();
+ };
+
+ // A OnceCallback that, when run, renders the overlay on a VideoFrame.
+ using OnceRenderer = base::OnceCallback<void(media::VideoFrame*)>;
+
+ // |frame_source| must outlive this instance.
+ explicit VideoCaptureOverlay(FrameSource* frame_source);
+
+ ~VideoCaptureOverlay();
+
+ // Sets/Changes the overlay |image| and its position and size, relative to the
+ // source content. |bounds| consists of coordinates where the range [0.0,1.0)
+ // indicates the relative position+size within the bounds of the source
+ // content (e.g., 0.0 refers to the top or left edge; 1.0 to just after the
+ // bottom or right edge). Pass empty |bounds| to temporarily hide the overlay
+ // until a later call to SetBounds().
+ void SetImageAndBounds(const SkBitmap& image, const gfx::RectF& bounds);
+ void SetBounds(const gfx::RectF& bounds);
+
+ // Returns a OnceCallback that, when run, renders this VideoCaptureOverlay on
+ // a VideoFrame. The overlay's position and size are computed based on the
+ // given content |region_in_frame|, and its color space is converted to match
+ // the |frame_color_space|. Returns a null OnceCallback if there is nothing to
+ // render at this time.
+ OnceRenderer MakeRenderer(const gfx::Rect& region_in_frame,
+ const media::VideoPixelFormat frame_format,
+ const gfx::ColorSpace& frame_color_space);
+
+ // Returns a OnceCallback that renders all of the given |overlays| in
+ // order. The remaining arguments are the same as in MakeRenderer(). This is a
+ // convenience that produces a single callback, so that client code need not
+ // deal with collections of callbacks. Returns a null OnceCallback if there is
+ // nothing to render at this time.
+ static OnceRenderer MakeCombinedRenderer(
+ const std::vector<std::unique_ptr<VideoCaptureOverlay>>& overlays,
+ const gfx::Rect& region_in_frame,
+ const media::VideoPixelFormat frame_format,
+ const gfx::ColorSpace& frame_color_space);
+
+ private:
+ // Transforms the overlay SkBitmap image by scaling and converting its color
+ // space, and then blitting it onto a VideoFrame. The transformation is lazy:
+ // Meaning, a reference to the SkBitmap image is held until the first call to
+ // Blit(), where the transformation is then executed and the reference to the
+ // original SkBitmap dropped. The transformed data is then cached for re-use
+ // for later Blit() calls.
+ class Sprite : public base::RefCounted<Sprite> {
+ public:
+ Sprite(const SkBitmap& image,
+ const gfx::Size& size,
+ const media::VideoPixelFormat format,
+ const gfx::ColorSpace& color_space);
+
+ const gfx::Size& size() const { return size_; }
+ media::VideoPixelFormat format() const { return format_; }
+ const gfx::ColorSpace& color_space() const { return color_space_; }
+
+ void Blit(const gfx::Point& position,
+ const gfx::Rect& blit_rect,
+ media::VideoFrame* frame);
+
+ private:
+ friend class base::RefCounted<Sprite>;
+ ~Sprite();
+
+ void TransformImageOnce();
+
+ // As Sprites can be long-lived and hidden from external code within
+ // callbacks, ensure that all Blit() calls are in-sequence.
+ SEQUENCE_CHECKER(sequence_checker_);
+
+ // If not null, this is the original, unscaled overlay image. After
+ // TransformImageOnce() has been called, this is set to null.
+ SkBitmap image_;
+
+ // The size, format, and color space of the cached transformed image.
+ const gfx::Size size_;
+ const media::VideoPixelFormat format_;
+ const gfx::ColorSpace color_space_;
+
+ // The transformed source image data. For blitting to ARGB format video
+ // frames, the source image data will consist of 4 elements per pixel pixel
+ // (A, R, G, B). For blitting to the I420 format, the source image data is
+ // not interlaved: Instead, there are 5 planes of data (one minus alpha, Y,
+ // subsampled one minus alpha, U, V). For both formats, the color components
+ // are premultiplied for more-efficient Blit()'s.
+ std::unique_ptr<float[]> transformed_image_;
+
+ DISALLOW_COPY_AND_ASSIGN(Sprite);
+ };
+
+ // Computes the region of the source that, if changed, would require
+ // re-rendering the overlay.
+ gfx::Rect ComputeSourceMutationRect() const;
+
+ FrameSource* const frame_source_;
+
+ // The currently-set overlay image.
+ SkBitmap image_;
+
+ // If empty, the overlay is currently hidden. Otherwise, this consists of
+ // coordinates where the range [0.0,1.0) indicates the relative position+size
+ // within the bounds of the video frame's content region (e.g., 0.0 refers to
+ // the top or left edge; 1.0 to just after the bottom or right edge).
+ gfx::RectF bounds_;
+
+ // The current Sprite. This is set to null whenever a settings change requires
+ // a new Sprite to be generated from the |image_|.
+ scoped_refptr<Sprite> sprite_;
+
+ DISALLOW_COPY_AND_ASSIGN(VideoCaptureOverlay);
+};
+
+} // namespace viz
+
+#endif // COMPONENTS_VIZ_SERVICE_FRAME_SINKS_VIDEO_CAPTURE_VIDEO_CAPTURE_OVERLAY_H_
diff --git a/components/viz/service/frame_sinks/video_capture/video_capture_overlay_unittest.cc b/components/viz/service/frame_sinks/video_capture/video_capture_overlay_unittest.cc
new file mode 100644
index 0000000..2bb5112
--- /dev/null
+++ b/components/viz/service/frame_sinks/video_capture/video_capture_overlay_unittest.cc
@@ -0,0 +1,554 @@
+// Copyright 2018 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 "components/viz/service/frame_sinks/video_capture/video_capture_overlay.h"
+
+#include <array>
+#include <utility>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/command_line.h"
+#include "base/files/file_path.h"
+#include "base/numerics/safe_conversions.h"
+#include "base/optional.h"
+#include "base/path_service.h"
+#include "base/run_loop.h"
+#include "base/stl_util.h"
+#include "cc/test/pixel_comparator.h"
+#include "cc/test/pixel_test_utils.h"
+#include "components/viz/test/paths.h"
+#include "media/base/video_frame.h"
+#include "media/base/video_types.h"
+#include "media/base/video_util.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "third_party/skia/include/core/SkColor.h"
+#include "third_party/skia/include/core/SkImageInfo.h"
+#include "third_party/skia/include/core/SkPixmap.h"
+#include "ui/gfx/color_space.h"
+#include "ui/gfx/color_transform.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/rect_f.h"
+#include "ui/gfx/geometry/size.h"
+
+using media::VideoFrame;
+using media::VideoPixelFormat;
+
+using testing::_;
+using testing::InvokeWithoutArgs;
+using testing::NiceMock;
+using testing::Return;
+using testing::StrictMock;
+
+namespace viz {
+namespace {
+
+class MockFrameSource : public VideoCaptureOverlay::FrameSource {
+ public:
+ MOCK_METHOD0(GetSourceSize, gfx::Size());
+ MOCK_METHOD1(InvalidateRect, void(const gfx::Rect& rect));
+ MOCK_METHOD0(RequestRefreshFrame, void());
+};
+
+class VideoCaptureOverlayTest : public testing::Test {
+ public:
+ VideoCaptureOverlayTest() = default;
+
+ NiceMock<MockFrameSource>* frame_source() { return &frame_source_; }
+
+ std::unique_ptr<VideoCaptureOverlay> CreateOverlay() {
+ return std::make_unique<VideoCaptureOverlay>(frame_source());
+ }
+
+ void RunUntilIdle() { base::RunLoop().RunUntilIdle(); }
+
+ // Makes a SkBitmap filled with a 50% white background color plus four rects
+ // of four different colors/opacities. |cycle| causes the four rects to rotate
+ // positions (counter-clockwise by N steps).
+ static SkBitmap MakeTestBitmap(int cycle) {
+ constexpr gfx::Size kTestImageSize = gfx::Size(24, 16);
+ // Test colors have been chosen to exercise different opacities,
+ // intensities, and color channels; to confirm all aspects of the "SrcOver"
+ // image blending algorithms are working properly.
+ constexpr SkColor kTestImageBackground =
+ SkColorSetARGB(0xff, 0xff, 0xff, 0xff);
+ constexpr SkColor kTestImageColors[4] = {
+ SkColorSetARGB(0xaa, 0xff, 0x00, 0x00),
+ SkColorSetARGB(0xbb, 0x00, 0xee, 0x00),
+ SkColorSetARGB(0xcc, 0x00, 0x00, 0x77),
+ SkColorSetARGB(0xdd, 0x66, 0x66, 0x00),
+ };
+ constexpr SkIRect kTestImageColorRects[4] = {
+ SkIRect::MakeXYWH(4, 2, 4, 4), SkIRect::MakeXYWH(16, 2, 4, 4),
+ SkIRect::MakeXYWH(4, 10, 4, 4), SkIRect::MakeXYWH(16, 10, 4, 4),
+ };
+
+ SkBitmap result;
+ const SkImageInfo info = SkImageInfo::MakeN32Premul(
+ kTestImageSize.width(), kTestImageSize.height(),
+ GetLinearSRGB().ToSkColorSpace());
+ CHECK(result.tryAllocPixels(info, info.minRowBytes()));
+ result.eraseColor(kTestImageBackground);
+ for (size_t i = 0; i < base::size(kTestImageColors); ++i) {
+ const size_t idx = (i + cycle) % base::size(kTestImageColors);
+ result.erase(kTestImageColors[idx], kTestImageColorRects[i]);
+ }
+
+ return result;
+ }
+
+ // Returns the sRGB color space, but with a linear transfer function.
+ static gfx::ColorSpace GetLinearSRGB() {
+ return gfx::ColorSpace(
+ gfx::ColorSpace::PrimaryID::BT709, gfx::ColorSpace::TransferID::LINEAR,
+ gfx::ColorSpace::MatrixID::RGB, gfx::ColorSpace::RangeID::FULL);
+ }
+
+ // Returns the BT709 color space (YUV), but with a linear transfer function.
+ static gfx::ColorSpace GetLinearRec709() {
+ return gfx::ColorSpace(
+ gfx::ColorSpace::PrimaryID::BT709, gfx::ColorSpace::TransferID::LINEAR,
+ gfx::ColorSpace::MatrixID::BT709, gfx::ColorSpace::RangeID::LIMITED);
+ }
+
+ static constexpr auto kARGBFormat = VideoPixelFormat::PIXEL_FORMAT_ARGB;
+ static constexpr auto kI420Format = VideoPixelFormat::PIXEL_FORMAT_I420;
+
+ private:
+ NiceMock<MockFrameSource> frame_source_;
+
+ DISALLOW_COPY_AND_ASSIGN(VideoCaptureOverlayTest);
+};
+
+// Tests that MakeRenderer() does not make a OnceRenderer until the client has
+// set the image.
+TEST_F(VideoCaptureOverlayTest, DoesNotRenderWithoutImage) {
+ constexpr gfx::Size kSize = gfx::Size(100, 75);
+ EXPECT_CALL(*frame_source(), GetSourceSize()).WillRepeatedly(Return(kSize));
+ std::unique_ptr<VideoCaptureOverlay> overlay = CreateOverlay();
+
+ // The overlay does not have an image yet, so the renderer should be null.
+ constexpr gfx::Rect kRegionInFrame = gfx::Rect(kSize);
+ EXPECT_FALSE(
+ overlay->MakeRenderer(kRegionInFrame, kI420Format, GetLinearRec709()));
+
+ // Once an image is set, the renderer should not be null.
+ overlay->SetImageAndBounds(MakeTestBitmap(1), gfx::RectF(0, 0, 1, 1));
+ EXPECT_TRUE(
+ overlay->MakeRenderer(kRegionInFrame, kI420Format, GetLinearRec709()));
+}
+
+// Tests that MakeRenderer() does not make a OnceRenderer if the bounds are set
+// to something outside the frame's content region.
+TEST_F(VideoCaptureOverlayTest, DoesNotRenderIfCompletelyOutOfBounds) {
+ constexpr gfx::Size kSize = gfx::Size(100, 75);
+ EXPECT_CALL(*frame_source(), GetSourceSize()).WillRepeatedly(Return(kSize));
+ std::unique_ptr<VideoCaptureOverlay> overlay = CreateOverlay();
+
+ // The overlay does not have an image yet, so the renderer should be null.
+ constexpr gfx::Rect kRegionInFrame = gfx::Rect(kSize);
+ EXPECT_FALSE(
+ overlay->MakeRenderer(kRegionInFrame, kI420Format, GetLinearRec709()));
+
+ // Setting an image, but out-of-bounds, should always result in a null
+ // renderer.
+ overlay->SetImageAndBounds(MakeTestBitmap(0), gfx::RectF(-1, -1, 1, 1));
+ EXPECT_FALSE(
+ overlay->MakeRenderer(kRegionInFrame, kI420Format, GetLinearRec709()));
+ overlay->SetBounds(gfx::RectF(1, 1, 1, 1));
+ EXPECT_FALSE(
+ overlay->MakeRenderer(kRegionInFrame, kI420Format, GetLinearRec709()));
+ overlay->SetBounds(gfx::RectF(-1, 1, 1, 1));
+ EXPECT_FALSE(
+ overlay->MakeRenderer(kRegionInFrame, kI420Format, GetLinearRec709()));
+ overlay->SetBounds(gfx::RectF(1, -1, 1, 1));
+ EXPECT_FALSE(
+ overlay->MakeRenderer(kRegionInFrame, kI420Format, GetLinearRec709()));
+}
+
+// Tests that that MakeCombinedRenderer() only makes a OnceRenderer when one or
+// more overlays are set to make visible changes to a video frame.
+TEST_F(VideoCaptureOverlayTest,
+ DoesNotDoCombinedRenderIfNoOverlaysWouldRender) {
+ constexpr gfx::Size kSize = gfx::Size(100, 75);
+ EXPECT_CALL(*frame_source(), GetSourceSize()).WillRepeatedly(Return(kSize));
+ std::vector<std::unique_ptr<VideoCaptureOverlay>> overlays;
+ overlays.emplace_back(CreateOverlay());
+ overlays.emplace_back(CreateOverlay());
+
+ // Neither overlay has an image yet, so the combined renderer should be null.
+ constexpr gfx::Rect kRegionInFrame = gfx::Rect(kSize);
+ EXPECT_FALSE(VideoCaptureOverlay::MakeCombinedRenderer(
+ overlays, kRegionInFrame, kI420Format, GetLinearRec709()));
+
+ // If just the first overlay renders, the combined renderer should not be
+ // null.
+ overlays[0]->SetImageAndBounds(MakeTestBitmap(0), gfx::RectF(0, 0, 1, 1));
+ EXPECT_TRUE(VideoCaptureOverlay::MakeCombinedRenderer(
+ overlays, kRegionInFrame, kI420Format, GetLinearRec709()));
+
+ // If both overlays render, the combined renderer should not be null.
+ overlays[1]->SetImageAndBounds(MakeTestBitmap(1), gfx::RectF(0, 0, 1, 1));
+ EXPECT_TRUE(VideoCaptureOverlay::MakeCombinedRenderer(
+ overlays, kRegionInFrame, kI420Format, GetLinearRec709()));
+
+ // If only the second overlay renders, because the first is hidden, the
+ // combined renderer should not be null.
+ overlays[0]->SetBounds(gfx::RectF());
+ EXPECT_TRUE(VideoCaptureOverlay::MakeCombinedRenderer(
+ overlays, kRegionInFrame, kI420Format, GetLinearRec709()));
+
+ // Both overlays are hidden, so the combined renderer should be null.
+ overlays[1]->SetBounds(gfx::RectF());
+ EXPECT_FALSE(VideoCaptureOverlay::MakeCombinedRenderer(
+ overlays, kRegionInFrame, kI420Format, GetLinearRec709()));
+}
+
+class VideoCaptureOverlayRenderTest
+ : public VideoCaptureOverlayTest,
+ public testing::WithParamInterface<VideoPixelFormat> {
+ public:
+ VideoCaptureOverlayRenderTest()
+ : trace_(__FILE__, __LINE__, VideoPixelFormatToString(pixel_format())) {}
+
+ VideoPixelFormat pixel_format() const { return GetParam(); }
+
+ bool is_argb_test() const {
+ return pixel_format() == media::PIXEL_FORMAT_ARGB;
+ }
+
+ gfx::ColorSpace GetColorSpace() const {
+ // For these tests, we use linear RGB and YUV color spaces. This is because
+ // VideoCaptureOverlay does not account for non-linear color spaces when
+ // blending. See class notes.
+ return is_argb_test() ? GetLinearSRGB() : GetLinearRec709();
+ }
+
+ scoped_refptr<VideoFrame> CreateVideoFrame(const gfx::Size& size) const {
+ auto frame = VideoFrame::CreateFrame(pixel_format(), size, gfx::Rect(size),
+ size, base::TimeDelta());
+
+ // Fill the video frame with black. For ARGB tests, also set alpha channel
+ // to 1.0. This allows the expected results of the ARGB tests to be the same
+ // as those of the YUV tests, and so only one set of golden files needs to
+ // be used.
+ if (is_argb_test()) {
+ uint8_t* dst = frame->visible_data(VideoFrame::kARGBPlane);
+ const int stride = frame->stride(VideoFrame::kARGBPlane);
+ for (int row = 0; row < size.height(); ++row, dst += stride) {
+ uint32_t* const begin = reinterpret_cast<uint32_t*>(dst);
+ std::fill(begin, begin + size.width(), UINT32_C(0xff000000));
+ }
+ } else /* if (!is_argb_test()) */ {
+ media::FillYUV(frame.get(), 0x00, 0x80, 0x80);
+ }
+
+ frame->set_color_space(GetColorSpace());
+ return frame;
+ }
+
+ bool FrameMatchesPNG(const VideoFrame& frame, const char* golden_file) {
+ const gfx::ColorSpace png_color_space = GetLinearSRGB();
+ // Note: Using kUnpremul_SkAlphaType since that is the semantics of
+ // PIXEL_FORMAT_ARGB, and converting to kPremul_SkAlphaType before producing
+ // the PNG would lose precision for no good reason.
+ const SkImageInfo canonical_format = SkImageInfo::Make(
+ frame.visible_rect().width(), frame.visible_rect().height(),
+ kN32_SkColorType, kUnpremul_SkAlphaType,
+ png_color_space.ToSkColorSpace());
+ SkBitmap canonical_bitmap;
+ CHECK(canonical_bitmap.tryAllocPixels(canonical_format, 0));
+
+ // Populate |canonical_bitmap| with data from the frame. For I420, use
+ // gfx::ColorTransform to map back from YUV→RGB.
+ switch (frame.format()) {
+ case media::PIXEL_FORMAT_ARGB: {
+ // Map from the video frame's ARGB format to the canonical
+ // representation.
+ const SkImageInfo frame_format = SkImageInfo::Make(
+ frame.visible_rect().width(), frame.visible_rect().height(),
+ kBGRA_8888_SkColorType, kUnpremul_SkAlphaType,
+ frame.ColorSpace().ToSkColorSpace());
+ canonical_bitmap.writePixels(
+ SkPixmap(frame_format, frame.visible_data(VideoFrame::kARGBPlane),
+ frame.stride(VideoFrame::kARGBPlane)),
+ 0, 0);
+ break;
+ }
+
+ case media::PIXEL_FORMAT_I420: {
+ // Map from I420 planar [0,255] (of which only [16,235] is used) values
+ // to interleaved [0.0,1.0] values.
+ const gfx::Size& size = frame.visible_rect().size();
+ std::unique_ptr<gfx::ColorTransform::TriStim[]> colors(
+ new gfx::ColorTransform::TriStim[size.GetArea()]);
+ int pos = 0;
+ for (int row = 0; row < size.height(); ++row) {
+ const uint8_t* y = frame.visible_data(VideoFrame::kYPlane) +
+ (row * frame.stride(VideoFrame::kYPlane));
+ const uint8_t* u = frame.visible_data(VideoFrame::kUPlane) +
+ ((row / 2) * frame.stride(VideoFrame::kUPlane));
+ const uint8_t* v = frame.visible_data(VideoFrame::kVPlane) +
+ ((row / 2) * frame.stride(VideoFrame::kVPlane));
+ for (int col = 0; col < size.width(); ++col) {
+ colors[pos].SetPoint(y[col] / 255.0f, u[col / 2] / 255.0f,
+ v[col / 2] / 255.0f);
+ ++pos;
+ }
+ }
+
+ // Execute the YUV→RGB conversion.
+ gfx::ColorTransform::NewColorTransform(
+ frame.ColorSpace(), png_color_space,
+ gfx::ColorTransform::Intent::INTENT_ABSOLUTE)
+ ->Transform(colors.get(), size.GetArea());
+
+ // Map back from interleaved [0.0,1.0] values to intervealed ARGB,
+ // setting alpha=100%.
+ const auto ToClamped255 = [](float value) -> uint32_t {
+ value = (value * 255.0f) + 0.5f /* rounding */;
+ return base::saturated_cast<uint8_t>(value);
+ };
+ pos = 0;
+ for (int row = 0; row < size.height(); ++row) {
+ uint32_t* out = canonical_bitmap.getAddr32(0, row);
+ for (int col = 0; col < size.width(); ++col) {
+ out[col] = ((UINT32_C(255) << SK_A32_SHIFT) |
+ (ToClamped255(colors[pos].x()) << SK_R32_SHIFT) |
+ (ToClamped255(colors[pos].y()) << SK_G32_SHIFT) |
+ (ToClamped255(colors[pos].z()) << SK_B32_SHIFT));
+ ++pos;
+ }
+ }
+
+ break;
+ }
+
+ default:
+ NOTREACHED();
+ return false;
+ }
+
+ // Determine the full path to the golden file to compare the results.
+ base::FilePath golden_file_path;
+ base::PathService::Get(Paths::DIR_TEST_DATA, &golden_file_path);
+ golden_file_path =
+ golden_file_path.Append(FILE_PATH_LITERAL("video_capture"))
+ .Append(base::FilePath::FromUTF8Unsafe(golden_file));
+
+ // If the very-specific command-line switch is present, rewrite the golden
+ // file. This is only done when the ARGB test runs, for the reasons outlined
+ // in the comments below (regarding FuzzyPixelComparator).
+ if (is_argb_test() &&
+ base::CommandLine::ForCurrentProcess()->HasSwitch(
+ "video-overlay-capture-test-update-golden-files")) {
+ LOG(INFO) << "Rewriting golden file: " << golden_file_path.AsUTF8Unsafe();
+ cc::WritePNGFile(canonical_bitmap, golden_file_path, false);
+ }
+
+ // FuzzyPixelComparator configuration: Allow 100% of pixels to mismatch, but
+ // no single pixel component should be different by more than 1/255 (64/255
+ // for YUV tests), and the absolute average error must not exceed 1/255
+ // (16/255 for YUV tests). The YUV tests allow for more error due to the
+ // expected errors introduced by both color space (dynamic range) and format
+ // (chroma subsampling) conversion.
+ cc::FuzzyPixelComparator comparator(false, 100.0f, 0.0f,
+ is_argb_test() ? 1.0f : 16.0f,
+ is_argb_test() ? 1 : 64, 0);
+ const bool matches_golden_file =
+ cc::MatchesPNGFile(canonical_bitmap, golden_file_path, comparator);
+ // If MatchesPNGFile() returned false, it will have LOG(ERROR)'ed the
+ // expected versus actual PNG data URLs. So, only do the VLOG(1)'s when
+ // MatchesPNGFile() returned true.
+ if (matches_golden_file && VLOG_IS_ON(1)) {
+ SkBitmap expected;
+ if (cc::ReadPNGFile(golden_file_path, &expected)) {
+ VLOG(1) << "Expected bitmap: " << cc::GetPNGDataUrl(expected);
+ }
+ VLOG(1) << "Actual bitmap: " << cc::GetPNGDataUrl(canonical_bitmap);
+ }
+ return matches_golden_file;
+ }
+
+ // The size of the compositor frame sink's Surface.
+ static constexpr gfx::Size kSourceSize = gfx::Size(96, 40);
+
+ private:
+ testing::ScopedTrace trace_;
+
+ DISALLOW_COPY_AND_ASSIGN(VideoCaptureOverlayRenderTest);
+};
+
+// static
+constexpr gfx::Size VideoCaptureOverlayRenderTest::kSourceSize;
+
+// Basic test: Render an overlay image that covers the entire video frame and is
+// not scaled.
+TEST_P(VideoCaptureOverlayRenderTest, FullCover_NoScaling) {
+ StrictMock<MockFrameSource> frame_source;
+ VideoCaptureOverlay overlay(&frame_source);
+
+ EXPECT_CALL(frame_source, GetSourceSize())
+ .WillRepeatedly(Return(kSourceSize));
+ EXPECT_CALL(frame_source, InvalidateRect(gfx::Rect())).RetiresOnSaturation();
+ EXPECT_CALL(frame_source, InvalidateRect(gfx::Rect(kSourceSize)))
+ .RetiresOnSaturation();
+ EXPECT_CALL(frame_source, RequestRefreshFrame());
+
+ const SkBitmap test_bitmap = MakeTestBitmap(0);
+ overlay.SetImageAndBounds(test_bitmap, gfx::RectF(0, 0, 1, 1));
+ const gfx::Size output_size(test_bitmap.width(), test_bitmap.height());
+ VideoCaptureOverlay::OnceRenderer renderer = overlay.MakeRenderer(
+ gfx::Rect(output_size), pixel_format(), GetColorSpace());
+ ASSERT_TRUE(renderer);
+ auto frame = CreateVideoFrame(output_size);
+ std::move(renderer).Run(frame.get());
+ EXPECT_TRUE(FrameMatchesPNG(*frame, "overlay_full_cover.png"));
+}
+
+// Basic test: Render an overlay image that covers the entire video frame and is
+// scaled.
+TEST_P(VideoCaptureOverlayRenderTest, FullCover_WithScaling) {
+ StrictMock<MockFrameSource> frame_source;
+ VideoCaptureOverlay overlay(&frame_source);
+
+ EXPECT_CALL(frame_source, GetSourceSize())
+ .WillRepeatedly(Return(kSourceSize));
+ EXPECT_CALL(frame_source, InvalidateRect(gfx::Rect())).RetiresOnSaturation();
+ EXPECT_CALL(frame_source, InvalidateRect(gfx::Rect(kSourceSize)))
+ .RetiresOnSaturation();
+ EXPECT_CALL(frame_source, RequestRefreshFrame());
+
+ const SkBitmap test_bitmap = MakeTestBitmap(0);
+ overlay.SetImageAndBounds(test_bitmap, gfx::RectF(0, 0, 1, 1));
+ const gfx::Size output_size(test_bitmap.width() * 4,
+ test_bitmap.height() * 4);
+ VideoCaptureOverlay::OnceRenderer renderer = overlay.MakeRenderer(
+ gfx::Rect(output_size), pixel_format(), GetColorSpace());
+ ASSERT_TRUE(renderer);
+ auto frame = CreateVideoFrame(output_size);
+ std::move(renderer).Run(frame.get());
+ EXPECT_TRUE(FrameMatchesPNG(*frame, "overlay_full_cover_scaled.png"));
+}
+
+// Tests that changing the position of the overlay results in it being rendered
+// at different locations in the video frame.
+TEST_P(VideoCaptureOverlayRenderTest, MovesAround) {
+ NiceMock<MockFrameSource> frame_source;
+ EXPECT_CALL(frame_source, GetSourceSize())
+ .WillRepeatedly(Return(kSourceSize));
+ VideoCaptureOverlay overlay(&frame_source);
+
+ const SkBitmap test_bitmap = MakeTestBitmap(0);
+ const gfx::Size frame_size(test_bitmap.width() * 4, test_bitmap.height() * 4);
+
+ const gfx::RectF relative_image_bounds[6] = {
+ gfx::RectF(0.0f, 0.0f, 0.5f, 0.5f),
+ gfx::RectF(1.0f / frame_size.width(), 0.0f, 0.5f, 0.5f),
+ gfx::RectF(2.0f / frame_size.width(), 0.0f, 0.5f, 0.5f),
+ gfx::RectF(2.0f / frame_size.width(), 1.0f / frame_size.height(), 0.5f,
+ 0.5f),
+ gfx::RectF(2.0f / frame_size.width(), 2.0f / frame_size.height(), 0.5f,
+ 0.5f),
+ gfx::RectF(0.5f, 0.5f, 0.5f, 0.5f),
+ };
+
+ VideoCaptureOverlay::OnceRenderer renderers[6];
+ for (int i = 0; i < 6; ++i) {
+ if (i == 0) {
+ overlay.SetImageAndBounds(test_bitmap, relative_image_bounds[i]);
+ } else {
+ overlay.SetBounds(relative_image_bounds[i]);
+ }
+ renderers[i] = overlay.MakeRenderer(gfx::Rect(frame_size), pixel_format(),
+ GetColorSpace());
+ }
+
+ constexpr std::array<const char*, 6> kGoldenFiles = {
+ "overlay_moves_0_0.png", "overlay_moves_1_0.png", "overlay_moves_2_0.png",
+ "overlay_moves_2_1.png", "overlay_moves_2_2.png", "overlay_moves_lr.png",
+ };
+
+ for (int i = 0; i < 6; ++i) {
+ SCOPED_TRACE(testing::Message() << "relative_image_bounds="
+ << relative_image_bounds[i].ToString()
+ << ", frame_size=" << frame_size.ToString()
+ << ", golden_file=" << kGoldenFiles[i]);
+ auto frame = CreateVideoFrame(frame_size);
+ std::move(renderers[i]).Run(frame.get());
+ EXPECT_TRUE(FrameMatchesPNG(*frame, kGoldenFiles[i]));
+ }
+}
+
+// Tests that the overlay will be partially rendered (clipped) when any part of
+// it extends outside the video frame's content region.
+//
+// For this test, the content region is a rectangle, centered within the frame
+// (e.g., the content is being letterboxed), and the test attempts to locate the
+// overlay such that part of it should be clipped. The test succeeds if the
+// overlay is clipped to the content region in the center. For example:
+//
+// +-------------------------------+
+// | |
+// | ...... |
+// | ..****//////////// | **** the drawn part of the overlay
+// | ..****CONTENT///// |
+// | /////REGION///// | .... the clipped part of the overlay
+// | //////////////// | (i.e., not drawn)
+// | |
+// | |
+// +-------------------------------+
+TEST_P(VideoCaptureOverlayRenderTest, ClipsToContentBounds) {
+ NiceMock<MockFrameSource> frame_source;
+ EXPECT_CALL(frame_source, GetSourceSize())
+ .WillRepeatedly(Return(kSourceSize));
+ VideoCaptureOverlay overlay(&frame_source);
+
+ const SkBitmap test_bitmap = MakeTestBitmap(0);
+ const gfx::Size frame_size(test_bitmap.width() * 4, test_bitmap.height() * 4);
+ const gfx::Rect region_in_frame(test_bitmap.width(), test_bitmap.height(),
+ test_bitmap.width() * 2,
+ test_bitmap.height() * 2);
+
+ const gfx::RectF relative_image_bounds[4] = {
+ gfx::RectF(-0.25f, -0.25f, 0.5f, 0.5f),
+ gfx::RectF(0.75f, -0.25f, 0.5f, 0.5f),
+ gfx::RectF(0.75f, 0.75f, 0.5f, 0.5f),
+ gfx::RectF(-0.25f, 0.75f, 0.5f, 0.5f),
+ };
+
+ VideoCaptureOverlay::OnceRenderer renderers[4];
+ for (int i = 0; i < 4; ++i) {
+ if (i == 0) {
+ overlay.SetImageAndBounds(test_bitmap, relative_image_bounds[i]);
+ } else {
+ overlay.SetBounds(relative_image_bounds[i]);
+ }
+ renderers[i] =
+ overlay.MakeRenderer(region_in_frame, pixel_format(), GetColorSpace());
+ }
+
+ constexpr std::array<const char*, 4> kGoldenFiles = {
+ "overlay_clips_ul.png", "overlay_clips_ur.png", "overlay_clips_lr.png",
+ "overlay_clips_ll.png",
+ };
+
+ for (int i = 0; i < 4; ++i) {
+ auto frame = CreateVideoFrame(frame_size);
+ std::move(renderers[i]).Run(frame.get());
+ EXPECT_TRUE(FrameMatchesPNG(*frame, kGoldenFiles[i]));
+ }
+}
+
+INSTANTIATE_TEST_CASE_P(
+ ,
+ VideoCaptureOverlayRenderTest,
+ testing::Values(VideoCaptureOverlayRenderTest::kARGBFormat,
+ VideoCaptureOverlayRenderTest::kI420Format));
+
+} // namespace
+} // namespace viz
diff --git a/components/viz/test/data/video_capture/overlay_clips_ll.png b/components/viz/test/data/video_capture/overlay_clips_ll.png
new file mode 100644
index 0000000..7faf623c
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_clips_ll.png
Binary files differ
diff --git a/components/viz/test/data/video_capture/overlay_clips_lr.png b/components/viz/test/data/video_capture/overlay_clips_lr.png
new file mode 100644
index 0000000..1eaf92c
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_clips_lr.png
Binary files differ
diff --git a/components/viz/test/data/video_capture/overlay_clips_ul.png b/components/viz/test/data/video_capture/overlay_clips_ul.png
new file mode 100644
index 0000000..f89dc43
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_clips_ul.png
Binary files differ
diff --git a/components/viz/test/data/video_capture/overlay_clips_ur.png b/components/viz/test/data/video_capture/overlay_clips_ur.png
new file mode 100644
index 0000000..416b1c59
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_clips_ur.png
Binary files differ
diff --git a/components/viz/test/data/video_capture/overlay_full_cover.png b/components/viz/test/data/video_capture/overlay_full_cover.png
new file mode 100644
index 0000000..b2aacf7c
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_full_cover.png
Binary files differ
diff --git a/components/viz/test/data/video_capture/overlay_full_cover_scaled.png b/components/viz/test/data/video_capture/overlay_full_cover_scaled.png
new file mode 100644
index 0000000..2485016
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_full_cover_scaled.png
Binary files differ
diff --git a/components/viz/test/data/video_capture/overlay_moves_0_0.png b/components/viz/test/data/video_capture/overlay_moves_0_0.png
new file mode 100644
index 0000000..f2efc15
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_moves_0_0.png
Binary files differ
diff --git a/components/viz/test/data/video_capture/overlay_moves_1_0.png b/components/viz/test/data/video_capture/overlay_moves_1_0.png
new file mode 100644
index 0000000..0bf0a55
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_moves_1_0.png
Binary files differ
diff --git a/components/viz/test/data/video_capture/overlay_moves_2_0.png b/components/viz/test/data/video_capture/overlay_moves_2_0.png
new file mode 100644
index 0000000..62668ce
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_moves_2_0.png
Binary files differ
diff --git a/components/viz/test/data/video_capture/overlay_moves_2_1.png b/components/viz/test/data/video_capture/overlay_moves_2_1.png
new file mode 100644
index 0000000..aa26331
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_moves_2_1.png
Binary files differ
diff --git a/components/viz/test/data/video_capture/overlay_moves_2_2.png b/components/viz/test/data/video_capture/overlay_moves_2_2.png
new file mode 100644
index 0000000..04b9a350
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_moves_2_2.png
Binary files differ
diff --git a/components/viz/test/data/video_capture/overlay_moves_lr.png b/components/viz/test/data/video_capture/overlay_moves_lr.png
new file mode 100644
index 0000000..352b6e4a
--- /dev/null
+++ b/components/viz/test/data/video_capture/overlay_moves_lr.png
Binary files differ