Commit 89733b60 authored by Yuri Wiitala's avatar Yuri Wiitala Committed by Commit Bot

Refactor many components of WCVCDBrowserTest for re-use.

Moves/Refactors a lot of test infrastructure that can be re-used in an
upcoming change to also test the new browser window capture impl:

ContentCaptureDeviceBrowserTestBase: A common base class that sets up a
content shell navigated to a test page, whose content can also be
changed as test procedures require.

FakeVideoCaptureStack: A simple representation of the entire downstream
video capture stack that just takes the screen-captured video frames an
stores them in a queue for later examination by the tests.

FrameTestUtil: Math/Color utilities for analyzing the content in the
captured video frames.

Bug: 806366
Change-Id: I899db11043944ea5a1206a58b2593a317222fdab
Reviewed-on: https://chromium-review.googlesource.com/1006072
Commit-Queue: Yuri Wiitala <miu@chromium.org>
Reviewed-by: default avatarXiangjun Zhang <xjz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#558034}
parent b137b49a
// 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 "content/browser/media/capture/content_capture_device_browsertest_base.h"
#include <cmath>
#include <utility>
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "content/browser/media/capture/frame_sink_video_capture_device.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using net::test_server::BasicHttpResponse;
using net::test_server::HttpRequest;
using net::test_server::HttpResponse;
namespace content {
ContentCaptureDeviceBrowserTestBase::ContentCaptureDeviceBrowserTestBase() =
default;
ContentCaptureDeviceBrowserTestBase::~ContentCaptureDeviceBrowserTestBase() =
default;
void ContentCaptureDeviceBrowserTestBase::ChangePageContentColor(
std::string css_color_hex) {
// See the HandleRequest() method for the original documents being modified
// here.
std::string script;
if (IsCrossSiteCaptureTest()) {
const GURL& inner_frame_url =
embedded_test_server()->GetURL(kInnerFrameHostname, kInnerFramePath);
script = base::StringPrintf(
"document.getElementsByTagName('iframe')[0].src = '%s?color=123456';",
inner_frame_url.spec().c_str());
} else {
script = "document.body.style.backgroundColor = '#123456';";
}
script.replace(script.find("123456"), 6, css_color_hex);
CHECK(ExecuteScript(shell()->web_contents(), script));
}
gfx::Size ContentCaptureDeviceBrowserTestBase::GetExpectedSourceSize() {
const gfx::Size source_size = GetCapturedSourceSize();
if (expected_source_size_) {
EXPECT_EQ(*expected_source_size_, source_size)
<< "Sanity-check failed: Source size changed during this test.";
} else {
expected_source_size_.emplace(source_size);
VLOG(1) << "Captured source size is " << expected_source_size_->ToString();
}
return source_size;
}
media::VideoCaptureParams
ContentCaptureDeviceBrowserTestBase::SnapshotCaptureParams() {
constexpr gfx::Size kMaxCaptureSize = gfx::Size(320, 320);
constexpr int kMaxFramesPerSecond = 60;
gfx::Size capture_size = kMaxCaptureSize;
if (IsFixedAspectRatioTest()) {
// Half either the width or height, depending on the source size. The goal
// is to force obvious letterboxing (or pillarboxing), regardless of how the
// source is currently sized/oriented.
const gfx::Size source_size = GetExpectedSourceSize();
if (source_size.width() < source_size.height()) {
capture_size.set_height(capture_size.height() / 2);
} else {
capture_size.set_width(capture_size.width() / 2);
}
}
media::VideoCaptureParams params;
params.requested_format = media::VideoCaptureFormat(
capture_size, kMaxFramesPerSecond, media::PIXEL_FORMAT_I420);
params.resolution_change_policy =
IsFixedAspectRatioTest()
? media::ResolutionChangePolicy::FIXED_ASPECT_RATIO
: media::ResolutionChangePolicy::ANY_WITHIN_LIMIT;
return params;
}
base::TimeDelta ContentCaptureDeviceBrowserTestBase::GetMinCapturePeriod() {
return base::TimeDelta::FromMicroseconds(
base::Time::kMicrosecondsPerSecond /
device_->capture_params().requested_format.frame_rate);
}
void ContentCaptureDeviceBrowserTestBase::NavigateToInitialDocument() {
// If doing a cross-site capture test, navigate to the more-complex document
// that also contains an iframe (rendered in a separate process). Otherwise,
// navigate to the simpler document.
if (IsCrossSiteCaptureTest()) {
ASSERT_TRUE(NavigateToURL(
shell(),
embedded_test_server()->GetURL(kOuterFrameHostname, kOuterFramePath)));
ASSERT_TRUE(WaitForLoadStop(shell()->web_contents()));
// Confirm the iframe is a cross-process child render frame.
auto* const child_frame =
ChildFrameAt(shell()->web_contents()->GetMainFrame(), 0);
ASSERT_TRUE(child_frame);
ASSERT_TRUE(child_frame->IsCrossProcessSubframe());
} else {
ASSERT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(
kSingleFrameHostname, kSingleFramePath)));
ASSERT_TRUE(WaitForLoadStop(shell()->web_contents()));
}
}
void ContentCaptureDeviceBrowserTestBase::
AllocateAndStartAndWaitForFirstFrame() {
capture_stack()->Reset();
device_ = CreateDevice();
device_->AllocateAndStartWithReceiver(SnapshotCaptureParams(),
capture_stack()->CreateFrameReceiver());
RunUntilIdle();
EXPECT_TRUE(capture_stack()->started());
EXPECT_FALSE(capture_stack()->error_occurred());
capture_stack()->ExpectNoLogMessages();
WaitForFirstFrame();
}
void ContentCaptureDeviceBrowserTestBase::StopAndDeAllocate() {
device_->StopAndDeAllocate();
RunUntilIdle();
device_.reset();
}
void ContentCaptureDeviceBrowserTestBase::RunUntilIdle() {
base::RunLoop().RunUntilIdle();
}
bool ContentCaptureDeviceBrowserTestBase::IsSoftwareCompositingTest() const {
return false;
}
bool ContentCaptureDeviceBrowserTestBase::IsFixedAspectRatioTest() const {
return false;
}
bool ContentCaptureDeviceBrowserTestBase::IsCrossSiteCaptureTest() const {
return false;
}
void ContentCaptureDeviceBrowserTestBase::SetUp() {
// IMPORTANT: Do not add the switches::kUseGpuInTests command line flag: It
// causes the tests to take 12+ seconds just to spin up a render process on
// debug builds. It can also cause test failures in MSAN builds, or exacerbate
// OOM situations on highly-loaded machines.
// Screen capture requires readback from compositor output.
EnablePixelOutput();
// Conditionally force software compositing instead of GPU-accelerated
// compositing.
if (IsSoftwareCompositingTest()) {
UseSoftwareCompositing();
}
ContentBrowserTest::SetUp();
}
void ContentCaptureDeviceBrowserTestBase::SetUpCommandLine(
base::CommandLine* command_line) {
IsolateAllSitesForTesting(command_line);
}
void ContentCaptureDeviceBrowserTestBase::SetUpOnMainThread() {
ContentBrowserTest::SetUpOnMainThread();
// Set-up and start the embedded test HTTP server.
host_resolver()->AddRule("*", "127.0.0.1");
embedded_test_server()->RegisterRequestHandler(
base::BindRepeating(&ContentCaptureDeviceBrowserTestBase::HandleRequest,
base::Unretained(this)));
ASSERT_TRUE(embedded_test_server()->Start());
}
void ContentCaptureDeviceBrowserTestBase::TearDownOnMainThread() {
ASSERT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
ClearCapturedFramesQueue();
// Run any left-over tasks (usually these are delete-soon's and orphaned
// tasks).
RunUntilIdle();
ContentBrowserTest::TearDownOnMainThread();
}
std::unique_ptr<HttpResponse>
ContentCaptureDeviceBrowserTestBase::HandleRequest(const HttpRequest& request) {
auto response = std::make_unique<BasicHttpResponse>();
response->set_content_type("text/html");
const GURL& url = request.GetURL();
if (url.path() == kOuterFramePath) {
// A page with a solid white fill color, but containing an iframe in its
// upper-left quadrant.
const GURL& inner_frame_url =
embedded_test_server()->GetURL(kInnerFrameHostname, kInnerFramePath);
response->set_content(base::StringPrintf(
"<!doctype html>"
"<body style='background-color: #ffffff;'>"
"<iframe src='%s' style='position:absolute; "
"top:0px; left:0px; margin:none; padding:none; border:none;'>"
"</iframe>"
"<script>"
"window.addEventListener('load', () => {"
" const iframe = document.getElementsByTagName('iframe')[0];"
" iframe.width = document.documentElement.clientWidth / 2;"
" iframe.height = document.documentElement.clientHeight / 2;"
"});"
"</script>"
"</body>",
inner_frame_url.spec().c_str()));
} else {
// A page whose solid fill color is based on a query parameter, or
// defaults to black.
const std::string& query = url.query();
std::string color = "#000000";
const auto pos = query.find("color=");
if (pos != std::string::npos) {
color = "#" + query.substr(pos + 6, 6);
}
response->set_content(
base::StringPrintf("<!doctype html>"
"<body style='background-color: %s;'></body>",
color.c_str()));
}
return std::move(response);
}
// static
constexpr char ContentCaptureDeviceBrowserTestBase::kInnerFrameHostname[];
// static
constexpr char ContentCaptureDeviceBrowserTestBase::kInnerFramePath[];
// static
constexpr char ContentCaptureDeviceBrowserTestBase::kOuterFrameHostname[];
// static
constexpr char ContentCaptureDeviceBrowserTestBase::kOuterFramePath[];
// static
constexpr char ContentCaptureDeviceBrowserTestBase::kSingleFrameHostname[];
// static
constexpr char ContentCaptureDeviceBrowserTestBase::kSingleFramePath[];
} // namespace content
// 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 CONTENT_BROWSER_MEDIA_CAPTURE_CONTENT_CAPTURE_DEVICE_BROWSERTEST_BASE_H_
#define CONTENT_BROWSER_MEDIA_CAPTURE_CONTENT_CAPTURE_DEVICE_BROWSERTEST_BASE_H_
#include <memory>
#include <string>
#include "base/macros.h"
#include "base/optional.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/media/capture/fake_video_capture_stack.h"
#include "content/public/test/content_browser_test.h"
#include "media/capture/video_capture_types.h"
#include "ui/gfx/geometry/size.h"
namespace net {
namespace test_server {
struct HttpRequest;
class HttpResponse;
} // namespace test_server
} // namespace net
namespace content {
class FrameSinkVideoCaptureDevice;
// Common base class for screen capture browser tests. Since this is a
// ContentBrowserTest, it assumes the test environment consists of a content
// shell and a single WebContents.
class ContentCaptureDeviceBrowserTestBase : public ContentBrowserTest {
public:
ContentCaptureDeviceBrowserTestBase();
~ContentCaptureDeviceBrowserTestBase() override;
FakeVideoCaptureStack* capture_stack() { return &capture_stack_; }
FrameSinkVideoCaptureDevice* device() const { return device_.get(); }
// Alters the solid fill color making up the page content. This will trigger a
// compositor update, which will trigger a frame capture.
void ChangePageContentColor(std::string css_color_hex);
// Returns the captured source size, but also sanity-checks that it is not
// changing during the test. Prefer to use this method instead of
// GetCapturedSourceSize() to improve test stability.
gfx::Size GetExpectedSourceSize();
// Returns capture parameters based on the captured source size.
media::VideoCaptureParams SnapshotCaptureParams();
// Returns the actual minimum capture period the device is using. This should
// not be called until after AllocateAndStartAndWaitForFirstFrame().
base::TimeDelta GetMinCapturePeriod();
// Navigates to the initial document, according to the current test
// parameters, and waits for page load completion. All test fixtures should
// call this before any of the other methods.
void NavigateToInitialDocument();
// Creates and starts the device for frame capture, and checks that the
// initial refresh frame is delivered.
void AllocateAndStartAndWaitForFirstFrame();
// Stops and destroys the device.
void StopAndDeAllocate();
// Runs the message loop until idle.
void RunUntilIdle();
void ClearCapturedFramesQueue() { capture_stack_.ClearCapturedFramesQueue(); }
bool HasCapturedFramesInQueue() const {
return capture_stack_.has_captured_frames();
}
protected:
// These all return false, but can be overridden for parameterized tests to
// change the behavior of this base class.
virtual bool IsSoftwareCompositingTest() const;
virtual bool IsFixedAspectRatioTest() const;
virtual bool IsCrossSiteCaptureTest() const;
// Returns the size of the original content (i.e., not including any
// stretching/scaling being done to fit it within a video frame).
virtual gfx::Size GetCapturedSourceSize() const = 0;
// Returns a new FrameSinkVideoCaptureDevice instance.
virtual std::unique_ptr<FrameSinkVideoCaptureDevice> CreateDevice() = 0;
// Called to wait for the first frame with expected content.
virtual void WaitForFirstFrame() = 0;
// ContentBrowserTest overrides to enable pixel output and set-up/tear-down
// the embedded HTTP server that provides test content.
void SetUp() override;
void SetUpCommandLine(base::CommandLine* command_line) override;
void SetUpOnMainThread() override;
void TearDownOnMainThread() override;
private:
// Called by the embedded test HTTP server to provide the document resources.
std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
const net::test_server::HttpRequest& request);
FakeVideoCaptureStack capture_stack_;
base::Optional<gfx::Size> expected_source_size_;
std::unique_ptr<FrameSinkVideoCaptureDevice> device_;
// Arbitrary string constants used to refer to each document by
// host+path. Note that the "inner frame" and "outer frame" must have
// different hostnames to engage the cross-site process isolation logic in the
// browser.
static constexpr char kInnerFrameHostname[] = "innerframe.com";
static constexpr char kInnerFramePath[] = "/inner.html";
static constexpr char kOuterFrameHostname[] = "outerframe.com";
static constexpr char kOuterFramePath[] = "/outer.html";
static constexpr char kSingleFrameHostname[] = "singleframe.com";
static constexpr char kSingleFramePath[] = "/single.html";
DISALLOW_COPY_AND_ASSIGN(ContentCaptureDeviceBrowserTestBase);
};
} // namespace content
#endif // CONTENT_BROWSER_MEDIA_CAPTURE_CONTENT_CAPTURE_DEVICE_BROWSERTEST_BASE_H_
// 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 "content/browser/media/capture/fake_video_capture_stack.h"
#include <stdint.h>
#include <utility>
#include "base/bind_helpers.h"
#include "media/base/video_frame.h"
#include "media/capture/video/video_frame_receiver.h"
#include "media/capture/video_capture_types.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/libyuv/include/libyuv.h"
#include "ui/gfx/geometry/rect.h"
namespace content {
FakeVideoCaptureStack::FakeVideoCaptureStack() = default;
FakeVideoCaptureStack::~FakeVideoCaptureStack() = default;
void FakeVideoCaptureStack::Reset() {
frames_.clear();
last_frame_timestamp_ = base::TimeDelta::Min();
}
class FakeVideoCaptureStack::Receiver : public media::VideoFrameReceiver {
public:
explicit Receiver(FakeVideoCaptureStack* capture_stack)
: capture_stack_(capture_stack) {}
~Receiver() final = default;
private:
using Buffer = media::VideoCaptureDevice::Client::Buffer;
void OnNewBuffer(int buffer_id,
media::mojom::VideoBufferHandlePtr buffer_handle) final {
buffers_[buffer_id] = std::move(buffer_handle);
}
void OnFrameReadyInBuffer(
int buffer_id,
int frame_feedback_id,
std::unique_ptr<Buffer::ScopedAccessPermission> access,
media::mojom::VideoFrameInfoPtr frame_info) final {
const auto it = buffers_.find(buffer_id);
CHECK(it != buffers_.end());
CHECK(it->second->is_shared_buffer_handle());
mojo::ScopedSharedBufferHandle& buffer =
it->second->get_shared_buffer_handle();
const size_t mapped_size =
media::VideoCaptureFormat(frame_info->coded_size, 0.0f,
frame_info->pixel_format)
.ImageAllocationSize();
mojo::ScopedSharedBufferMapping mapping = buffer->Map(mapped_size);
CHECK(mapping.get());
auto frame = media::VideoFrame::WrapExternalData(
frame_info->pixel_format, frame_info->coded_size,
frame_info->visible_rect, frame_info->visible_rect.size(),
reinterpret_cast<uint8_t*>(mapping.get()), mapped_size,
frame_info->timestamp);
CHECK(frame);
frame->metadata()->MergeInternalValuesFrom(frame_info->metadata);
// This destruction observer will unmap the shared memory when the
// VideoFrame goes out-of-scope.
frame->AddDestructionObserver(
base::BindOnce(base::DoNothing::Once<mojo::ScopedSharedBufferMapping>(),
std::move(mapping)));
// This destruction observer will notify the video capture device once all
// downstream code is done using the VideoFrame.
frame->AddDestructionObserver(base::BindOnce(
[](std::unique_ptr<Buffer::ScopedAccessPermission> access) {},
std::move(access)));
capture_stack_->OnReceivedFrame(std::move(frame));
}
void OnBufferRetired(int buffer_id) final {
const auto it = buffers_.find(buffer_id);
CHECK(it != buffers_.end());
buffers_.erase(it);
}
void OnError() final { capture_stack_->error_occurred_ = true; }
void OnLog(const std::string& message) final {
capture_stack_->log_messages_.push_back(message);
}
void OnStarted() final { capture_stack_->started_ = true; }
void OnStartedUsingGpuDecode() final { NOTREACHED(); }
FakeVideoCaptureStack* const capture_stack_;
base::flat_map<int, media::mojom::VideoBufferHandlePtr> buffers_;
DISALLOW_COPY_AND_ASSIGN(Receiver);
};
std::unique_ptr<media::VideoFrameReceiver>
FakeVideoCaptureStack::CreateFrameReceiver() {
return std::make_unique<Receiver>(this);
}
SkBitmap FakeVideoCaptureStack::NextCapturedFrame() {
CHECK(!frames_.empty());
media::VideoFrame& frame = *(frames_.front());
SkBitmap bitmap;
bitmap.allocN32Pixels(frame.visible_rect().width(),
frame.visible_rect().height());
// TODO(crbug/810131): This is not Rec.709 colorspace conversion, and so will
// introduce inaccuracies.
libyuv::I420ToARGB(frame.visible_data(media::VideoFrame::kYPlane),
frame.stride(media::VideoFrame::kYPlane),
frame.visible_data(media::VideoFrame::kUPlane),
frame.stride(media::VideoFrame::kUPlane),
frame.visible_data(media::VideoFrame::kVPlane),
frame.stride(media::VideoFrame::kVPlane),
reinterpret_cast<uint8_t*>(bitmap.getPixels()),
static_cast<int>(bitmap.rowBytes()), bitmap.width(),
bitmap.height());
frames_.pop_front();
return bitmap;
}
void FakeVideoCaptureStack::ClearCapturedFramesQueue() {
frames_.clear();
}
void FakeVideoCaptureStack::ExpectHasLogMessages() {
EXPECT_FALSE(log_messages_.empty());
while (!log_messages_.empty()) {
VLOG(1) << "Next log message: " << log_messages_.front();
log_messages_.pop_front();
}
}
void FakeVideoCaptureStack::ExpectNoLogMessages() {
while (!log_messages_.empty()) {
ADD_FAILURE() << "Unexpected log message: " << log_messages_.front();
log_messages_.pop_front();
}
}
void FakeVideoCaptureStack::OnReceivedFrame(
scoped_refptr<media::VideoFrame> frame) {
// Frame timestamps should be monotionically increasing.
EXPECT_LT(last_frame_timestamp_, frame->timestamp());
last_frame_timestamp_ = frame->timestamp();
frames_.emplace_back(std::move(frame));
}
} // namespace content
// 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 CONTENT_BROWSER_MEDIA_CAPTURE_FAKE_VIDEO_CAPTURE_STACK_H_
#define CONTENT_BROWSER_MEDIA_CAPTURE_FAKE_VIDEO_CAPTURE_STACK_H_
#include <memory>
#include <string>
#include "base/containers/circular_deque.h"
#include "base/memory/scoped_refptr.h"
#include "base/time/time.h"
#include "content/public/browser/browser_thread.h"
#include "third_party/skia/include/core/SkBitmap.h"
namespace media {
class VideoFrame;
class VideoFrameReceiver;
}; // namespace media
namespace content {
// Provides a fake representation of the entire video capture stack. It creates
// a VideoFrameReceiver that a device can deliver video frames to, and adapts
// that to a simple collector of video frames, represented as SkBitmaps, for
// further examination by the browser tests.
class FakeVideoCaptureStack {
public:
FakeVideoCaptureStack();
~FakeVideoCaptureStack();
// Reset the capture stack to a state where it contains no frames and is
// expecting a first frame.
void Reset();
// Returns a VideoFrameReceiver that the implementation under test delivers
// frames to.
std::unique_ptr<media::VideoFrameReceiver> CreateFrameReceiver();
// Returns true if the device called VideoFrameReceiver::OnStarted().
bool started() const { return started_; }
// Returns true if the device called VideoFrameReceiver::OnError().
bool error_occurred() const { return error_occurred_; }
// Accessors to capture frame queue.
bool has_captured_frames() const { return !frames_.empty(); }
SkBitmap NextCapturedFrame();
void ClearCapturedFramesQueue();
// Called when tests expect there to be one or more log messages sent to the
// video capture stack. Turn on verbose logging for a dump of the actual log
// messages. This method clears the queue of log messages.
void ExpectHasLogMessages();
// Called when tests expect there to be no log messages sent to the video
// capture stack.
void ExpectNoLogMessages();
private:
// A minimal implementation of VideoFrameReceiver that wraps buffers into
// VideoFrame instances and forwards all relevant callbacks and data to the
// parent FakeVideoCaptureStack.
class Receiver;
// Checks that the frame timestamp is monotonically increasing and then
// stashes it in the |frames_| queue for later examination by the tests.
void OnReceivedFrame(scoped_refptr<media::VideoFrame> frame);
bool started_ = false;
bool error_occurred_ = false;
base::circular_deque<std::string> log_messages_;
base::circular_deque<scoped_refptr<media::VideoFrame>> frames_;
base::TimeDelta last_frame_timestamp_ = base::TimeDelta::Min();
};
} // namespace content
#endif // CONTENT_BROWSER_MEDIA_CAPTURE_FAKE_VIDEO_CAPTURE_STACK_H_
// 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 "content/browser/media/capture/frame_test_util.h"
#include <stdint.h>
#include <cmath>
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/transform.h"
namespace content {
// static
FrameTestUtil::RGB FrameTestUtil::ComputeAverageColor(
SkBitmap frame,
const gfx::Rect& raw_include_rect,
const gfx::Rect& raw_exclude_rect) {
// Clip the rects to the valid region within |frame|. Also, only the subregion
// of |exclude_rect| within |include_rect| is relevant.
gfx::Rect include_rect = raw_include_rect;
include_rect.Intersect(gfx::Rect(0, 0, frame.width(), frame.height()));
gfx::Rect exclude_rect = raw_exclude_rect;
exclude_rect.Intersect(include_rect);
// Sum up the color values in each color channel for all pixels in
// |include_rect| not contained by |exclude_rect|.
int64_t include_sums[3] = {0};
for (int y = include_rect.y(), bottom = include_rect.bottom(); y < bottom;
++y) {
for (int x = include_rect.x(), right = include_rect.right(); x < right;
++x) {
const SkColor color = frame.getColor(x, y);
if (exclude_rect.Contains(x, y)) {
continue;
}
include_sums[0] += SkColorGetR(color);
include_sums[1] += SkColorGetG(color);
include_sums[2] += SkColorGetB(color);
}
}
// Divide the sums by the area to compute the average color.
const int include_area =
include_rect.size().GetArea() - exclude_rect.size().GetArea();
if (include_area <= 0) {
return RGB{NAN, NAN, NAN};
} else {
const auto include_area_f = static_cast<double>(include_area);
return RGB{include_sums[0] / include_area_f,
include_sums[1] / include_area_f,
include_sums[2] / include_area_f};
}
}
// static
bool FrameTestUtil::IsApproximatelySameColor(SkColor color,
const RGB& rgb,
int max_diff) {
const double r_diff = std::abs(SkColorGetR(color) - rgb.r);
const double g_diff = std::abs(SkColorGetG(color) - rgb.g);
const double b_diff = std::abs(SkColorGetB(color) - rgb.b);
return r_diff < max_diff && g_diff < max_diff && b_diff < max_diff;
}
// static
gfx::RectF FrameTestUtil::TransformSimilarly(const gfx::Rect& original,
const gfx::RectF& transformed,
const gfx::Rect& rect) {
if (original.IsEmpty()) {
return gfx::RectF(transformed.x() - original.x(),
transformed.y() - original.y(), 0.0f, 0.0f);
}
// The following is the scale-then-translate 2D matrix.
const gfx::Transform transform(transformed.width() / original.width(), 0.0f,
0.0f, transformed.height() / original.height(),
transformed.x() - original.x(),
transformed.y() - original.y());
gfx::RectF result(rect);
transform.TransformRect(&result);
return result;
}
std::ostream& operator<<(std::ostream& out, const FrameTestUtil::RGB& rgb) {
return (out << "{r=" << rgb.r << ",g=" << rgb.g << ",b=" << rgb.b << '}');
}
} // namespace content
// 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 CONTENT_BROWSER_MEDIA_CAPTURE_FRAME_TEST_UTIL_H_
#define CONTENT_BROWSER_MEDIA_CAPTURE_FRAME_TEST_UTIL_H_
#include <ostream>
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
namespace gfx {
class Rect;
class RectF;
} // namespace gfx
namespace content {
class FrameTestUtil {
public:
struct RGB {
double r;
double g;
double b;
};
// Returns the average RGB color in |include_rect| except for pixels also in
// |exclude_rect|.
static RGB ComputeAverageColor(SkBitmap frame,
const gfx::Rect& include_rect,
const gfx::Rect& exclude_rect);
// Returns true if the red, green, and blue components are all within
// |max_diff| of each other.
static bool IsApproximatelySameColor(SkColor color,
const RGB& rgb,
int max_diff = kMaxColorDifference);
// Determines how |original| has been scaled and translated to become
// |transformed|, and then applies the same transform on |rect| and returns
// the result.
static gfx::RectF TransformSimilarly(const gfx::Rect& original,
const gfx::RectF& transformed,
const gfx::Rect& rect);
// The default maximum color value difference, assuming there will be a little
// error due to pixel boundaries being rounded after coordinate system
// transforms.
static constexpr int kMaxColorDifference = 16;
// A much more-relaxed maximum color value difference, assuming errors caused
// by indifference towards color space concerns (and also "studio" versus
// "jpeg" YUV ranges).
// TODO(crbug/810131): Once color space issues are fixed, remove this.
static constexpr int kMaxInaccurateColorDifference = 48;
};
// A convenience for logging and gtest expectations output.
std::ostream& operator<<(std::ostream& out, const FrameTestUtil::RGB& rgb);
} // namespace content
#endif // CONTENT_BROWSER_MEDIA_CAPTURE_FRAME_TEST_UTIL_H_
...@@ -4,298 +4,44 @@ ...@@ -4,298 +4,44 @@
#include "content/browser/media/capture/web_contents_video_capture_device.h" #include "content/browser/media/capture/web_contents_video_capture_device.h"
#include <stdint.h>
#include <array>
#include <cmath>
#include <string>
#include <tuple> #include <tuple>
#include "base/containers/circular_deque.h" #include "base/macros.h"
#include "base/containers/flat_map.h"
#include "base/run_loop.h" #include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h" #include "build/build_config.h"
#include "cc/test/pixel_test_utils.h" #include "cc/test/pixel_test_utils.h"
#include "content/browser/media/capture/content_capture_device_browsertest_base.h"
#include "content/browser/media/capture/fake_video_capture_stack.h"
#include "content/browser/media/capture/frame_test_util.h"
#include "content/public/browser/browser_thread.h" #include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h" #include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h" #include "content/shell/browser/shell.h"
#include "media/base/video_frame.h"
#include "media/base/video_util.h" #include "media/base/video_util.h"
#include "media/capture/video/video_frame_receiver.h"
#include "media/capture/video_capture_types.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
#include "third_party/libyuv/include/libyuv.h"
#include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h" #include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h" #include "ui/gfx/geometry/rect_conversions.h"
#include "url/gurl.h" #include "ui/gfx/geometry/rect_f.h"
using net::test_server::BasicHttpResponse;
using net::test_server::HttpRequest;
using net::test_server::HttpResponse;
namespace content { namespace content {
namespace { namespace {
// Provides a fake representation of the entire video capture stack. It creates class WebContentsVideoCaptureDeviceBrowserTest
// a VideoFrameReceiver that the device can deliver VideoFrames to, and adapts : public ContentCaptureDeviceBrowserTestBase {
// that to a simple callback structure that allows the browser tests to examine
// each video frame that was captured.
class FakeVideoCaptureStack {
public:
using FrameCallback =
base::RepeatingCallback<void(scoped_refptr<media::VideoFrame> frame)>;
void SetFrameCallback(FrameCallback callback) {
frame_callback_ = std::move(callback);
}
std::unique_ptr<media::VideoFrameReceiver> CreateFrameReceiver() {
return std::make_unique<FakeVideoFrameReceiver>(this);
}
bool started() const { return started_; }
bool error_occurred() const { return error_occurred_; }
void ExpectHasLogMessages() {
EXPECT_FALSE(log_messages_.empty());
while (!log_messages_.empty()) {
VLOG(1) << "Next log message: " << log_messages_.front();
log_messages_.pop_front();
}
}
void ExpectNoLogMessages() {
while (!log_messages_.empty()) {
ADD_FAILURE() << "Unexpected log message: " << log_messages_.front();
log_messages_.pop_front();
}
}
private:
// A minimal implementation of VideoFrameReceiver that wraps buffers into
// VideoFrame instances and forwards all relevant callbacks and data to the
// parent FakeVideoCaptureStack.
class FakeVideoFrameReceiver : public media::VideoFrameReceiver {
public:
explicit FakeVideoFrameReceiver(FakeVideoCaptureStack* capture_stack)
: capture_stack_(capture_stack) {}
private:
using Buffer = media::VideoCaptureDevice::Client::Buffer;
void OnNewBuffer(int buffer_id,
media::mojom::VideoBufferHandlePtr buffer_handle) final {
buffers_[buffer_id] = std::move(buffer_handle);
}
void OnFrameReadyInBuffer(
int buffer_id,
int frame_feedback_id,
std::unique_ptr<Buffer::ScopedAccessPermission> access,
media::mojom::VideoFrameInfoPtr frame_info) final {
const auto it = buffers_.find(buffer_id);
CHECK(it != buffers_.end());
CHECK(it->second->is_shared_buffer_handle());
mojo::ScopedSharedBufferHandle& buffer =
it->second->get_shared_buffer_handle();
const size_t mapped_size =
media::VideoCaptureFormat(frame_info->coded_size, 0.0f,
frame_info->pixel_format)
.ImageAllocationSize();
mojo::ScopedSharedBufferMapping mapping = buffer->Map(mapped_size);
CHECK(mapping.get());
auto frame = media::VideoFrame::WrapExternalData(
frame_info->pixel_format, frame_info->coded_size,
frame_info->visible_rect, frame_info->visible_rect.size(),
reinterpret_cast<uint8_t*>(mapping.get()), mapped_size,
frame_info->timestamp);
CHECK(frame);
frame->metadata()->MergeInternalValuesFrom(frame_info->metadata);
// This destruction observer will unmap the shared memory when the
// VideoFrame goes out-of-scope.
frame->AddDestructionObserver(base::BindOnce(
[](mojo::ScopedSharedBufferMapping mapping) {}, std::move(mapping)));
// This destruction observer will notify the WebContentsVideoCaptureDevice
// once all downstream code is done using the VideoFrame.
frame->AddDestructionObserver(base::BindOnce(
[](std::unique_ptr<Buffer::ScopedAccessPermission> access) {},
std::move(access)));
capture_stack_->frame_callback_.Run(std::move(frame));
}
void OnBufferRetired(int buffer_id) final {
const auto it = buffers_.find(buffer_id);
CHECK(it != buffers_.end());
buffers_.erase(it);
}
void OnError() final { capture_stack_->error_occurred_ = true; }
void OnLog(const std::string& message) final {
capture_stack_->log_messages_.push_back(message);
}
void OnStarted() final { capture_stack_->started_ = true; }
void OnStartedUsingGpuDecode() final { NOTREACHED(); }
FakeVideoCaptureStack* const capture_stack_;
base::flat_map<int, media::mojom::VideoBufferHandlePtr> buffers_;
};
FrameCallback frame_callback_;
bool started_ = false;
bool error_occurred_ = false;
base::circular_deque<std::string> log_messages_;
};
class WebContentsVideoCaptureDeviceBrowserTest : public ContentBrowserTest {
public: public:
FakeVideoCaptureStack* capture_stack() { return &capture_stack_; } WebContentsVideoCaptureDeviceBrowserTest() = default;
WebContentsVideoCaptureDevice* device() const { return device_.get(); } ~WebContentsVideoCaptureDeviceBrowserTest() override = default;
// Alters the solid fill color making up the page content. This will trigger a
// compositor update, which will trigger a frame capture.
void ChangePageContentColor(std::string css_color_hex) {
// See the HandleRequest() method for the original documents being modified
// here.
std::string script;
if (is_cross_site_capture_test()) {
const GURL& inner_frame_url =
embedded_test_server()->GetURL(kInnerFrameHostname, kInnerFramePath);
script = base::StringPrintf(
"document.getElementsByTagName('iframe')[0].src = '%s?color=123456';",
inner_frame_url.spec().c_str());
} else {
script = "document.body.style.backgroundColor = '#123456';";
}
script.replace(script.find("123456"), 6, css_color_hex);
CHECK(ExecuteScript(shell()->web_contents(), script));
}
// Returns the size of the WebContents top-level frame view.
gfx::Size GetViewSize() const {
return shell()
->web_contents()
->GetMainFrame()
->GetView()
->GetViewBounds()
.size();
}
// Returns capture parameters based on the current size of the source view,
// which is based on the size of the Shell window.
media::VideoCaptureParams SnapshotCaptureParams() const {
constexpr gfx::Size kMaxCaptureSize = gfx::Size(320, 320);
constexpr int kMaxFramesPerSecond = 60;
gfx::Size capture_size = kMaxCaptureSize;
if (use_fixed_aspect_ratio()) {
// Half either the width or height, depending on the source view size. The
// goal is to force obvious letterboxing (or pillarboxing), regardless of
// how the source view is currently sized.
const gfx::Size view_size = GetViewSize();
if (view_size.width() < view_size.height()) {
capture_size.set_height(capture_size.height() / 2);
} else {
capture_size.set_width(capture_size.width() / 2);
}
}
media::VideoCaptureParams params;
params.requested_format = media::VideoCaptureFormat(
capture_size, kMaxFramesPerSecond, media::PIXEL_FORMAT_I420);
params.resolution_change_policy =
use_fixed_aspect_ratio()
? media::ResolutionChangePolicy::FIXED_ASPECT_RATIO
: media::ResolutionChangePolicy::ANY_WITHIN_LIMIT;
return params;
}
// Navigates to the initial document, according to the current test
// parameters, and waits for page load completion.
void NavigateToInitialDocument() {
// Navigate to the single-frame test's document and record the view size.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(
kSingleFrameHostname, kSingleFramePath)));
ASSERT_TRUE(WaitForLoadStop(shell()->web_contents()));
expected_view_size_ = GetViewSize();
VLOG(1) << "View size is " << expected_view_size_.ToString();
// If doing a cross-site capture test, navigate to the more-complex document
// that also contains an iframe (rendered in a separate process).
if (is_cross_site_capture_test()) {
ASSERT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(
kOuterFrameHostname, kOuterFramePath)));
ASSERT_TRUE(WaitForLoadStop(shell()->web_contents()));
// Confirm the iframe is a cross-process child render frame.
auto* const child_frame =
ChildFrameAt(shell()->web_contents()->GetMainFrame(), 0);
ASSERT_TRUE(child_frame);
ASSERT_TRUE(child_frame->IsCrossProcessSubframe());
}
}
// Creates and starts the device for frame capture, and checks that the
// initial refresh frame is delivered.
void AllocateAndStartAndWaitForFirstFrame() {
frames_.clear();
last_frame_timestamp_ = base::TimeDelta::Min();
capture_stack()->SetFrameCallback(
base::BindRepeating(&WebContentsVideoCaptureDeviceBrowserTest::OnFrame,
base::Unretained(this)));
auto* const main_frame = shell()->web_contents()->GetMainFrame();
device_ = std::make_unique<WebContentsVideoCaptureDevice>(
main_frame->GetProcess()->GetID(), main_frame->GetRoutingID());
device_->AllocateAndStartWithReceiver(
SnapshotCaptureParams(), capture_stack()->CreateFrameReceiver());
RunAllPendingInMessageLoop(BrowserThread::UI);
EXPECT_TRUE(capture_stack()->started());
EXPECT_FALSE(capture_stack()->error_occurred());
capture_stack()->ExpectNoLogMessages();
min_capture_period_ = base::TimeDelta::FromMicroseconds(
base::Time::kMicrosecondsPerSecond /
device_->capture_params().requested_format.frame_rate);
WaitForFrameWithColor(SK_ColorBLACK);
}
// Stops and destroys the device.
void StopAndDeAllocate() {
device_->StopAndDeAllocate();
RunAllPendingInMessageLoop(BrowserThread::UI);
device_.reset();
}
void ClearCapturedFramesQueue() { frames_.clear(); }
bool HasCapturedFramesInQueue() const { return !frames_.empty(); } // Runs the browser until a frame whose content matches the given |color| is
// found in the captured frames queue, or until a testing failure has
// Runs the browser until a frame with the given |color| is found in the // occurred.
// captured frames queue, or until a testing failure has occurred.
void WaitForFrameWithColor(SkColor color) { void WaitForFrameWithColor(SkColor color) {
VLOG(1) << "Waiting for frame filled with color: red=" << SkColorGetR(color) VLOG(1) << "Waiting for frame content area filled with color: red="
<< ", green=" << SkColorGetG(color) << SkColorGetR(color) << ", green=" << SkColorGetG(color)
<< ", blue=" << SkColorGetB(color); << ", blue=" << SkColorGetB(color);
while (!testing::Test::HasFailure()) { while (!testing::Test::HasFailure()) {
...@@ -303,52 +49,59 @@ class WebContentsVideoCaptureDeviceBrowserTest : public ContentBrowserTest { ...@@ -303,52 +49,59 @@ class WebContentsVideoCaptureDeviceBrowserTest : public ContentBrowserTest {
EXPECT_FALSE(capture_stack()->error_occurred()); EXPECT_FALSE(capture_stack()->error_occurred());
capture_stack()->ExpectNoLogMessages(); capture_stack()->ExpectNoLogMessages();
while (!frames_.empty() && !testing::Test::HasFailure()) { while (capture_stack()->has_captured_frames() &&
!testing::Test::HasFailure()) {
// Pop the next frame from the front of the queue and convert to a RGB // Pop the next frame from the front of the queue and convert to a RGB
// bitmap for analysis. // bitmap for analysis.
SkBitmap rgb_frame = ConvertToSkBitmap(*(frames_.front())); const SkBitmap rgb_frame = capture_stack()->NextCapturedFrame();
frames_.pop_front();
EXPECT_FALSE(rgb_frame.empty()); EXPECT_FALSE(rgb_frame.empty());
// Analyze the frame and compute the average color value for each of: 1) // Three regions of the frame will be analyzed: 1) the upper-left
// the upper-left quadrant of the content region; 2) the remaining three // quadrant of the content region where the iframe draws; 2) the
// quadrants of the content region; and 3) the non-content (i.e., // remaining three quadrants of the content region where the main frame
// letterboxed) region. // draws; and 3) the non-content (i.e., letterboxed) region.
const gfx::Size frame_size(rgb_frame.width(), rgb_frame.height()); const gfx::Size frame_size(rgb_frame.width(), rgb_frame.height());
const gfx::Size current_view_size = GetViewSize(); const gfx::Size source_size = GetExpectedSourceSize();
EXPECT_EQ(expected_view_size_, current_view_size) const gfx::Rect iframe_rect(0, 0, source_size.width() / 2,
<< "Sanity-check failed: View size changed sized during this test."; source_size.height() / 2);
const gfx::Rect content_rect =
use_fixed_aspect_ratio() // Compute the Rects representing where the three regions would be in
? media::ComputeLetterboxRegion(gfx::Rect(frame_size), // the |rgb_frame|.
current_view_size) const gfx::RectF content_in_frame_rect_f(
: gfx::Rect(frame_size); IsFixedAspectRatioTest() ? media::ComputeLetterboxRegion(
std::array<double, 3> average_ul_content_rgb; gfx::Rect(frame_size), source_size)
std::array<double, 3> average_rem_content_rgb; : gfx::Rect(frame_size));
std::array<double, 3> average_letterbox_rgb; const gfx::RectF iframe_in_frame_rect_f =
AnalyzeFrame(rgb_frame, content_rect, &average_ul_content_rgb, FrameTestUtil::TransformSimilarly(
&average_rem_content_rgb, &average_letterbox_rgb); gfx::Rect(source_size), content_in_frame_rect_f, iframe_rect);
const gfx::Rect content_in_frame_rect =
const auto ToTriplet = [](const std::array<double, 3>& rgb) { gfx::ToEnclosingRect(content_in_frame_rect_f);
return base::StringPrintf("(%f,%f,%f)", rgb[0], rgb[1], rgb[2]); const gfx::Rect iframe_in_frame_rect =
}; gfx::ToEnclosingRect(iframe_in_frame_rect_f);
VLOG(1) << "Video frame analysis: size=" << frame_size.ToString()
<< ", expected content_rect=" << content_rect.ToString() // Determine the average RGB color in the three regions-of-interest in
<< ", average upper-left content quadrant rgb=" // the frame.
<< ToTriplet(average_ul_content_rgb) const auto average_iframe_rgb = FrameTestUtil::ComputeAverageColor(
<< ", average remaining content rgb=" rgb_frame, iframe_in_frame_rect, gfx::Rect());
<< ToTriplet(average_rem_content_rgb) const auto average_mainframe_rgb = FrameTestUtil::ComputeAverageColor(
<< ", average letterbox rgb=" rgb_frame, content_in_frame_rect, iframe_in_frame_rect);
<< ToTriplet(average_letterbox_rgb); const auto average_letterbox_rgb = FrameTestUtil::ComputeAverageColor(
rgb_frame, gfx::Rect(frame_size), content_in_frame_rect);
// The letterboxed region should be black.
if (use_fixed_aspect_ratio()) { VLOG(1)
EXPECT_NEAR(SkColorGetR(SK_ColorBLACK), average_letterbox_rgb[0], << "Video frame analysis: size=" << frame_size.ToString()
kMaxColorDifference); << ", captured upper-left quadrant of content should be at "
EXPECT_NEAR(SkColorGetG(SK_ColorBLACK), average_letterbox_rgb[1], << iframe_in_frame_rect.ToString() << " and has average color "
kMaxColorDifference); << average_iframe_rgb
EXPECT_NEAR(SkColorGetB(SK_ColorBLACK), average_letterbox_rgb[2], << ", captured remaining quadrants of content should be bound by "
kMaxColorDifference); << content_in_frame_rect.ToString() << " and has average color "
<< average_mainframe_rgb << ", letterbox region has average color "
<< average_letterbox_rgb;
// The letterboxed region should always be black.
if (IsFixedAspectRatioTest()) {
EXPECT_TRUE(FrameTestUtil::IsApproximatelySameColor(
SK_ColorBLACK, average_letterbox_rgb));
} }
if (testing::Test::HasFailure()) { if (testing::Test::HasFailure()) {
...@@ -357,14 +110,19 @@ class WebContentsVideoCaptureDeviceBrowserTest : public ContentBrowserTest { ...@@ -357,14 +110,19 @@ class WebContentsVideoCaptureDeviceBrowserTest : public ContentBrowserTest {
return; return;
} }
if (is_cross_site_capture_test() && // Return if the content region(s) now has/have the expected color(s).
IsApproximatelySameColor(color, average_ul_content_rgb) && if (IsCrossSiteCaptureTest() &&
IsApproximatelySameColor(SK_ColorWHITE, average_rem_content_rgb)) { FrameTestUtil::IsApproximatelySameColor(color,
average_iframe_rgb) &&
FrameTestUtil::IsApproximatelySameColor(SK_ColorWHITE,
average_mainframe_rgb)) {
VLOG(1) << "Observed desired frame."; VLOG(1) << "Observed desired frame.";
return; return;
} else if (!is_cross_site_capture_test() && } else if (!IsCrossSiteCaptureTest() &&
IsApproximatelySameColor(color, average_ul_content_rgb) && FrameTestUtil::IsApproximatelySameColor(
IsApproximatelySameColor(color, average_rem_content_rgb)) { color, average_iframe_rgb) &&
FrameTestUtil::IsApproximatelySameColor(
color, average_mainframe_rgb)) {
VLOG(1) << "Observed desired frame."; VLOG(1) << "Observed desired frame.";
return; return;
} else { } else {
...@@ -378,226 +136,34 @@ class WebContentsVideoCaptureDeviceBrowserTest : public ContentBrowserTest { ...@@ -378,226 +136,34 @@ class WebContentsVideoCaptureDeviceBrowserTest : public ContentBrowserTest {
base::RunLoop run_loop; base::RunLoop run_loop;
BrowserThread::PostDelayedTask(BrowserThread::UI, FROM_HERE, BrowserThread::PostDelayedTask(BrowserThread::UI, FROM_HERE,
run_loop.QuitClosure(), run_loop.QuitClosure(),
min_capture_period_); GetMinCapturePeriod());
run_loop.Run(); run_loop.Run();
} }
} }
protected: protected:
// These are overridden for the parameterized tests. // Don't call this. Call <BaseClass>::GetExpectedSourceSize() instead.
virtual bool use_software_compositing() const { return false; } gfx::Size GetCapturedSourceSize() const final {
virtual bool use_fixed_aspect_ratio() const { return false; } return shell()
virtual bool is_cross_site_capture_test() const { return false; } ->web_contents()
->GetMainFrame()
void SetUp() override { ->GetView()
// IMPORTANT: Do not add the switches::kUseGpuInTests command line flag: It ->GetViewBounds()
// causes the tests to take 12+ seconds just to spin up a render process on .size();
// debug builds. It can also cause test failures in MSAN builds, or
// exacerbate OOM situations on highly-loaded machines.
// Screen capture requires readback from compositor output.
EnablePixelOutput();
// Conditionally force software compositing instead of GPU-accelerated
// compositing.
if (use_software_compositing()) {
UseSoftwareCompositing();
}
ContentBrowserTest::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
IsolateAllSitesForTesting(command_line);
} }
void SetUpOnMainThread() override { std::unique_ptr<FrameSinkVideoCaptureDevice> CreateDevice() final {
ContentBrowserTest::SetUpOnMainThread(); auto* const main_frame = shell()->web_contents()->GetMainFrame();
return std::make_unique<WebContentsVideoCaptureDevice>(
// Set-up and start the embedded test HTTP server. main_frame->GetProcess()->GetID(), main_frame->GetRoutingID());
host_resolver()->AddRule("*", "127.0.0.1");
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&WebContentsVideoCaptureDeviceBrowserTest::HandleRequest,
base::Unretained(this)));
ASSERT_TRUE(embedded_test_server()->Start());
} }
void TearDownOnMainThread() override { void WaitForFirstFrame() final { WaitForFrameWithColor(SK_ColorBLACK); }
ASSERT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
frames_.clear();
// Run any left-over tasks (usually these are delete-soon's and orphaned
// tasks).
base::RunLoop().RunUntilIdle();
ContentBrowserTest::TearDownOnMainThread();
}
private: private:
void OnFrame(scoped_refptr<media::VideoFrame> frame) { DISALLOW_COPY_AND_ASSIGN(WebContentsVideoCaptureDeviceBrowserTest);
// Frame timestamps should be monotionically increasing.
EXPECT_LT(last_frame_timestamp_, frame->timestamp());
last_frame_timestamp_ = frame->timestamp();
frames_.emplace_back(std::move(frame));
}
// Called by the embedded test HTTP server to provide the document resources.
std::unique_ptr<HttpResponse> HandleRequest(const HttpRequest& request) {
auto response = std::make_unique<BasicHttpResponse>();
response->set_content_type("text/html");
const GURL& url = request.GetURL();
if (url.path() == kOuterFramePath) {
// A page with a solid white fill color, but containing an iframe in its
// upper-left quadrant.
const GURL& inner_frame_url =
embedded_test_server()->GetURL(kInnerFrameHostname, kInnerFramePath);
response->set_content(base::StringPrintf(
"<!doctype html>"
"<body style='background-color: #ffffff;'>"
"<iframe src='%s' width=%d height=%d style='position:absolute; "
"top:0px; left:0px; margin:none; padding:none; border:none;'>"
"</iframe>"
"</body>",
inner_frame_url.spec().c_str(), expected_view_size_.width() / 2,
expected_view_size_.height() / 2));
} else {
// A page whose solid fill color is based on a query parameter, or
// defaults to black.
const std::string& query = url.query();
std::string color = "#000000";
const auto pos = query.find("color=");
if (pos != std::string::npos) {
color = "#" + query.substr(pos + 6, 6);
}
response->set_content(
base::StringPrintf("<!doctype html>"
"<body style='background-color: %s;'></body>",
color.c_str()));
}
return std::move(response);
}
static SkBitmap ConvertToSkBitmap(const media::VideoFrame& frame) {
SkBitmap bitmap;
bitmap.allocN32Pixels(frame.visible_rect().width(),
frame.visible_rect().height());
// TODO(miu): This is not Rec.709 colorspace conversion, and so will
// introduce inaccuracies.
libyuv::I420ToARGB(frame.visible_data(media::VideoFrame::kYPlane),
frame.stride(media::VideoFrame::kYPlane),
frame.visible_data(media::VideoFrame::kUPlane),
frame.stride(media::VideoFrame::kUPlane),
frame.visible_data(media::VideoFrame::kVPlane),
frame.stride(media::VideoFrame::kVPlane),
reinterpret_cast<uint8_t*>(bitmap.getPixels()),
static_cast<int>(bitmap.rowBytes()), bitmap.width(),
bitmap.height());
return bitmap;
}
// Computes the average color in the frame for each of these regions: 1) the
// upper-left quadrant of the content; 2) the remaining three quadrants of the
// content; 3) the letterboxed regions (if any).
static void AnalyzeFrame(SkBitmap frame,
const gfx::Rect& content_rect,
std::array<double, 3>* average_ul_content_rgb,
std::array<double, 3>* average_rem_content_rgb,
std::array<double, 3>* average_letterbox_rgb) {
const gfx::Rect ul_content_rect(content_rect.x(), content_rect.y(),
content_rect.width() / 2,
content_rect.height() / 2);
int64_t sum_of_ul_content_values[3] = {0};
int64_t sum_of_rem_content_values[3] = {0};
int64_t sum_of_letterbox_values[3] = {0};
for (int y = 0; y < frame.height(); ++y) {
for (int x = 0; x < frame.width(); ++x) {
const SkColor color = frame.getColor(x, y);
int64_t* const sums =
ul_content_rect.Contains(x, y)
? sum_of_ul_content_values
: (content_rect.Contains(x, y) ? sum_of_rem_content_values
: sum_of_letterbox_values);
sums[0] += SkColorGetR(color);
sums[1] += SkColorGetG(color);
sums[2] += SkColorGetB(color);
}
}
const double ul_content_area =
static_cast<double>(ul_content_rect.size().GetArea());
for (int i = 0; i < 3; ++i) {
(*average_ul_content_rgb)[i] =
(ul_content_area <= 0.0)
? NAN
: (sum_of_ul_content_values[i] / ul_content_area);
}
const double rem_content_area = static_cast<double>(
content_rect.size().GetArea() - ul_content_rect.size().GetArea());
for (int i = 0; i < 3; ++i) {
(*average_rem_content_rgb)[i] =
(rem_content_area <= 0.0)
? NAN
: (sum_of_rem_content_values[i] / rem_content_area);
}
const double letterbox_area = static_cast<double>(
(frame.width() * frame.height()) - content_rect.size().GetArea());
for (int i = 0; i < 3; ++i) {
(*average_letterbox_rgb)[i] =
(letterbox_area <= 0.0)
? NAN
: (sum_of_letterbox_values[i] / letterbox_area);
}
}
static bool IsApproximatelySameColor(SkColor color,
const std::array<double, 3> rgb) {
const double r_diff = std::abs(SkColorGetR(color) - rgb[0]);
const double g_diff = std::abs(SkColorGetG(color) - rgb[1]);
const double b_diff = std::abs(SkColorGetB(color) - rgb[2]);
return r_diff < kMaxColorDifference && g_diff < kMaxColorDifference &&
b_diff < kMaxColorDifference;
}
FakeVideoCaptureStack capture_stack_;
gfx::Size expected_view_size_;
std::unique_ptr<WebContentsVideoCaptureDevice> device_;
base::TimeDelta min_capture_period_;
base::circular_deque<scoped_refptr<media::VideoFrame>> frames_;
base::TimeDelta last_frame_timestamp_;
static constexpr int kMaxColorDifference = 8;
// Arbitrary string constants used to refer to each document by
// host+path. Note that the "inner frame" and "outer frame" must have
// different hostnames to engage the cross-site process isolation logic in the
// browser.
static constexpr char kInnerFrameHostname[] = "innerframe.com";
static constexpr char kInnerFramePath[] = "/inner.html";
static constexpr char kOuterFrameHostname[] = "outerframe.com";
static constexpr char kOuterFramePath[] = "/outer.html";
static constexpr char kSingleFrameHostname[] = "singleframe.com";
static constexpr char kSingleFramePath[] = "/single.html";
}; };
// static
constexpr char WebContentsVideoCaptureDeviceBrowserTest::kInnerFrameHostname[];
// static
constexpr char WebContentsVideoCaptureDeviceBrowserTest::kInnerFramePath[];
// static
constexpr char WebContentsVideoCaptureDeviceBrowserTest::kOuterFrameHostname[];
// static
constexpr char WebContentsVideoCaptureDeviceBrowserTest::kOuterFramePath[];
// static
constexpr char WebContentsVideoCaptureDeviceBrowserTest::kSingleFrameHostname[];
// static
constexpr char WebContentsVideoCaptureDeviceBrowserTest::kSingleFramePath[];
// Tests that the device refuses to start if the WebContents target was // Tests that the device refuses to start if the WebContents target was
// destroyed before the device could start. // destroyed before the device could start.
IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest, IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest,
...@@ -619,7 +185,7 @@ IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest, ...@@ -619,7 +185,7 @@ IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest,
render_process_id, render_frame_id); render_process_id, render_frame_id);
// Running the pending UI tasks should cause the device to realize the // Running the pending UI tasks should cause the device to realize the
// WebContents is gone. // WebContents is gone.
RunAllPendingInMessageLoop(BrowserThread::UI); RunUntilIdle();
// Attempt to start the device, and expect the video capture stack to have // Attempt to start the device, and expect the video capture stack to have
// been notified of the error. // been notified of the error.
...@@ -630,7 +196,7 @@ IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest, ...@@ -630,7 +196,7 @@ IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest,
capture_stack()->ExpectHasLogMessages(); capture_stack()->ExpectHasLogMessages();
device->StopAndDeAllocate(); device->StopAndDeAllocate();
RunAllPendingInMessageLoop(BrowserThread::UI); RunUntilIdle();
} }
// Tests that the device starts, captures a frame, and then gracefully // Tests that the device starts, captures a frame, and then gracefully
...@@ -647,7 +213,7 @@ IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest, ...@@ -647,7 +213,7 @@ IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest,
// Delete the WebContents instance and the Shell, and allow the the "target // Delete the WebContents instance and the Shell, and allow the the "target
// permanently lost" error to propagate to the video capture stack. // permanently lost" error to propagate to the video capture stack.
shell()->web_contents()->Close(); shell()->web_contents()->Close();
RunAllPendingInMessageLoop(BrowserThread::UI); RunUntilIdle();
EXPECT_TRUE(capture_stack()->error_occurred()); EXPECT_TRUE(capture_stack()->error_occurred());
capture_stack()->ExpectHasLogMessages(); capture_stack()->ExpectHasLogMessages();
...@@ -668,7 +234,7 @@ IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest, ...@@ -668,7 +234,7 @@ IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest,
// Suspend the device. // Suspend the device.
device()->MaybeSuspend(); device()->MaybeSuspend();
RunAllPendingInMessageLoop(BrowserThread::UI); RunUntilIdle();
ClearCapturedFramesQueue(); ClearCapturedFramesQueue();
// Change the page content and run the browser for five seconds. Expect no // Change the page content and run the browser for five seconds. Expect no
...@@ -715,13 +281,13 @@ class WebContentsVideoCaptureDeviceBrowserTestP ...@@ -715,13 +281,13 @@ class WebContentsVideoCaptureDeviceBrowserTestP
: public WebContentsVideoCaptureDeviceBrowserTest, : public WebContentsVideoCaptureDeviceBrowserTest,
public testing::WithParamInterface<std::tuple<bool, bool, bool>> { public testing::WithParamInterface<std::tuple<bool, bool, bool>> {
public: public:
bool use_software_compositing() const override { bool IsSoftwareCompositingTest() const override {
return std::get<0>(GetParam()); return std::get<0>(GetParam());
} }
bool use_fixed_aspect_ratio() const override { bool IsFixedAspectRatioTest() const override {
return std::get<1>(GetParam()); return std::get<1>(GetParam());
} }
bool is_cross_site_capture_test() const override { bool IsCrossSiteCaptureTest() const override {
return std::get<2>(GetParam()); return std::get<2>(GetParam());
} }
}; };
...@@ -758,10 +324,10 @@ IN_PROC_BROWSER_TEST_P(WebContentsVideoCaptureDeviceBrowserTestP, ...@@ -758,10 +324,10 @@ IN_PROC_BROWSER_TEST_P(WebContentsVideoCaptureDeviceBrowserTestP,
CapturesContentChanges) { CapturesContentChanges) {
SCOPED_TRACE(testing::Message() SCOPED_TRACE(testing::Message()
<< "Test parameters: " << "Test parameters: "
<< (use_software_compositing() ? "Software Compositing" << (IsSoftwareCompositingTest() ? "Software Compositing"
: "GPU Compositing") : "GPU Compositing")
<< " with " << " with "
<< (use_fixed_aspect_ratio() ? "Fixed Video Aspect Ratio" << (IsFixedAspectRatioTest() ? "Fixed Video Aspect Ratio"
: "Variable Video Aspect Ratio")); : "Variable Video Aspect Ratio"));
NavigateToInitialDocument(); NavigateToInitialDocument();
...@@ -769,22 +335,30 @@ IN_PROC_BROWSER_TEST_P(WebContentsVideoCaptureDeviceBrowserTestP, ...@@ -769,22 +335,30 @@ IN_PROC_BROWSER_TEST_P(WebContentsVideoCaptureDeviceBrowserTestP,
for (int visilibilty_case = 0; visilibilty_case < 3; ++visilibilty_case) { for (int visilibilty_case = 0; visilibilty_case < 3; ++visilibilty_case) {
switch (visilibilty_case) { switch (visilibilty_case) {
case 0: case 0: {
VLOG(1) << "Visibility case: WebContents is showing."; SCOPED_TRACE(testing::Message()
<< "Visibility case: WebContents is showing.");
shell()->web_contents()->WasShown(); shell()->web_contents()->WasShown();
base::RunLoop().RunUntilIdle(); base::RunLoop().RunUntilIdle();
ASSERT_EQ(shell()->web_contents()->GetVisibility(), ASSERT_EQ(shell()->web_contents()->GetVisibility(),
content::Visibility::VISIBLE); content::Visibility::VISIBLE);
break; break;
case 1: }
VLOG(1) << "Visibility case: WebContents is hidden.";
case 1: {
SCOPED_TRACE(testing::Message()
<< "Visibility case: WebContents is hidden.");
shell()->web_contents()->WasHidden(); shell()->web_contents()->WasHidden();
base::RunLoop().RunUntilIdle(); base::RunLoop().RunUntilIdle();
ASSERT_EQ(shell()->web_contents()->GetVisibility(), ASSERT_EQ(shell()->web_contents()->GetVisibility(),
content::Visibility::HIDDEN); content::Visibility::HIDDEN);
break; break;
case 2: }
VLOG(1) << "Visibility case: WebContents is showing, but occluded.";
case 2: {
SCOPED_TRACE(
testing::Message()
<< "Visibility case: WebContents is showing, but occluded.");
shell()->web_contents()->WasShown(); shell()->web_contents()->WasShown();
shell()->web_contents()->WasOccluded(); shell()->web_contents()->WasOccluded();
base::RunLoop().RunUntilIdle(); base::RunLoop().RunUntilIdle();
...@@ -792,6 +366,7 @@ IN_PROC_BROWSER_TEST_P(WebContentsVideoCaptureDeviceBrowserTestP, ...@@ -792,6 +366,7 @@ IN_PROC_BROWSER_TEST_P(WebContentsVideoCaptureDeviceBrowserTestP,
content::Visibility::OCCLUDED); content::Visibility::OCCLUDED);
break; break;
} }
}
static const struct { static const struct {
const char* const css_hex; const char* const css_hex;
......
...@@ -995,7 +995,16 @@ test("content_browsertests") { ...@@ -995,7 +995,16 @@ test("content_browsertests") {
} }
if (is_linux || is_mac || is_win) { if (is_linux || is_mac || is_win) {
sources += [ "../browser/media/capture/web_contents_video_capture_device_browsertest.cc" ] sources += [
"../browser/media/capture/content_capture_device_browsertest_base.cc",
"../browser/media/capture/content_capture_device_browsertest_base.h",
"../browser/media/capture/fake_video_capture_stack.cc",
"../browser/media/capture/fake_video_capture_stack.h",
"../browser/media/capture/frame_test_util.cc",
"../browser/media/capture/frame_test_util.h",
"../browser/media/capture/web_contents_video_capture_device_browsertest.cc",
]
deps += [ "//third_party/libyuv" ] deps += [ "//third_party/libyuv" ]
data += [ data += [
"//net/tools/testserver/", "//net/tools/testserver/",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment