blob: 337c91dd2d009c85215001871bd07a8e76e8412e [file] [log] [blame]
// 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 "base/command_line.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "cc/base/math_util.h"
#include "components/viz/common/gpu/context_provider.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/service_manager_connection.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/shell/browser/shell.h"
#include "gpu/command_buffer/client/gles2_interface.h"
#include "media/base/bind_to_current_loop.h"
#include "media/base/media_switches.h"
#include "media/base/video_frame.h"
#include "media/base/video_frame_metadata.h"
#include "media/capture/video/shared_memory_handle_provider.h"
#include "mojo/public/cpp/bindings/strong_binding.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/service_manager/public/cpp/connector.h"
#include "services/video_capture/public/mojom/constants.mojom.h"
#include "services/video_capture/public/mojom/device_factory_provider.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/khronos/GLES2/gl2ext.h"
#include "ui/compositor/compositor.h"
// ImageTransportFactory::GetInstance is not available on all build configs.
#if defined(USE_AURA) || defined(OS_MACOSX)
#define CAN_USE_IMAGE_TRANSPORT_FACTORY 1
#endif
#if defined(CAN_USE_IMAGE_TRANSPORT_FACTORY)
#include "content/browser/compositor/image_transport_factory.h"
namespace content {
namespace {
class InvokeClosureOnDelete
: public video_capture::mojom::ScopedAccessPermission {
public:
explicit InvokeClosureOnDelete(base::OnceClosure closure)
: closure_(std::move(closure)) {}
~InvokeClosureOnDelete() override { std::move(closure_).Run(); }
private:
base::OnceClosure closure_;
};
static const char kVideoCaptureHtmlFile[] = "/media/video_capture_test.html";
static const char kStartVideoCaptureAndVerify[] =
"startVideoCaptureFromVirtualDeviceAndVerifyUniformColorVideoWithSize(%d, "
"%d)";
static const char kVirtualDeviceId[] = "/virtual/device";
static const char kVirtualDeviceName[] = "Virtual Device";
static const gfx::Size kDummyFrameCodedSize(320, 200);
static const gfx::Rect kDummyFrameVisibleRect(94, 36, 178, 150);
static const int kDummyFrameRate = 5;
} // namespace
// Abstraction for logic that is different between exercising
// DeviceFactory.AddTextureVirtualDevice() and
// DeviceFactory.AddSharedMemoryVirtualDevice().
class VirtualDeviceExerciser {
public:
virtual ~VirtualDeviceExerciser() {}
virtual void Initialize() = 0;
virtual void RegisterVirtualDeviceAtFactory(
video_capture::mojom::DeviceFactoryPtr* factory,
const media::VideoCaptureDeviceInfo& info) = 0;
virtual gfx::Size GetVideoSize() = 0;
virtual void PushNextFrame(base::TimeDelta timestamp) = 0;
virtual void ShutDown() = 0;
};
// A VirtualDeviceExerciser for exercising
// DeviceFactory.AddTextureVirtualDevice(). It alternates between two texture
// RGB dummy frames, one dark one and one light one.
class TextureDeviceExerciser : public VirtualDeviceExerciser {
public:
TextureDeviceExerciser() : weak_factory_(this) {
DETACH_FROM_SEQUENCE(sequence_checker_);
}
void Initialize() override {
ImageTransportFactory* factory = ImageTransportFactory::GetInstance();
CHECK(factory);
context_provider_ =
factory->GetContextFactory()->SharedMainThreadContextProvider();
CHECK(context_provider_);
gpu::gles2::GLES2Interface* gl = context_provider_->ContextGL();
CHECK(gl);
const uint8_t kDarkFrameByteValue = 0;
const uint8_t kLightFrameByteValue = 200;
CreateDummyRgbFrame(gl, kDarkFrameByteValue,
&dummy_frame_0_mailbox_holder_);
CreateDummyRgbFrame(gl, kLightFrameByteValue,
&dummy_frame_1_mailbox_holder_);
}
void RegisterVirtualDeviceAtFactory(
video_capture::mojom::DeviceFactoryPtr* factory,
const media::VideoCaptureDeviceInfo& info) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
(*factory)->AddTextureVirtualDevice(info,
mojo::MakeRequest(&virtual_device_));
virtual_device_->OnNewMailboxHolderBufferHandle(
0, media::mojom::MailboxBufferHandleSet::New(
std::move(dummy_frame_0_mailbox_holder_)));
virtual_device_->OnNewMailboxHolderBufferHandle(
1, media::mojom::MailboxBufferHandleSet::New(
std::move(dummy_frame_1_mailbox_holder_)));
frame_being_consumed_[0] = false;
frame_being_consumed_[1] = false;
}
gfx::Size GetVideoSize() override { return kDummyFrameCodedSize; }
void PushNextFrame(base::TimeDelta timestamp) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (frame_being_consumed_[dummy_frame_index_]) {
LOG(INFO) << "Frame " << dummy_frame_index_ << " is still being consumed";
return;
}
video_capture::mojom::ScopedAccessPermissionPtr access_permission_proxy;
mojo::MakeStrongBinding<video_capture::mojom::ScopedAccessPermission>(
std::make_unique<InvokeClosureOnDelete>(
base::BindOnce(&TextureDeviceExerciser::OnFrameConsumptionFinished,
weak_factory_.GetWeakPtr(), dummy_frame_index_)),
mojo::MakeRequest(&access_permission_proxy));
media::VideoFrameMetadata metadata;
metadata.SetDouble(media::VideoFrameMetadata::FRAME_RATE, kDummyFrameRate);
metadata.SetTimeTicks(media::VideoFrameMetadata::REFERENCE_TIME,
base::TimeTicks::Now());
media::mojom::VideoFrameInfoPtr info = media::mojom::VideoFrameInfo::New();
info->timestamp = timestamp;
info->pixel_format = media::PIXEL_FORMAT_ARGB;
info->coded_size = kDummyFrameCodedSize;
info->visible_rect = gfx::Rect(kDummyFrameCodedSize);
info->metadata = metadata.GetInternalValues().Clone();
frame_being_consumed_[dummy_frame_index_] = true;
virtual_device_->OnFrameReadyInBuffer(dummy_frame_index_,
std::move(access_permission_proxy),
std::move(info));
dummy_frame_index_ = (dummy_frame_index_ + 1) % 2;
}
void ShutDown() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
virtual_device_ = nullptr;
weak_factory_.InvalidateWeakPtrs();
}
private:
void CreateDummyRgbFrame(gpu::gles2::GLES2Interface* gl,
uint8_t value_for_all_rgb_bytes,
std::vector<gpu::MailboxHolder>* target) {
const int32_t kBytesPerRGBPixel = 3;
int32_t frame_size_in_bytes = kDummyFrameCodedSize.width() *
kDummyFrameCodedSize.height() *
kBytesPerRGBPixel;
std::unique_ptr<uint8_t[]> dummy_frame_data(
new uint8_t[frame_size_in_bytes]);
memset(dummy_frame_data.get(), value_for_all_rgb_bytes,
frame_size_in_bytes);
for (int i = 0; i < media::VideoFrame::kMaxPlanes; i++) {
// For RGB formats, only the first plane needs to be filled with an
// actual texture.
if (i != 0) {
target->push_back(gpu::MailboxHolder());
continue;
}
GLuint texture_id = 0;
gl->GenTextures(1, &texture_id);
gl->BindTexture(GL_TEXTURE_2D, texture_id);
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
gl->TexImage2D(GL_TEXTURE_2D, 0, GL_RGB, kDummyFrameCodedSize.width(),
kDummyFrameCodedSize.height(), 0, GL_RGB, GL_UNSIGNED_BYTE,
dummy_frame_data.get());
gl->BindTexture(GL_TEXTURE_2D, 0);
gpu::Mailbox mailbox;
gl->ProduceTextureDirectCHROMIUM(texture_id, mailbox.name);
gpu::SyncToken sync_token;
gl->GenSyncTokenCHROMIUM(sync_token.GetData());
target->push_back(gpu::MailboxHolder(mailbox, sync_token, GL_TEXTURE_2D));
}
gl->ShallowFlushCHROMIUM();
CHECK_EQ(gl->GetError(), static_cast<GLenum>(GL_NO_ERROR));
}
void OnFrameConsumptionFinished(int frame_index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
frame_being_consumed_[frame_index] = false;
}
SEQUENCE_CHECKER(sequence_checker_);
scoped_refptr<viz::ContextProvider> context_provider_;
video_capture::mojom::TextureVirtualDevicePtr virtual_device_;
int dummy_frame_index_ = 0;
std::vector<gpu::MailboxHolder> dummy_frame_0_mailbox_holder_;
std::vector<gpu::MailboxHolder> dummy_frame_1_mailbox_holder_;
std::array<bool, 2> frame_being_consumed_;
base::WeakPtrFactory<TextureDeviceExerciser> weak_factory_;
};
// A VirtualDeviceExerciser for exercising
// DeviceFactory.AddSharedMemoryVirtualDevice().
// It generates (dummy) I420 frame data by setting all bytes equal to the
// current frame count. Padding bytes are set to 0.
class SharedMemoryDeviceExerciser : public VirtualDeviceExerciser,
public video_capture::mojom::Producer {
public:
explicit SharedMemoryDeviceExerciser(
media::mojom::PlaneStridesPtr strides = nullptr)
: strides_(std::move(strides)),
producer_binding_(this),
weak_factory_(this) {}
// VirtualDeviceExerciser implementation.
void Initialize() override {}
void RegisterVirtualDeviceAtFactory(
video_capture::mojom::DeviceFactoryPtr* factory,
const media::VideoCaptureDeviceInfo& info) override {
video_capture::mojom::ProducerPtr producer;
static const bool kSendBufferHandlesToProducerAsRawFileDescriptors = false;
producer_binding_.Bind(mojo::MakeRequest(&producer));
(*factory)->AddSharedMemoryVirtualDevice(
info, std::move(producer),
kSendBufferHandlesToProducerAsRawFileDescriptors,
mojo::MakeRequest(&virtual_device_));
}
gfx::Size GetVideoSize() override {
return gfx::Size(kDummyFrameVisibleRect.width(),
kDummyFrameVisibleRect.height());
}
void PushNextFrame(base::TimeDelta timestamp) override {
virtual_device_->RequestFrameBuffer(
kDummyFrameCodedSize, media::VideoPixelFormat::PIXEL_FORMAT_I420,
strides_.Clone(),
base::BindOnce(&SharedMemoryDeviceExerciser::OnFrameBufferReceived,
weak_factory_.GetWeakPtr(), timestamp));
}
void ShutDown() override {
virtual_device_ = nullptr;
producer_binding_.Close();
weak_factory_.InvalidateWeakPtrs();
}
// video_capture::mojom::Producer implementation.
void OnNewBuffer(int32_t buffer_id,
media::mojom::VideoBufferHandlePtr buffer_handle,
OnNewBufferCallback callback) override {
CHECK(buffer_handle->is_shared_buffer_handle());
auto handle_provider =
std::make_unique<media::SharedMemoryHandleProvider>();
handle_provider->InitFromMojoHandle(
std::move(buffer_handle->get_shared_buffer_handle()));
outgoing_buffer_id_to_buffer_map_.insert(
std::make_pair(buffer_id, std::move(handle_provider)));
std::move(callback).Run();
}
void OnBufferRetired(int32_t buffer_id) override {
outgoing_buffer_id_to_buffer_map_.erase(buffer_id);
}
private:
void OnFrameBufferReceived(base::TimeDelta timestamp, int32_t buffer_id) {
if (buffer_id == video_capture::mojom::kInvalidBufferId)
return;
media::VideoFrameMetadata metadata;
metadata.SetDouble(media::VideoFrameMetadata::FRAME_RATE, kDummyFrameRate);
metadata.SetTimeTicks(media::VideoFrameMetadata::REFERENCE_TIME,
base::TimeTicks::Now());
media::mojom::VideoFrameInfoPtr info = media::mojom::VideoFrameInfo::New();
info->timestamp = timestamp;
info->pixel_format = media::PIXEL_FORMAT_I420;
info->coded_size = kDummyFrameCodedSize;
info->visible_rect = kDummyFrameVisibleRect;
info->metadata = metadata.GetInternalValues().Clone();
info->strides = strides_.Clone();
auto outgoing_buffer = outgoing_buffer_id_to_buffer_map_.at(buffer_id)
->GetHandleForInProcessAccess();
static int frame_count = 0;
frame_count++;
const uint8_t dummy_value = frame_count % 256;
// Reset the whole buffer to 0
memset(outgoing_buffer->data(), 0, outgoing_buffer->mapped_size());
// Set all bytes affecting |info->visible_rect| to |dummy_value|.
const int kYStride = info->strides ? info->strides->stride_by_plane[0]
: info->coded_size.width();
const int kYColsToSkipAtStart = info->visible_rect.x();
const int kYVisibleColCount = info->visible_rect.width();
const int kYCodedRowCount = info->coded_size.height();
const int kYRowsToSkipAtStart = info->visible_rect.y();
const int kYVisibleRowCount = info->visible_rect.height();
const int kUStride = info->strides ? info->strides->stride_by_plane[1]
: info->coded_size.width() / 2;
const int kUColsToSkipAtStart =
cc::MathUtil::UncheckedRoundDown(info->visible_rect.x(), 2) / 2;
const int kUVisibleColCount =
(cc::MathUtil::UncheckedRoundUp(info->visible_rect.right(), 2) / 2) -
kUColsToSkipAtStart;
const int kUCodedRowCount = info->coded_size.height() / 2;
const int kURowsToSkipAtStart =
cc::MathUtil::UncheckedRoundDown(info->visible_rect.y(), 2) / 2;
const int kUVisibleRowCount =
(cc::MathUtil::UncheckedRoundUp(info->visible_rect.bottom(), 2) / 2) -
kURowsToSkipAtStart;
const int kVStride = info->strides ? info->strides->stride_by_plane[2]
: info->coded_size.width() / 2;
uint8_t* write_ptr = outgoing_buffer->data();
FillVisiblePortionOfPlane(&write_ptr, dummy_value, kYCodedRowCount,
kYRowsToSkipAtStart, kYVisibleRowCount, kYStride,
kYColsToSkipAtStart, kYVisibleColCount);
FillVisiblePortionOfPlane(&write_ptr, dummy_value, kUCodedRowCount,
kURowsToSkipAtStart, kUVisibleRowCount, kUStride,
kUColsToSkipAtStart, kUVisibleColCount);
FillVisiblePortionOfPlane(&write_ptr, dummy_value, kUCodedRowCount,
kURowsToSkipAtStart, kUVisibleRowCount, kVStride,
kUColsToSkipAtStart, kUVisibleColCount);
virtual_device_->OnFrameReadyInBuffer(buffer_id, std::move(info));
}
void FillVisiblePortionOfPlane(uint8_t** write_ptr,
uint8_t fill_value,
int row_count,
int rows_to_skip_at_start,
int visible_row_count,
int col_count,
int cols_to_skip_at_start,
int visible_col_count) {
const int kColsToSkipAtEnd =
col_count - visible_col_count - cols_to_skip_at_start;
const int kRowsToSkipAtEnd =
row_count - visible_row_count - rows_to_skip_at_start;
// Skip rows at start
(*write_ptr) += col_count * rows_to_skip_at_start;
// Fill rows
for (int i = 0; i < visible_row_count; i++) {
// Skip cols at start
(*write_ptr) += cols_to_skip_at_start;
// Fill visible bytes
memset(*write_ptr, fill_value, visible_col_count);
(*write_ptr) += visible_col_count;
// Skip cols at end
(*write_ptr) += kColsToSkipAtEnd;
}
// Skip rows at end
(*write_ptr) += col_count * kRowsToSkipAtEnd;
}
media::mojom::PlaneStridesPtr strides_;
mojo::Binding<video_capture::mojom::Producer> producer_binding_;
video_capture::mojom::SharedMemoryVirtualDevicePtr virtual_device_;
std::map<int32_t /*buffer_id*/,
std::unique_ptr<media::SharedMemoryHandleProvider>>
outgoing_buffer_id_to_buffer_map_;
base::WeakPtrFactory<SharedMemoryDeviceExerciser> weak_factory_;
};
// Integration test that obtains a connection to the video capture service via
// the Browser process' service manager. It then registers a virtual device at
// the service and feeds frames to it. It opens the virtual device in a <video>
// element on a test page and verifies that the element plays in the expected
// dimenstions and the pixel content on the element changes.
class WebRtcVideoCaptureServiceBrowserTest : public ContentBrowserTest {
public:
WebRtcVideoCaptureServiceBrowserTest()
: virtual_device_thread_("Virtual Device Thread"), weak_factory_(this) {
scoped_feature_list_.InitAndEnableFeature(features::kMojoVideoCapture);
virtual_device_thread_.Start();
}
~WebRtcVideoCaptureServiceBrowserTest() override {}
void AddVirtualDeviceAndStartCapture(VirtualDeviceExerciser* device_exerciser,
base::OnceClosure finish_test_cb) {
DCHECK(virtual_device_thread_.task_runner()->RunsTasksInCurrentSequence());
connector_->BindInterface(video_capture::mojom::kServiceName, &provider_);
provider_->ConnectToDeviceFactory(mojo::MakeRequest(&factory_));
media::VideoCaptureDeviceInfo info;
info.descriptor.device_id = kVirtualDeviceId;
info.descriptor.set_display_name(kVirtualDeviceName);
info.descriptor.capture_api = media::VideoCaptureApi::VIRTUAL_DEVICE;
video_size_ = device_exerciser->GetVideoSize();
device_exerciser->RegisterVirtualDeviceAtFactory(&factory_, info);
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&WebRtcVideoCaptureServiceBrowserTest::
OpenVirtualDeviceInRendererAndWaitForPlaying,
base::Unretained(this),
media::BindToCurrentLoop(base::BindOnce(
&WebRtcVideoCaptureServiceBrowserTest::
ShutDownVirtualDeviceAndContinue,
base::Unretained(this), device_exerciser,
std::move(finish_test_cb)))));
PushDummyFrameAndScheduleNextPush(device_exerciser);
}
void PushDummyFrameAndScheduleNextPush(
VirtualDeviceExerciser* device_exerciser) {
DCHECK(virtual_device_thread_.task_runner()->RunsTasksInCurrentSequence());
device_exerciser->PushNextFrame(CalculateTimeSinceFirstInvocation());
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&WebRtcVideoCaptureServiceBrowserTest::
PushDummyFrameAndScheduleNextPush,
weak_factory_.GetWeakPtr(), device_exerciser),
base::TimeDelta::FromMilliseconds(1000 / kDummyFrameRate));
}
void ShutDownVirtualDeviceAndContinue(
VirtualDeviceExerciser* device_exerciser,
base::OnceClosure continuation) {
DCHECK(virtual_device_thread_.task_runner()->RunsTasksInCurrentSequence());
LOG(INFO) << "Shutting down virtual device";
device_exerciser->ShutDown();
factory_ = nullptr;
provider_ = nullptr;
weak_factory_.InvalidateWeakPtrs();
std::move(continuation).Run();
}
void OpenVirtualDeviceInRendererAndWaitForPlaying(
base::OnceClosure finish_test_cb) {
DCHECK(main_task_runner_->RunsTasksInCurrentSequence());
embedded_test_server()->StartAcceptingConnections();
GURL url(embedded_test_server()->GetURL(kVideoCaptureHtmlFile));
NavigateToURL(shell(), url);
std::string javascript_to_execute = base::StringPrintf(
kStartVideoCaptureAndVerify, video_size_.width(), video_size_.height());
std::string result;
// Start video capture and wait until it started rendering
ASSERT_TRUE(
ExecuteScriptAndExtractString(shell(), javascript_to_execute, &result));
ASSERT_EQ("OK", result);
std::move(finish_test_cb).Run();
}
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
// Note: We are not planning to actually use the fake device, but we want
// to avoid enumerating or otherwise calling into real capture devices.
command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream);
command_line->AppendSwitch(switches::kUseFakeUIForMediaStream);
}
void SetUp() override {
ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
EnablePixelOutput();
ContentBrowserTest::SetUp();
}
void Initialize() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
main_task_runner_ = base::ThreadTaskRunnerHandle::Get();
auto* connection = content::ServiceManagerConnection::GetForProcess();
ASSERT_TRUE(connection);
auto* connector = connection->GetConnector();
ASSERT_TRUE(connector);
// We need to clone it so that we can use the clone on a different thread.
connector_ = connector->Clone();
}
base::Thread virtual_device_thread_;
scoped_refptr<base::TaskRunner> main_task_runner_;
std::unique_ptr<service_manager::Connector> connector_;
private:
base::TimeDelta CalculateTimeSinceFirstInvocation() {
if (first_frame_time_.is_null())
first_frame_time_ = base::TimeTicks::Now();
return base::TimeTicks::Now() - first_frame_time_;
}
base::test::ScopedFeatureList scoped_feature_list_;
video_capture::mojom::DeviceFactoryProviderPtr provider_;
video_capture::mojom::DeviceFactoryPtr factory_;
gfx::Size video_size_;
base::TimeTicks first_frame_time_;
base::WeakPtrFactory<WebRtcVideoCaptureServiceBrowserTest> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(WebRtcVideoCaptureServiceBrowserTest);
};
IN_PROC_BROWSER_TEST_F(
WebRtcVideoCaptureServiceBrowserTest,
FramesSentThroughTextureVirtualDeviceGetDisplayedOnPage) {
Initialize();
auto device_exerciser = std::make_unique<TextureDeviceExerciser>();
device_exerciser->Initialize();
base::RunLoop run_loop;
virtual_device_thread_.task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&WebRtcVideoCaptureServiceBrowserTest::
AddVirtualDeviceAndStartCapture,
base::Unretained(this), device_exerciser.get(),
media::BindToCurrentLoop(run_loop.QuitClosure())));
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(
WebRtcVideoCaptureServiceBrowserTest,
FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage) {
Initialize();
auto device_exerciser = std::make_unique<SharedMemoryDeviceExerciser>();
device_exerciser->Initialize();
base::RunLoop run_loop;
virtual_device_thread_.task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&WebRtcVideoCaptureServiceBrowserTest::
AddVirtualDeviceAndStartCapture,
base::Unretained(this), device_exerciser.get(),
media::BindToCurrentLoop(run_loop.QuitClosure())));
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(
WebRtcVideoCaptureServiceBrowserTest,
PaddedI420FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage) {
Initialize();
auto device_exerciser = std::make_unique<SharedMemoryDeviceExerciser>(
media::mojom::PlaneStrides::New(
std::vector<uint32_t>({1024u, 512u, 1024u, 0u})));
device_exerciser->Initialize();
base::RunLoop run_loop;
virtual_device_thread_.task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&WebRtcVideoCaptureServiceBrowserTest::
AddVirtualDeviceAndStartCapture,
base::Unretained(this), device_exerciser.get(),
media::BindToCurrentLoop(run_loop.QuitClosure())));
run_loop.Run();
}
} // namespace content
#endif // defined(CAN_USE_IMAGE_TRANSPORT_FACTORY)