| // 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 "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 kStartVideoCaptureAndVerifySize[] = |
| "startVideoCaptureFromDeviceNamedVirtualDeviceAndVerifySize()"; |
| |
| static const char kVirtualDeviceId[] = "/virtual/device"; |
| static const char kVirtualDeviceName[] = "Virtual Device"; |
| |
| static const gfx::Size kDummyFrameDimensions(320, 200); |
| 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 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; |
| } |
| |
| 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 = kDummyFrameDimensions; |
| info->visible_rect = gfx::Rect(kDummyFrameDimensions.width(), |
| kDummyFrameDimensions.height()); |
| 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 = kDummyFrameDimensions.width() * |
| kDummyFrameDimensions.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, kDummyFrameDimensions.width(), |
| kDummyFrameDimensions.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. |
| class SharedMemoryDeviceExerciser : public VirtualDeviceExerciser, |
| public video_capture::mojom::Producer { |
| public: |
| SharedMemoryDeviceExerciser() |
| : 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_)); |
| } |
| void PushNextFrame(base::TimeDelta timestamp) override { |
| virtual_device_->RequestFrameBuffer( |
| kDummyFrameDimensions, media::VideoPixelFormat::PIXEL_FORMAT_I420, |
| 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 = kDummyFrameDimensions; |
| info->visible_rect = gfx::Rect(kDummyFrameDimensions.width(), |
| kDummyFrameDimensions.height()); |
| info->metadata = metadata.GetInternalValues().Clone(); |
| |
| auto outgoing_buffer = outgoing_buffer_id_to_buffer_map_.at(buffer_id) |
| ->GetHandleForInProcessAccess(); |
| |
| static int frame_count = 0; |
| frame_count++; |
| memset(outgoing_buffer->data(), frame_count % 256, |
| outgoing_buffer->mapped_size()); |
| |
| virtual_device_->OnFrameReadyInBuffer(buffer_id, std::move(info)); |
| } |
| |
| 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; |
| |
| 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 result; |
| // Start video capture and wait until it started rendering |
| ASSERT_TRUE(ExecuteScriptAndExtractString( |
| shell(), kStartVideoCaptureAndVerifySize, &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_; |
| 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(); |
| } |
| |
| } // namespace content |
| |
| #endif // defined(CAN_USE_IMAGE_TRANSPORT_FACTORY) |