| // 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 <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/memory/read_only_shared_memory_region.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "content/browser/devtools/devtools_video_consumer.h" |
| #include "content/public/test/test_utils.h" |
| #include "media/base/limits.h" |
| #include "media/capture/mojom/video_capture_types.mojom.h" |
| #include "mojo/public/cpp/base/shared_memory_utils.h" |
| #include "mojo/public/cpp/bindings/binding.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| |
| using testing::_; |
| |
| namespace content { |
| namespace { |
| |
| // Capture parameters. |
| constexpr gfx::Size kResolution = gfx::Size(320, 180); // Arbitrarily chosen. |
| constexpr media::VideoPixelFormat kFormat = media::PIXEL_FORMAT_I420; |
| |
| // A non-zero FrameSinkId to prevent validation errors when |
| // DevToolsVideoConsumer::ChangeTarget(viz::FrameSinkId) is called |
| // (which eventually fails in FrameSinkVideoCapturerStubDispatch::Accept). |
| constexpr viz::FrameSinkId kInitialFrameSinkId = viz::FrameSinkId(1, 1); |
| |
| } // namespace |
| |
| // Mock for the FrameSinkVideoCapturer running in the VIZ process. |
| class MockFrameSinkVideoCapturer : public viz::mojom::FrameSinkVideoCapturer { |
| public: |
| MockFrameSinkVideoCapturer() : binding_(this) {} |
| |
| bool is_bound() const { return binding_.is_bound(); } |
| |
| void Bind(viz::mojom::FrameSinkVideoCapturerRequest request) { |
| DCHECK(!binding_.is_bound()); |
| binding_.Bind(std::move(request)); |
| } |
| |
| void Reset() { |
| binding_.Close(); |
| consumer_.reset(); |
| } |
| |
| // This is never called. |
| MOCK_METHOD2(SetFormat, |
| void(media::VideoPixelFormat format, |
| const gfx::ColorSpace& color_space)); |
| void SetMinCapturePeriod(base::TimeDelta min_capture_period) final { |
| min_capture_period_ = min_capture_period; |
| MockSetMinCapturePeriod(min_capture_period_); |
| } |
| MOCK_METHOD1(MockSetMinCapturePeriod, |
| void(base::TimeDelta min_capture_period)); |
| void SetMinSizeChangePeriod(base::TimeDelta min_period) final { |
| min_period_ = min_period; |
| MockSetMinSizeChangePeriod(min_period_); |
| } |
| MOCK_METHOD1(MockSetMinSizeChangePeriod, void(base::TimeDelta min_period)); |
| void SetResolutionConstraints(const gfx::Size& min_frame_size, |
| const gfx::Size& max_frame_size, |
| bool use_fixed_aspect_ratio) final { |
| min_frame_size_ = min_frame_size; |
| max_frame_size_ = max_frame_size; |
| MockSetResolutionConstraints(min_frame_size_, max_frame_size_, true); |
| } |
| MOCK_METHOD3(MockSetResolutionConstraints, |
| void(const gfx::Size& min_frame_size, |
| const gfx::Size& max_frame_size, |
| bool use_fixed_aspect_ratio)); |
| // This is never called. |
| MOCK_METHOD1(SetAutoThrottlingEnabled, void(bool)); |
| void ChangeTarget( |
| const base::Optional<viz::FrameSinkId>& frame_sink_id) final { |
| frame_sink_id_ = frame_sink_id ? *frame_sink_id : viz::FrameSinkId(); |
| MockChangeTarget(frame_sink_id_); |
| } |
| MOCK_METHOD1(MockChangeTarget, void(const viz::FrameSinkId& frame_sink_id)); |
| void Start(viz::mojom::FrameSinkVideoConsumerPtr consumer) final { |
| DCHECK(!consumer_); |
| consumer_ = std::move(consumer); |
| MockStart(consumer_.get()); |
| } |
| MOCK_METHOD1(MockStart, void(viz::mojom::FrameSinkVideoConsumer* consumer)); |
| void Stop() final { |
| binding_.Close(); |
| consumer_.reset(); |
| MockStop(); |
| } |
| MOCK_METHOD0(MockStop, void()); |
| MOCK_METHOD0(RequestRefreshFrame, void()); |
| MOCK_METHOD2(CreateOverlay, |
| void(int32_t stacking_index, |
| viz::mojom::FrameSinkVideoCaptureOverlayRequest request)); |
| |
| // Const accessors to get the cached variables. |
| base::TimeDelta min_capture_period() const { return min_capture_period_; } |
| base::TimeDelta min_period() const { return min_period_; } |
| gfx::Size min_frame_size() const { return min_frame_size_; } |
| gfx::Size max_frame_size() const { return max_frame_size_; } |
| viz::FrameSinkId frame_sink_id() const { return frame_sink_id_; } |
| |
| private: |
| // These variables are cached when they are received from |
| // DevToolsVideoConsumer. |
| base::TimeDelta min_capture_period_; |
| base::TimeDelta min_period_; |
| gfx::Size min_frame_size_; |
| gfx::Size max_frame_size_; |
| viz::FrameSinkId frame_sink_id_; |
| viz::mojom::FrameSinkVideoConsumerPtr consumer_; |
| |
| mojo::Binding<viz::mojom::FrameSinkVideoCapturer> binding_; |
| }; |
| |
| // Represents the FrameSinkVideoConsumerFrameCallbacks instance in the VIZ |
| // process. |
| class MockFrameSinkVideoConsumerFrameCallbacks |
| : public viz::mojom::FrameSinkVideoConsumerFrameCallbacks { |
| public: |
| MockFrameSinkVideoConsumerFrameCallbacks() : binding_(this) {} |
| |
| void Bind(viz::mojom::FrameSinkVideoConsumerFrameCallbacksRequest request) { |
| binding_.Bind(std::move(request)); |
| } |
| |
| MOCK_METHOD0(Done, void()); |
| MOCK_METHOD1(ProvideFeedback, void(double utilization)); |
| |
| private: |
| mojo::Binding<viz::mojom::FrameSinkVideoConsumerFrameCallbacks> binding_; |
| }; |
| |
| // Mock for the classes like TracingHandler that receive frames from |
| // DevToolsVideoConsumer via the OnFrameCapturedCallback. |
| class MockDevToolsVideoFrameReceiver { |
| public: |
| MOCK_METHOD1(OnFrameFromVideoConsumerMock, |
| void(scoped_refptr<media::VideoFrame> frame)); |
| |
| MockDevToolsVideoFrameReceiver() : weak_factory_(this) {} |
| |
| scoped_refptr<media::VideoFrame> TakeFrameAt(int i) { |
| return std::move(frames_[i]); |
| } |
| |
| void OnFrameFromVideoConsumer(scoped_refptr<media::VideoFrame> frame) { |
| OnFrameFromVideoConsumerMock(frame); |
| frames_.push_back(std::move(frame)); |
| } |
| |
| std::unique_ptr<DevToolsVideoConsumer> CreateDevToolsVideoConsumer() { |
| return std::make_unique<DevToolsVideoConsumer>(base::BindRepeating( |
| &MockDevToolsVideoFrameReceiver::OnFrameFromVideoConsumer, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| private: |
| std::vector<scoped_refptr<media::VideoFrame>> frames_; |
| base::WeakPtrFactory<MockDevToolsVideoFrameReceiver> weak_factory_; |
| }; |
| |
| class DevToolsVideoConsumerTest : public testing::Test { |
| public: |
| DevToolsVideoConsumerTest() : weak_factory_(this) {} |
| |
| void SetUp() override { |
| consumer_ = receiver_.CreateDevToolsVideoConsumer(); |
| |
| consumer_->SetFrameSinkId(kInitialFrameSinkId); |
| } |
| |
| void SimulateFrameCapture(base::ReadOnlySharedMemoryRegion data) { |
| viz::mojom::FrameSinkVideoConsumerFrameCallbacksPtr callbacks_ptr; |
| callbacks.Bind(mojo::MakeRequest(&callbacks_ptr)); |
| |
| media::mojom::VideoFrameInfoPtr info = media::mojom::VideoFrameInfo::New( |
| base::TimeDelta(), base::Value(base::Value::Type::DICTIONARY), kFormat, |
| kResolution, gfx::Rect(kResolution), gfx::ColorSpace::CreateREC709(), |
| nullptr); |
| |
| consumer_->OnFrameCaptured(std::move(data), std::move(info), |
| gfx::Rect(kResolution), gfx::Rect(kResolution), |
| std::move(callbacks_ptr)); |
| } |
| |
| void StartCaptureWithMockCapturer() { |
| consumer_->InnerStartCapture(CreateMockCapturer()); |
| } |
| |
| bool IsValidMinAndMaxFrameSize(gfx::Size min_frame_size, |
| gfx::Size max_frame_size) { |
| return consumer_->IsValidMinAndMaxFrameSize(min_frame_size, max_frame_size); |
| } |
| |
| static gfx::Size GetVideoConsumerDefaultMinFrameSize() { |
| return DevToolsVideoConsumer::kDefaultMinFrameSize; |
| } |
| |
| static gfx::Size GetVideoConsumerDefaultMaxFrameSize() { |
| return DevToolsVideoConsumer::kDefaultMaxFrameSize; |
| } |
| |
| // Getters for |consumer_|'s private variables. |
| base::TimeDelta GetMinCapturePeriod() const { |
| return consumer_->min_capture_period_; |
| } |
| gfx::Size GetMinFrameSize() const { return consumer_->min_frame_size_; } |
| gfx::Size GetMaxFrameSize() const { return consumer_->max_frame_size_; } |
| viz::FrameSinkId GetFrameSinkId() const { return consumer_->frame_sink_id_; } |
| |
| protected: |
| MockFrameSinkVideoCapturer capturer_; |
| MockFrameSinkVideoConsumerFrameCallbacks callbacks; |
| MockDevToolsVideoFrameReceiver receiver_; |
| std::unique_ptr<DevToolsVideoConsumer> consumer_; |
| |
| private: |
| std::unique_ptr<viz::ClientFrameSinkVideoCapturer> CreateMockCapturer() { |
| return std::make_unique<viz::ClientFrameSinkVideoCapturer>( |
| base::BindRepeating( |
| [](base::WeakPtr<DevToolsVideoConsumerTest> self, |
| viz::mojom::FrameSinkVideoCapturerRequest request) { |
| self->capturer_.Bind(std::move(request)); |
| }, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| base::test::ScopedTaskEnvironment task_environment_; |
| base::WeakPtrFactory<DevToolsVideoConsumerTest> weak_factory_; |
| }; |
| |
| // Tests that the OnFrameFromVideoConsumer callbacks is called when |
| // OnFrameCaptured is passed a valid buffer with valid mapping. |
| TEST_F(DevToolsVideoConsumerTest, CallbacksAreCalledWhenBufferValid) { |
| // On valid buffer the |receiver_| gets a frame via OnFrameFromVideoConsumer. |
| EXPECT_CALL(receiver_, OnFrameFromVideoConsumerMock(_)).Times(1); |
| |
| auto region = mojo::CreateReadOnlySharedMemoryRegion( |
| media::VideoFrame::AllocationSize(kFormat, kResolution)) |
| .region; |
| ASSERT_TRUE(region.IsValid()); |
| SimulateFrameCapture(std::move(region)); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Tests that only the OnFrameFromVideoConsumer callback is not called when |
| // OnFrameCaptured is passed an invalid buffer. |
| TEST_F(DevToolsVideoConsumerTest, CallbackIsNotCalledWhenBufferIsNotValid) { |
| // On invalid buffer, the |receiver_| doesn't get a frame. |
| EXPECT_CALL(receiver_, OnFrameFromVideoConsumerMock(_)).Times(0); |
| |
| SimulateFrameCapture(base::ReadOnlySharedMemoryRegion()); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Tests that the OnFrameFromVideoConsumer callback is not called when |
| // OnFrameCaptured is passed a buffer with less-than-expected size. |
| TEST_F(DevToolsVideoConsumerTest, CallbackIsNotCalledWhenBufferIsTooSmall) { |
| // On invalid mapping, the |receiver_| doesn't get a frame. |
| EXPECT_CALL(receiver_, OnFrameFromVideoConsumerMock(_)).Times(0); |
| |
| constexpr size_t too_few_number_of_bytes = 4; |
| ASSERT_LT(too_few_number_of_bytes, |
| media::VideoFrame::AllocationSize(kFormat, kResolution)); |
| auto region = |
| mojo::CreateReadOnlySharedMemoryRegion(too_few_number_of_bytes).region; |
| ASSERT_TRUE(region.IsValid()); |
| SimulateFrameCapture(std::move(region)); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Tests that starting capture calls |capturer_| functions, and capture can be |
| // restarted. This test is important as it ensures that when restarting capture, |
| // a FrameSinkVideoCapturerPtrInfo is bound to |capturer_| and it verifies that |
| // resources used in the previous StartCapture aren't reused. |
| TEST_F(DevToolsVideoConsumerTest, StartCaptureCallsSetFunctions) { |
| // Starting capture should call these |capturer_| functions once. |
| EXPECT_CALL(capturer_, MockSetMinCapturePeriod(_)); |
| EXPECT_CALL(capturer_, MockSetMinSizeChangePeriod(_)); |
| EXPECT_CALL(capturer_, MockSetResolutionConstraints(_, _, _)); |
| EXPECT_CALL(capturer_, MockChangeTarget(_)); |
| EXPECT_CALL(capturer_, MockStart(_)); |
| StartCaptureWithMockCapturer(); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Stop capturing. |
| consumer_->StopCapture(); |
| |
| // Reset the mock to allow the next consumer to connect. |
| capturer_.Reset(); |
| |
| // Start capturing again, and expect that these |capturer_| functions are |
| // called once. This will re-bind the |capturer_| and ensures that destroyed |
| // resources aren't being reused. |
| EXPECT_CALL(capturer_, MockSetMinCapturePeriod(_)); |
| EXPECT_CALL(capturer_, MockSetMinSizeChangePeriod(_)); |
| EXPECT_CALL(capturer_, MockSetResolutionConstraints(_, _, _)); |
| EXPECT_CALL(capturer_, MockChangeTarget(_)); |
| EXPECT_CALL(capturer_, MockStart(_)); |
| StartCaptureWithMockCapturer(); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Tests that calling 'Set' functions in DevToolsVideoConsumer before |
| // |capturer_| is initialized results in the passed values being cached. |
| // When capture is later started (and |capturer_| initialized), these cached |
| // values should be used and sent to the |capturer_|. |
| TEST_F(DevToolsVideoConsumerTest, CapturerIsPassedCachedValues) { |
| // These values are chosen so that they are valid, and different from |
| // the default values in DevToolsVideoConsumer. |
| constexpr base::TimeDelta kNewMinCapturePeriod = base::TimeDelta(); |
| const gfx::Size kNewMinFrameSize = |
| gfx::Size(GetVideoConsumerDefaultMinFrameSize().width() + 1, |
| GetVideoConsumerDefaultMinFrameSize().height() + 1); |
| const gfx::Size kNewMaxFrameSize = |
| gfx::Size(GetVideoConsumerDefaultMaxFrameSize().width() + 1, |
| GetVideoConsumerDefaultMaxFrameSize().width() + 1); |
| constexpr viz::FrameSinkId kNewFrameSinkId = viz::FrameSinkId(2, 2); |
| |
| // Right now, |capturer_| has not been created via StartCapture, so |
| // calling these functions should not call the |capturer_|, but the |
| // values that are passed in should be cached. |
| EXPECT_CALL(capturer_, MockSetMinCapturePeriod(_)).Times(0); |
| EXPECT_CALL(capturer_, MockSetMinSizeChangePeriod(_)).Times(0); |
| EXPECT_CALL(capturer_, MockSetResolutionConstraints(_, _, _)).Times(0); |
| EXPECT_CALL(capturer_, MockChangeTarget(_)).Times(0); |
| EXPECT_CALL(capturer_, MockStart(_)).Times(0); |
| consumer_->SetMinCapturePeriod(kNewMinCapturePeriod); |
| consumer_->SetMinAndMaxFrameSize(kNewMinFrameSize, kNewMaxFrameSize); |
| consumer_->SetFrameSinkId(kNewFrameSinkId); |
| base::RunLoop().RunUntilIdle(); |
| // Verify that new values are cached. |
| EXPECT_EQ(GetMinCapturePeriod(), kNewMinCapturePeriod); |
| EXPECT_EQ(GetMinFrameSize(), kNewMinFrameSize); |
| EXPECT_EQ(GetMaxFrameSize(), kNewMaxFrameSize); |
| EXPECT_EQ(GetFrameSinkId(), kNewFrameSinkId); |
| |
| // Starting capture now, will result in the cached values being sent to |
| // |capturer_|. So, expect that these calls are made and verify the values. |
| EXPECT_CALL(capturer_, MockSetMinCapturePeriod(_)); |
| EXPECT_CALL(capturer_, MockSetMinSizeChangePeriod(_)); |
| EXPECT_CALL(capturer_, MockSetResolutionConstraints(_, _, _)); |
| EXPECT_CALL(capturer_, MockChangeTarget(_)); |
| EXPECT_CALL(capturer_, MockStart(_)); |
| StartCaptureWithMockCapturer(); |
| base::RunLoop().RunUntilIdle(); |
| // Verify that the previously cached values are sent to |capturer_|. |
| EXPECT_EQ(capturer_.min_capture_period(), kNewMinCapturePeriod); |
| EXPECT_EQ(capturer_.min_frame_size(), kNewMinFrameSize); |
| EXPECT_EQ(capturer_.max_frame_size(), kNewMaxFrameSize); |
| EXPECT_EQ(capturer_.frame_sink_id(), kNewFrameSinkId); |
| } |
| |
| // Tests that DevToolsVideoConsumer::IsValidMinAndMaxFrameSize adheres to the |
| // limits set by media::limits::kMaxDimension |
| TEST_F(DevToolsVideoConsumerTest, IsValidMinAndMaxFrameSize) { |
| // Choosing valid frame sizes with |
| // kNormalMinSize.height() > kNormalMaxSize.width() so that width |
| // and height are not interchanged in this test. |
| constexpr gfx::Size kNormalMinSize = gfx::Size(50, 150); |
| constexpr gfx::Size kNormalMaxSize = gfx::Size(100, 200); |
| |
| // Testing success cases. |
| EXPECT_TRUE(IsValidMinAndMaxFrameSize(kNormalMinSize, kNormalMaxSize)); |
| // Non-zero frames that are equal should pass. |
| EXPECT_TRUE(IsValidMinAndMaxFrameSize(kNormalMinSize, kNormalMaxSize)); |
| // Swapping width and height of frames should pass. |
| EXPECT_TRUE(IsValidMinAndMaxFrameSize( |
| gfx::Size(kNormalMinSize.height(), kNormalMinSize.width()), |
| gfx::Size(kNormalMaxSize.height(), kNormalMaxSize.width()))); |
| |
| // Testing failure cases. |
| // |min_frame_size|.width() should be > 0 |
| EXPECT_FALSE(IsValidMinAndMaxFrameSize(gfx::Size(0, kNormalMinSize.height()), |
| kNormalMaxSize)); |
| // |min_frame_size|.height() should be > 0 |
| EXPECT_FALSE(IsValidMinAndMaxFrameSize(gfx::Size(kNormalMinSize.width(), 0), |
| kNormalMaxSize)); |
| // |min_frame_size|.width() should be <= |max_frame_size|.width() |
| EXPECT_FALSE(IsValidMinAndMaxFrameSize( |
| gfx::Size(kNormalMaxSize.width() + 1, kNormalMinSize.height()), |
| kNormalMaxSize)); |
| // |max_frame_size|.height() should be <= |max_frame_size|.height() |
| EXPECT_FALSE(IsValidMinAndMaxFrameSize( |
| gfx::Size(kNormalMinSize.width(), kNormalMaxSize.height() + 1), |
| kNormalMaxSize)); |
| // |max_frame_size|.height() should be <= media::limits::kMaxDimension |
| EXPECT_FALSE(IsValidMinAndMaxFrameSize( |
| kNormalMinSize, |
| gfx::Size(kNormalMaxSize.width(), media::limits::kMaxDimension + 1))); |
| // |max_frame_size|.width() should be <= media::limits::kMaxDimension |
| EXPECT_FALSE(IsValidMinAndMaxFrameSize( |
| kNormalMinSize, |
| gfx::Size(media::limits::kMaxDimension + 1, kNormalMaxSize.height()))); |
| } |
| |
| } // namespace content |