| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.h" |
| |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/memory/shared_memory_mapping.h" |
| #include "base/optional.h" |
| #include "base/run_loop.h" |
| #include "base/test/test_mock_time_task_runner.h" |
| #include "base/time/time.h" |
| #include "components/viz/common/frame_sinks/begin_frame_args.h" |
| #include "components/viz/common/frame_sinks/copy_output_request.h" |
| #include "components/viz/common/frame_sinks/copy_output_result.h" |
| #include "components/viz/common/frame_sinks/copy_output_util.h" |
| #include "components/viz/common/surfaces/subtree_capture_id.h" |
| #include "components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_manager.h" |
| #include "media/base/limits.h" |
| #include "media/base/video_util.h" |
| #include "media/capture/mojom/video_capture_types.mojom.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "services/viz/privileged/mojom/compositing/frame_sink_video_capture.mojom.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/gfx/color_space.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| |
| using media::VideoCaptureOracle; |
| using media::VideoFrame; |
| using media::VideoFrameMetadata; |
| |
| using testing::_; |
| using testing::InvokeWithoutArgs; |
| using testing::NiceMock; |
| using testing::Return; |
| |
| namespace viz { |
| namespace { |
| |
| bool AlignsWithI420SubsamplingBoundaries(const gfx::Rect& update_rect) { |
| return (update_rect.x() % 2 == 0) && (update_rect.y() % 2 == 0) && |
| (update_rect.width() % 2 == 0) && (update_rect.height() % 2 == 0); |
| } |
| |
| // Returns true if |frame|'s device scale factor, page scale factor and root |
| // scroll offset are equal to the expected values. |
| bool CompareVarsInCompositorFrameMetadata( |
| const VideoFrame& frame, |
| float device_scale_factor, |
| float page_scale_factor, |
| const gfx::Vector2dF& root_scroll_offset) { |
| auto dsf = frame.metadata()->device_scale_factor; |
| auto psf = frame.metadata()->page_scale_factor; |
| auto rso_x = frame.metadata()->root_scroll_offset_x; |
| auto rso_y = frame.metadata()->root_scroll_offset_y; |
| |
| bool valid = dsf.has_value() && psf.has_value() && rso_x.has_value() && |
| rso_y.has_value(); |
| |
| return valid && *dsf == device_scale_factor && *psf == page_scale_factor && |
| gfx::Vector2dF(*rso_x, *rso_y) == root_scroll_offset; |
| } |
| |
| // Dummy frame sink ID. |
| constexpr FrameSinkId kFrameSinkId = FrameSinkId(1, 1); |
| |
| // The compositor frame interval. |
| constexpr auto kVsyncInterval = base::TimeDelta::FromSeconds(1) / 60; |
| |
| const struct SizeSet { |
| // The size of the compositor frame sink's Surface. |
| gfx::Size source_size; |
| // The size of the VideoFrames produced by the capturer. |
| gfx::Size capture_size; |
| // The location of the letterboxed content within each VideoFrame. All pixels |
| // outside of this region should be black. |
| gfx::Rect expected_content_rect; |
| } kSizeSets[4] = { |
| {gfx::Size(100, 100), gfx::Size(32, 18), gfx::Rect(6, 0, 18, 18)}, |
| {gfx::Size(64, 18), gfx::Size(32, 18), gfx::Rect(0, 4, 32, 8)}, |
| {gfx::Size(64, 18), gfx::Size(64, 18), gfx::Rect(0, 0, 64, 18)}, |
| {gfx::Size(100, 100), gfx::Size(16, 8), gfx::Rect(0, 0, 8, 8)}}; |
| |
| constexpr float kDefaultDeviceScaleFactor = 1.f; |
| constexpr float kDefaultPageScaleFactor = 1.f; |
| constexpr gfx::Vector2dF kDefaultRootScrollOffset = gfx::Vector2dF(0, 0); |
| |
| struct YUVColor { |
| uint8_t y; |
| uint8_t u; |
| uint8_t v; |
| }; |
| |
| // Forces any pending Mojo method calls between the capturer and consumer to be |
| // made. |
| void PropagateMojoTasks( |
| scoped_refptr<base::TestMockTimeTaskRunner> runner = nullptr) { |
| if (runner) { |
| runner->RunUntilIdle(); |
| } |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| class MockFrameSinkManager : public FrameSinkVideoCapturerManager { |
| public: |
| MOCK_METHOD1(FindCapturableFrameSink, |
| CapturableFrameSink*(const FrameSinkId& frame_sink_id)); |
| MOCK_METHOD1(OnCapturerConnectionLost, |
| void(FrameSinkVideoCapturerImpl* capturer)); |
| }; |
| |
| class MockConsumer : public mojom::FrameSinkVideoConsumer { |
| public: |
| MockConsumer() {} |
| |
| MOCK_METHOD0(OnFrameCapturedMock, void()); |
| MOCK_METHOD0(OnStopped, void()); |
| MOCK_METHOD1(OnLog, void(const std::string&)); |
| |
| int num_frames_received() const { return frames_.size(); } |
| |
| scoped_refptr<VideoFrame> TakeFrame(int i) { return std::move(frames_[i]); } |
| |
| void SendDoneNotification(int i) { |
| std::move(done_callbacks_[i]).Run(); |
| PropagateMojoTasks(); |
| } |
| |
| mojo::PendingRemote<mojom::FrameSinkVideoConsumer> BindVideoConsumer() { |
| return receiver_.BindNewPipeAndPassRemote(); |
| } |
| |
| private: |
| void OnFrameCaptured( |
| base::ReadOnlySharedMemoryRegion data, |
| media::mojom::VideoFrameInfoPtr info, |
| const gfx::Rect& expected_content_rect, |
| mojo::PendingRemote<mojom::FrameSinkVideoConsumerFrameCallbacks> |
| callbacks) final { |
| ASSERT_TRUE(data.IsValid()); |
| const auto required_bytes_to_hold_planes = |
| static_cast<uint32_t>(info->coded_size.GetArea() * 3 / 2); |
| ASSERT_LE(required_bytes_to_hold_planes, data.GetSize()); |
| ASSERT_TRUE(info); |
| |
| mojo::Remote<mojom::FrameSinkVideoConsumerFrameCallbacks> callbacks_remote( |
| std::move(callbacks)); |
| ASSERT_TRUE(callbacks_remote.get()); |
| |
| // Map the shared memory buffer and re-constitute a VideoFrame instance |
| // around it for analysis via TakeFrame(). |
| base::ReadOnlySharedMemoryMapping mapping = data.Map(); |
| ASSERT_TRUE(mapping.IsValid()); |
| ASSERT_LE( |
| media::VideoFrame::AllocationSize(info->pixel_format, info->coded_size), |
| mapping.size()); |
| scoped_refptr<media::VideoFrame> frame = |
| media::VideoFrame::WrapExternalData( |
| info->pixel_format, info->coded_size, info->visible_rect, |
| info->visible_rect.size(), |
| const_cast<uint8_t*>(static_cast<const uint8_t*>(mapping.memory())), |
| mapping.size(), info->timestamp); |
| ASSERT_TRUE(frame); |
| frame->set_metadata(info->metadata); |
| if (info->color_space.has_value()) |
| frame->set_color_space(info->color_space.value()); |
| |
| frame->AddDestructionObserver(base::BindOnce( |
| [](base::ReadOnlySharedMemoryMapping mapping) {}, std::move(mapping))); |
| OnFrameCapturedMock(); |
| |
| frames_.push_back(std::move(frame)); |
| done_callbacks_.push_back( |
| base::BindOnce(&mojom::FrameSinkVideoConsumerFrameCallbacks::Done, |
| std::move(callbacks_remote))); |
| } |
| |
| mojo::Receiver<mojom::FrameSinkVideoConsumer> receiver_{this}; |
| std::vector<scoped_refptr<VideoFrame>> frames_; |
| std::vector<base::OnceClosure> done_callbacks_; |
| }; |
| |
| class SolidColorI420Result : public CopyOutputResult { |
| public: |
| SolidColorI420Result(const gfx::Rect rect, YUVColor color) |
| : CopyOutputResult(CopyOutputResult::Format::I420_PLANES, rect), |
| color_(color) {} |
| |
| bool ReadI420Planes(uint8_t* y_out, |
| int y_out_stride, |
| uint8_t* u_out, |
| int u_out_stride, |
| uint8_t* v_out, |
| int v_out_stride) const final { |
| CHECK(y_out); |
| CHECK(y_out_stride >= size().width()); |
| CHECK(u_out); |
| const int chroma_width = (size().width() + 1) / 2; |
| CHECK(u_out_stride >= chroma_width); |
| CHECK(v_out); |
| CHECK(v_out_stride >= chroma_width); |
| for (int i = 0; i < size().height(); ++i, y_out += y_out_stride) { |
| memset(y_out, color_.y, size().width()); |
| } |
| const int chroma_height = (size().height() + 1) / 2; |
| for (int i = 0; i < chroma_height; ++i, u_out += u_out_stride) { |
| memset(u_out, color_.u, chroma_width); |
| } |
| for (int i = 0; i < chroma_height; ++i, v_out += v_out_stride) { |
| memset(v_out, color_.v, chroma_width); |
| } |
| return true; |
| } |
| |
| private: |
| const YUVColor color_; |
| }; |
| |
| class FakeCapturableFrameSink : public CapturableFrameSink { |
| public: |
| FakeCapturableFrameSink() : size_set_(kSizeSets[0]) { |
| metadata_.root_scroll_offset = kDefaultRootScrollOffset; |
| metadata_.page_scale_factor = kDefaultPageScaleFactor; |
| metadata_.device_scale_factor = kDefaultDeviceScaleFactor; |
| } |
| |
| Client* attached_client() const { return client_; } |
| |
| void AttachCaptureClient(Client* client) override { |
| ASSERT_FALSE(client_); |
| ASSERT_TRUE(client); |
| client_ = client; |
| } |
| |
| void DetachCaptureClient(Client* client) override { |
| ASSERT_TRUE(client); |
| ASSERT_EQ(client, client_); |
| client_ = nullptr; |
| } |
| |
| gfx::Size GetActiveFrameSize() override { return source_size(); } |
| |
| void RequestCopyOfOutput( |
| PendingCopyOutputRequest pending_copy_output_request) override { |
| auto& request = pending_copy_output_request.copy_output_request; |
| EXPECT_EQ(CopyOutputResult::Format::I420_PLANES, request->result_format()); |
| EXPECT_NE(base::UnguessableToken(), request->source()); |
| EXPECT_EQ(gfx::Rect(size_set_.source_size), request->area()); |
| EXPECT_EQ(gfx::Rect(size_set_.expected_content_rect.size()), |
| request->result_selection()); |
| |
| auto result = std::make_unique<SolidColorI420Result>( |
| request->result_selection(), color_); |
| results_.push_back(base::BindOnce( |
| [](std::unique_ptr<CopyOutputRequest> request, |
| std::unique_ptr<CopyOutputResult> result) { |
| request->SendResult(std::move(result)); |
| }, |
| std::move(request), std::move(result))); |
| } |
| |
| const CompositorFrameMetadata* GetLastActivatedFrameMetadata() override { |
| return &metadata_; |
| } |
| |
| const gfx::Size& source_size() const { return size_set_.source_size; } |
| |
| void set_size_set(const SizeSet& size_set) { size_set_ = size_set; } |
| |
| void set_metadata(const CompositorFrameMetadata& metadata) { |
| metadata_ = metadata.Clone(); |
| } |
| |
| void SetCopyOutputColor(YUVColor color) { color_ = color; } |
| |
| int num_copy_results() const { return results_.size(); } |
| |
| void SendCopyOutputResult(int offset) { |
| auto it = results_.begin() + offset; |
| std::move(*it).Run(); |
| PropagateMojoTasks(task_runner_); |
| } |
| |
| void set_task_runner(scoped_refptr<base::TestMockTimeTaskRunner> runner) { |
| task_runner_ = std::move(runner); |
| } |
| |
| private: |
| CapturableFrameSink::Client* client_ = nullptr; |
| YUVColor color_ = {0xde, 0xad, 0xbf}; |
| SizeSet size_set_; |
| CompositorFrameMetadata metadata_; |
| scoped_refptr<base::TestMockTimeTaskRunner> task_runner_; |
| |
| std::vector<base::OnceClosure> results_; |
| }; |
| |
| class InstrumentedVideoCaptureOracle : public media::VideoCaptureOracle { |
| public: |
| explicit InstrumentedVideoCaptureOracle(bool enable_auto_throttling) |
| : media::VideoCaptureOracle(enable_auto_throttling), |
| return_false_on_complete_capture_(false) {} |
| |
| bool CompleteCapture(int frame_number, |
| bool capture_was_successful, |
| base::TimeTicks* frame_timestamp) override { |
| capture_was_successful &= !return_false_on_complete_capture_; |
| return media::VideoCaptureOracle::CompleteCapture( |
| frame_number, capture_was_successful, frame_timestamp); |
| } |
| |
| void set_return_false_on_complete_capture(bool should_return_false) { |
| return_false_on_complete_capture_ = should_return_false; |
| } |
| |
| gfx::Size capture_size() const override { |
| if (forced_capture_size_.has_value()) |
| return forced_capture_size_.value(); |
| return media::VideoCaptureOracle::capture_size(); |
| } |
| |
| void set_forced_capture_size(base::Optional<gfx::Size> size) { |
| forced_capture_size_ = size; |
| } |
| |
| private: |
| bool return_false_on_complete_capture_; |
| base::Optional<gfx::Size> forced_capture_size_; |
| }; |
| |
| // Matcher that returns true if the content region of a letterboxed VideoFrame |
| // is filled with the given color, and black everywhere else. |
| MATCHER_P2(IsLetterboxedFrame, color, content_rect, "") { |
| if (!arg) { |
| return false; |
| } |
| |
| const VideoFrame& frame = *arg; |
| const gfx::Rect kContentRect = content_rect; |
| const auto IsLetterboxedPlane = [&frame, kContentRect](int plane, |
| uint8_t color) { |
| gfx::Rect content_rect_copy = kContentRect; |
| if (plane != VideoFrame::kYPlane) { |
| content_rect_copy = gfx::Rect( |
| content_rect_copy.x() / 2, content_rect_copy.y() / 2, |
| content_rect_copy.width() / 2, content_rect_copy.height() / 2); |
| } |
| for (int row = 0; row < frame.rows(plane); ++row) { |
| const uint8_t* p = frame.visible_data(plane) + row * frame.stride(plane); |
| for (int col = 0; col < frame.row_bytes(plane); ++col) { |
| if (content_rect_copy.Contains(gfx::Point(col, row))) { |
| if (p[col] != color) { |
| return false; |
| } |
| } else { // Letterbox border around content. |
| if (plane == VideoFrame::kYPlane && p[col] != 0x00) { |
| return false; |
| } |
| } |
| } |
| } |
| return true; |
| }; |
| |
| return IsLetterboxedPlane(VideoFrame::kYPlane, color.y) && |
| IsLetterboxedPlane(VideoFrame::kUPlane, color.u) && |
| IsLetterboxedPlane(VideoFrame::kVPlane, color.v); |
| } |
| |
| } // namespace |
| |
| class FrameSinkVideoCapturerTest : public testing::Test { |
| public: |
| FrameSinkVideoCapturerTest() : size_set_(kSizeSets[0]) { |
| auto oracle = std::make_unique<InstrumentedVideoCaptureOracle>( |
| true /* enable_auto_throttling */); |
| oracle_ = oracle.get(); |
| capturer_ = std::make_unique<FrameSinkVideoCapturerImpl>( |
| &frame_sink_manager_, mojo::NullReceiver(), std::move(oracle), false); |
| } |
| |
| void SetUp() override { |
| // Override the capturer's TickClock with a virtual clock managed by a |
| // manually-driven task runner. |
| task_runner_ = new base::TestMockTimeTaskRunner( |
| base::Time::Now(), base::TimeTicks() + base::TimeDelta::FromSeconds(1), |
| base::TestMockTimeTaskRunner::Type::kStandalone); |
| start_time_ = task_runner_->NowTicks(); |
| capturer_->clock_ = task_runner_->GetMockTickClock(); |
| |
| // Ensure any posted tasks for CopyOutputResults will be handled when |
| // PropagateMojoTasks() is called |
| frame_sink_.set_task_runner(task_runner_); |
| |
| // Replace the retry timer with one that uses this test's fake clock and |
| // task runner. |
| capturer_->refresh_frame_retry_timer_.emplace( |
| task_runner_->GetMockTickClock()); |
| capturer_->refresh_frame_retry_timer_->SetTaskRunner(task_runner_); |
| |
| // Before setting the format, ensure the defaults are in-place. Then, for |
| // these tests, set a specific format and color space. |
| ASSERT_EQ(FrameSinkVideoCapturerImpl::kDefaultPixelFormat, |
| capturer_->pixel_format_); |
| ASSERT_EQ(FrameSinkVideoCapturerImpl::kDefaultColorSpace, |
| capturer_->color_space_); |
| capturer_->SetFormat(media::PIXEL_FORMAT_I420, |
| gfx::ColorSpace::CreateREC709()); |
| ASSERT_EQ(media::PIXEL_FORMAT_I420, capturer_->pixel_format_); |
| ASSERT_EQ(gfx::ColorSpace::CreateREC709(), capturer_->color_space_); |
| |
| // Set min capture period as small as possible so that the |
| // media::VideoCapturerOracle used by the capturer will want to capture |
| // every composited frame. The capturer will override the too-small value of |
| // zero with a value based on media::limits::kMaxFramesPerSecond. |
| capturer_->SetMinCapturePeriod(base::TimeDelta()); |
| ASSERT_LT(base::TimeDelta(), oracle_->min_capture_period()); |
| |
| capturer_->SetResolutionConstraints(size_set_.capture_size, |
| size_set_.capture_size, false); |
| } |
| |
| void TearDown() override { task_runner_->ClearPendingTasks(); } |
| |
| void StartCapture(MockConsumer* consumer) { |
| capturer_->Start(consumer->BindVideoConsumer()); |
| PropagateMojoTasks(); |
| } |
| |
| void StopCapture() { |
| capturer_->Stop(); |
| PropagateMojoTasks(); |
| } |
| |
| base::TimeTicks GetNextVsync() const { |
| const auto now = task_runner_->NowTicks(); |
| return now + kVsyncInterval - ((now - start_time_) % kVsyncInterval); |
| } |
| |
| void AdvanceClockToNextVsync() { |
| task_runner_->FastForwardBy(GetNextVsync() - task_runner_->NowTicks()); |
| } |
| |
| void SwitchToSizeSet(const SizeSet& size_set) { |
| size_set_ = size_set; |
| oracle_->set_forced_capture_size(size_set.capture_size); |
| frame_sink_.set_size_set(size_set); |
| capturer_->SetResolutionConstraints(size_set_.capture_size, |
| size_set_.capture_size, false); |
| } |
| |
| void ForceOracleSize(const SizeSet& size_set) { |
| size_set_ = size_set; |
| oracle_->set_forced_capture_size(size_set.capture_size); |
| frame_sink_.set_size_set(size_set); |
| // No call to capturer_, because this method simulates size change requested |
| // by the oracle, internal to the capturer. |
| } |
| |
| const SizeSet& size_set() { return size_set_; } |
| |
| void NotifyFrameDamaged( |
| gfx::Rect damage_rect, |
| float device_scale_factor = kDefaultDeviceScaleFactor, |
| float page_scale_factor = kDefaultPageScaleFactor, |
| gfx::Vector2dF root_scroll_offset = kDefaultRootScrollOffset) { |
| CompositorFrameMetadata metadata; |
| |
| metadata.device_scale_factor = device_scale_factor; |
| metadata.page_scale_factor = page_scale_factor; |
| metadata.root_scroll_offset = root_scroll_offset; |
| |
| frame_sink_.set_metadata(metadata); |
| |
| capturer_->OnFrameDamaged(frame_sink_.source_size(), damage_rect, |
| GetNextVsync(), metadata); |
| } |
| |
| void NotifyTargetWentAway() { |
| capturer_->OnTargetWillGoAway(); |
| PropagateMojoTasks(); |
| } |
| |
| bool IsRefreshRetryTimerRunning() { |
| return capturer_->refresh_frame_retry_timer_->IsRunning(); |
| } |
| |
| void AdvanceClockForRefreshTimer() { |
| task_runner_->FastForwardBy(capturer_->GetDelayBeforeNextRefreshAttempt()); |
| PropagateMojoTasks(); |
| } |
| |
| gfx::Rect ExpandRectToI420SubsampleBoundaries(const gfx::Rect& rect) { |
| return FrameSinkVideoCapturerImpl::ExpandRectToI420SubsampleBoundaries( |
| rect); |
| } |
| |
| protected: |
| SizeSet size_set_; |
| scoped_refptr<base::TestMockTimeTaskRunner> task_runner_; |
| base::TimeTicks start_time_; |
| MockFrameSinkManager frame_sink_manager_; |
| FakeCapturableFrameSink frame_sink_; |
| std::unique_ptr<FrameSinkVideoCapturerImpl> capturer_; |
| InstrumentedVideoCaptureOracle* oracle_; |
| }; |
| |
| // Tests that the capturer attaches to a frame sink immediately, in the case |
| // where the frame sink was already known by the manager. |
| TEST_F(FrameSinkVideoCapturerTest, ResolvesTargetImmediately) { |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(&frame_sink_)); |
| |
| EXPECT_EQ(FrameSinkId(), capturer_->requested_target()); |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| EXPECT_EQ(kFrameSinkId, capturer_->requested_target()); |
| EXPECT_EQ(capturer_.get(), frame_sink_.attached_client()); |
| } |
| |
| // Tests that the capturer attaches to a frame sink later, in the case where the |
| // frame sink becomes known to the manager at some later point. |
| TEST_F(FrameSinkVideoCapturerTest, ResolvesTargetLater) { |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(nullptr)); |
| |
| EXPECT_EQ(FrameSinkId(), capturer_->requested_target()); |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| EXPECT_EQ(kFrameSinkId, capturer_->requested_target()); |
| EXPECT_EQ(nullptr, frame_sink_.attached_client()); |
| |
| capturer_->SetResolvedTarget(&frame_sink_); |
| EXPECT_EQ(capturer_.get(), frame_sink_.attached_client()); |
| } |
| |
| // Tests that no initial frame is sent after Start() is called until after the |
| // target has been resolved. |
| TEST_F(FrameSinkVideoCapturerTest, PostponesCaptureWithoutATarget) { |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(&frame_sink_)); |
| |
| MockConsumer consumer; |
| EXPECT_CALL(consumer, OnFrameCapturedMock()).Times(0); |
| EXPECT_CALL(consumer, OnStopped()).Times(1); |
| |
| StartCapture(&consumer); |
| // No copy requests should have been issued/executed. |
| EXPECT_EQ(0, frame_sink_.num_copy_results()); |
| // The refresh timer is running, which represents the need for an initial |
| // frame to be sent. |
| EXPECT_TRUE(IsRefreshRetryTimerRunning()); |
| |
| // Simulate several refresh timer intervals elapsing and the timer firing. |
| // Nothing should happen because the capture target was never set. |
| for (int i = 0; i < 5; ++i) { |
| AdvanceClockForRefreshTimer(); |
| ASSERT_EQ(0, frame_sink_.num_copy_results()); |
| ASSERT_TRUE(IsRefreshRetryTimerRunning()); |
| } |
| |
| // Now, set the target. As it resolves, the capturer will immediately attempt |
| // a refresh capture, which will cancel the timer and trigger a copy request. |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| EXPECT_EQ(1, frame_sink_.num_copy_results()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| |
| StopCapture(); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| } |
| |
| // An end-to-end pipeline test where compositor updates trigger the capturer to |
| // make copy requests, and a stream of video frames is delivered to the |
| // consumer. |
| TEST_F(FrameSinkVideoCapturerTest, CapturesCompositedFrames) { |
| frame_sink_.SetCopyOutputColor(YUVColor{0x80, 0x80, 0x80}); |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(&frame_sink_)); |
| |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| |
| MockConsumer consumer; |
| const int num_refresh_frames = 1; |
| const int num_update_frames = |
| 3 * FrameSinkVideoCapturerImpl::kDesignLimitMaxFrames; |
| EXPECT_CALL(consumer, OnFrameCapturedMock()) |
| .Times(num_refresh_frames + num_update_frames); |
| EXPECT_CALL(consumer, OnStopped()).Times(1); |
| StartCapture(&consumer); |
| |
| // Since the target was already resolved at start, the capturer will have |
| // immediately executed a refresh capture and triggered a copy request. |
| ASSERT_EQ(num_refresh_frames, frame_sink_.num_copy_results()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| |
| // Simulate execution of the copy request and expect to see the initial |
| // refresh frame delivered to the consumer. |
| frame_sink_.SendCopyOutputResult(0); |
| ASSERT_EQ(num_refresh_frames, consumer.num_frames_received()); |
| EXPECT_THAT(consumer.TakeFrame(0), |
| IsLetterboxedFrame(YUVColor{0x80, 0x80, 0x80}, |
| size_set().expected_content_rect)); |
| consumer.SendDoneNotification(0); |
| |
| // Drive the capturer pipeline for a series of frame composites. |
| base::TimeDelta last_timestamp; |
| for (int i = num_refresh_frames; i < num_refresh_frames + num_update_frames; |
| ++i) { |
| SCOPED_TRACE(testing::Message() << "frame #" << i); |
| |
| // Move time forward to the next display vsync. |
| AdvanceClockToNextVsync(); |
| const base::TimeTicks expected_reference_time = |
| task_runner_->NowTicks() + kVsyncInterval; |
| |
| // Change the content of the frame sink and notify the capturer of the |
| // damage. |
| const YUVColor color = {i << 4, (i << 4) + 0x10, (i << 4) + 0x20}; |
| frame_sink_.SetCopyOutputColor(color); |
| task_runner_->FastForwardBy(kVsyncInterval / 4); |
| const base::TimeTicks expected_capture_begin_time = |
| task_runner_->NowTicks(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| |
| // The frame sink should have received a CopyOutputRequest. Simulate a short |
| // pause before the result is sent back to the capturer, and the capturer |
| // should then deliver the frame. |
| ASSERT_EQ(i + 1, frame_sink_.num_copy_results()); |
| task_runner_->FastForwardBy(kVsyncInterval / 4); |
| const base::TimeTicks expected_capture_end_time = task_runner_->NowTicks(); |
| frame_sink_.SendCopyOutputResult(i); |
| ASSERT_EQ(i + 1, consumer.num_frames_received()); |
| |
| // Verify the frame is the right size, has the right content, and has |
| // required metadata set. |
| const scoped_refptr<VideoFrame> frame = consumer.TakeFrame(i); |
| EXPECT_THAT(frame, |
| IsLetterboxedFrame(color, size_set().expected_content_rect)); |
| EXPECT_EQ(size_set().capture_size, frame->coded_size()); |
| EXPECT_EQ(gfx::Rect(size_set().capture_size), frame->visible_rect()); |
| EXPECT_LT(last_timestamp, frame->timestamp()); |
| last_timestamp = frame->timestamp(); |
| const VideoFrameMetadata* metadata = frame->metadata(); |
| EXPECT_EQ(expected_capture_begin_time, *metadata->capture_begin_time); |
| EXPECT_EQ(expected_capture_end_time, *metadata->capture_end_time); |
| EXPECT_EQ(gfx::ColorSpace::CreateREC709(), frame->ColorSpace()); |
| // frame_duration is an estimate computed by the VideoCaptureOracle, so it |
| // its exact value is not being checked here. |
| EXPECT_TRUE(metadata->frame_duration.has_value()); |
| EXPECT_NEAR(media::limits::kMaxFramesPerSecond, *metadata->frame_rate, |
| 0.001); |
| EXPECT_EQ(expected_reference_time, *metadata->reference_time); |
| |
| // Notify the capturer that the consumer is done with the frame. |
| consumer.SendDoneNotification(i); |
| |
| if (HasFailure()) { |
| break; |
| } |
| } |
| |
| StopCapture(); |
| } |
| |
| // Tests that frame capturing halts when too many frames are in-flight, whether |
| // that is because there are too many copy requests in-flight or because the |
| // consumer has not finished consuming frames fast enough. |
| TEST_F(FrameSinkVideoCapturerTest, HaltsWhenPipelineIsFull) { |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(&frame_sink_)); |
| |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| |
| NiceMock<MockConsumer> consumer; |
| StartCapture(&consumer); |
| // With the start, an immediate refresh occurred. |
| const int num_refresh_frames = 1; |
| ASSERT_EQ(num_refresh_frames, frame_sink_.num_copy_results()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| |
| // Saturate the pipeline with CopyOutputRequests that have not yet executed. |
| int num_frames = FrameSinkVideoCapturerImpl::kDesignLimitMaxFrames; |
| for (int i = num_refresh_frames; i < num_frames; ++i) { |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| // The oracle should not be rejecting captures caused by compositor updates. |
| ASSERT_FALSE(IsRefreshRetryTimerRunning()); |
| } |
| ASSERT_EQ(num_frames, frame_sink_.num_copy_results()); |
| |
| // Notifying the capturer of new compositor updates should cause no new copy |
| // requests to be issued at this point. However, the refresh timer should be |
| // scheduled to account for the capture of changed content that could not take |
| // place. |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| ASSERT_EQ(num_frames, frame_sink_.num_copy_results()); |
| EXPECT_TRUE(IsRefreshRetryTimerRunning()); |
| |
| // Complete the first copy request. When notifying the capturer of another |
| // compositor update, no new copy requests should be issued because the first |
| // frame is still in the middle of being delivered/consumed. |
| frame_sink_.SendCopyOutputResult(0); |
| ASSERT_EQ(1, consumer.num_frames_received()); |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| ASSERT_EQ(num_frames, frame_sink_.num_copy_results()); |
| EXPECT_TRUE(IsRefreshRetryTimerRunning()); |
| |
| // Notify the capturer that the first frame has been consumed. Then, with |
| // another compositor update, the capturer should issue another new copy |
| // request. The refresh timer should no longer be running because the next |
| // capture will satisfy the need to send updated content to the consumer. |
| EXPECT_TRUE(consumer.TakeFrame(0)); |
| consumer.SendDoneNotification(0); |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| ++num_frames; |
| ASSERT_EQ(num_frames, frame_sink_.num_copy_results()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| |
| // With yet another compositor update, no new copy requests should be issued |
| // because the pipeline became saturated again. Once again, the refresh timer |
| // should be started to account for the need to capture at some future point. |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| ASSERT_EQ(num_frames, frame_sink_.num_copy_results()); |
| EXPECT_TRUE(IsRefreshRetryTimerRunning()); |
| |
| // Complete all pending copy requests. Another compositor update should not |
| // cause any new copy requests to be issued because all frames are being |
| // delivered/consumed. |
| for (int i = 1; i < frame_sink_.num_copy_results(); ++i) { |
| SCOPED_TRACE(testing::Message() << "frame #" << i); |
| frame_sink_.SendCopyOutputResult(i); |
| } |
| ASSERT_EQ(frame_sink_.num_copy_results(), consumer.num_frames_received()); |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| ASSERT_EQ(num_frames, frame_sink_.num_copy_results()); |
| EXPECT_TRUE(IsRefreshRetryTimerRunning()); |
| |
| // Notify the capturer that all frames have been consumed. Finally, with |
| // another compositor update, capture should resume. |
| for (int i = 1; i < consumer.num_frames_received(); ++i) { |
| SCOPED_TRACE(testing::Message() << "frame #" << i); |
| EXPECT_TRUE(consumer.TakeFrame(i)); |
| consumer.SendDoneNotification(i); |
| } |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| ++num_frames; |
| ASSERT_EQ(num_frames, frame_sink_.num_copy_results()); |
| frame_sink_.SendCopyOutputResult(frame_sink_.num_copy_results() - 1); |
| ASSERT_EQ(frame_sink_.num_copy_results(), consumer.num_frames_received()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| |
| StopCapture(); |
| } |
| |
| // Tests that copy requests completed out-of-order are accounted for by the |
| // capturer, with results delivered to the consumer in-order. |
| TEST_F(FrameSinkVideoCapturerTest, DeliversFramesInOrder) { |
| std::vector<YUVColor> colors; |
| colors.push_back(YUVColor{0x00, 0x80, 0x80}); |
| frame_sink_.SetCopyOutputColor(colors.back()); |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(&frame_sink_)); |
| |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| |
| NiceMock<MockConsumer> consumer; |
| StartCapture(&consumer); |
| |
| // Simulate five compositor updates. Each composited frame has its content |
| // region set to a different color to check that the video frames are being |
| // delivered in-order. |
| constexpr int num_refresh_frames = 1; |
| constexpr int num_frames = 5; |
| ASSERT_EQ(num_refresh_frames, frame_sink_.num_copy_results()); |
| for (int i = num_refresh_frames; i < num_frames; ++i) { |
| colors.push_back(YUVColor{static_cast<uint8_t>(i << 4), |
| static_cast<uint8_t>((i << 4) + 0x10), |
| static_cast<uint8_t>((i << 4) + 0x20)}); |
| frame_sink_.SetCopyOutputColor(colors.back()); |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| } |
| ASSERT_EQ(num_frames, frame_sink_.num_copy_results()); |
| |
| // Complete the copy requests out-of-order. Check that frames are not |
| // delivered until they can all be delivered in-order, and that the content of |
| // each video frame is correct. |
| frame_sink_.SendCopyOutputResult(0); |
| ASSERT_EQ(1, consumer.num_frames_received()); |
| EXPECT_THAT(consumer.TakeFrame(0), |
| IsLetterboxedFrame(colors[0], size_set().expected_content_rect)); |
| frame_sink_.SendCopyOutputResult(2); |
| ASSERT_EQ(1, consumer.num_frames_received()); // Waiting for frame 1. |
| frame_sink_.SendCopyOutputResult(3); |
| ASSERT_EQ(1, consumer.num_frames_received()); // Still waiting for frame 1. |
| frame_sink_.SendCopyOutputResult(1); |
| ASSERT_EQ(4, consumer.num_frames_received()); // Sent frames 1, 2, and 3. |
| EXPECT_THAT(consumer.TakeFrame(1), |
| IsLetterboxedFrame(colors[1], size_set().expected_content_rect)); |
| EXPECT_THAT(consumer.TakeFrame(2), |
| IsLetterboxedFrame(colors[2], size_set().expected_content_rect)); |
| EXPECT_THAT(consumer.TakeFrame(3), |
| IsLetterboxedFrame(colors[3], size_set().expected_content_rect)); |
| frame_sink_.SendCopyOutputResult(4); |
| ASSERT_EQ(5, consumer.num_frames_received()); |
| EXPECT_THAT(consumer.TakeFrame(4), |
| IsLetterboxedFrame(colors[4], size_set().expected_content_rect)); |
| |
| StopCapture(); |
| } |
| |
| // Tests that in-flight copy requests are canceled when the capturer is |
| // stopped. When it is started again with a new consumer, only the results from |
| // newer copy requests should appear in video frames delivered to the consumer. |
| TEST_F(FrameSinkVideoCapturerTest, CancelsInFlightCapturesOnStop) { |
| const YUVColor color1 = {0xaa, 0xbb, 0xcc}; |
| frame_sink_.SetCopyOutputColor(color1); |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(&frame_sink_)); |
| |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| |
| // Start capturing to the first consumer. |
| MockConsumer consumer; |
| EXPECT_CALL(consumer, OnFrameCapturedMock()).Times(2); |
| EXPECT_CALL(consumer, OnStopped()).Times(1); |
| StartCapture(&consumer); |
| // With the start, an immediate refresh should have occurred. |
| const int num_refresh_frames = 1; |
| ASSERT_EQ(num_refresh_frames, frame_sink_.num_copy_results()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| |
| // Simulate two compositor updates following the initial refresh. |
| int num_copy_requests = 3; |
| for (int i = num_refresh_frames; i < num_copy_requests; ++i) { |
| SCOPED_TRACE(testing::Message() << "frame #" << i); |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| } |
| ASSERT_EQ(num_copy_requests, frame_sink_.num_copy_results()); |
| |
| // Complete the first two copy requests. |
| int num_completed_captures = 2; |
| for (int i = 0; i < num_completed_captures; ++i) { |
| SCOPED_TRACE(testing::Message() << "frame #" << i); |
| frame_sink_.SendCopyOutputResult(i); |
| ASSERT_EQ(i + 1, consumer.num_frames_received()); |
| EXPECT_THAT(consumer.TakeFrame(i), |
| IsLetterboxedFrame(color1, size_set().expected_content_rect)); |
| } |
| |
| // Stopping capture should cancel the remaning copy requests. |
| StopCapture(); |
| |
| // Change the content color and start capturing to the second consumer. |
| const YUVColor color2 = {0xdd, 0xee, 0xff}; |
| frame_sink_.SetCopyOutputColor(color2); |
| MockConsumer consumer2; |
| const int num_captures_for_second_consumer = 3; |
| EXPECT_CALL(consumer2, OnFrameCapturedMock()) |
| .Times(num_captures_for_second_consumer); |
| EXPECT_CALL(consumer2, OnStopped()).Times(1); |
| StartCapture(&consumer2); |
| // With the start, a refresh was attempted, but since the attempt occurred so |
| // soon after the last frame capture, the oracle should have rejected it. |
| // Thus, the refresh timer should be running. |
| EXPECT_TRUE(IsRefreshRetryTimerRunning()); |
| |
| // Complete the copy requests for the first consumer. Expect that they have no |
| // effect on the second consumer. |
| for (int i = num_completed_captures; i < num_copy_requests; ++i) { |
| frame_sink_.SendCopyOutputResult(i); |
| ASSERT_EQ(0, consumer2.num_frames_received()); |
| } |
| |
| // Reset the counter for |consumer2|. |
| num_completed_captures = 0; |
| |
| // From here, any new copy requests should be executed with video frames |
| // delivered to the consumer containing |color2|. |
| for (int i = 0; i < num_captures_for_second_consumer; ++i) { |
| AdvanceClockToNextVsync(); |
| if (i == 0) { |
| // Expect that advancing the clock caused the refresh timer to fire. |
| } else { |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| } |
| ++num_copy_requests; |
| ASSERT_EQ(num_copy_requests, frame_sink_.num_copy_results()); |
| ASSERT_FALSE(IsRefreshRetryTimerRunning()); |
| frame_sink_.SendCopyOutputResult(frame_sink_.num_copy_results() - 1); |
| ++num_completed_captures; |
| ASSERT_EQ(num_completed_captures, consumer2.num_frames_received()); |
| EXPECT_THAT(consumer2.TakeFrame(consumer2.num_frames_received() - 1), |
| IsLetterboxedFrame(color2, size_set().expected_content_rect)); |
| } |
| |
| StopCapture(); |
| } |
| |
| // Tests that refresh requests ultimately result in a frame being delivered to |
| // the consumer. |
| TEST_F(FrameSinkVideoCapturerTest, EventuallySendsARefreshFrame) { |
| frame_sink_.SetCopyOutputColor(YUVColor{0x80, 0x80, 0x80}); |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(&frame_sink_)); |
| |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| |
| MockConsumer consumer; |
| const int num_refresh_frames = 2; // Initial, plus later refresh. |
| const int num_update_frames = 3; |
| EXPECT_CALL(consumer, OnFrameCapturedMock()) |
| .Times(num_refresh_frames + num_update_frames); |
| EXPECT_CALL(consumer, OnStopped()).Times(1); |
| StartCapture(&consumer); |
| |
| // With the start, an immediate refresh occurred. Simulate a copy result and |
| // expect to see the refresh frame delivered to the consumer. |
| ASSERT_EQ(1, frame_sink_.num_copy_results()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| frame_sink_.SendCopyOutputResult(0); |
| ASSERT_EQ(1, consumer.num_frames_received()); |
| consumer.SendDoneNotification(0); |
| |
| // Drive the capturer pipeline for a series of frame composites. |
| int num_frames = 1 + num_update_frames; |
| for (int i = 1; i < num_frames; ++i) { |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size)); |
| ASSERT_EQ(i + 1, frame_sink_.num_copy_results()); |
| ASSERT_FALSE(IsRefreshRetryTimerRunning()); |
| frame_sink_.SendCopyOutputResult(i); |
| ASSERT_EQ(i + 1, consumer.num_frames_received()); |
| consumer.SendDoneNotification(i); |
| } |
| |
| // Request a refresh frame. Because the refresh request was made just after |
| // the last frame capture, the refresh retry timer should be started. |
| capturer_->RequestRefreshFrame(); |
| ASSERT_EQ(num_frames, frame_sink_.num_copy_results()); |
| EXPECT_TRUE(IsRefreshRetryTimerRunning()); |
| |
| // Simulate the elapse of time and the firing of the refresh retry timer. |
| // Since no compositor updates occurred in the meantime, this will execute a |
| // passive refresh, which resurrects the last buffer instead of spawning an |
| // additional copy request. |
| AdvanceClockForRefreshTimer(); |
| ASSERT_EQ(num_frames, frame_sink_.num_copy_results()); |
| ASSERT_EQ(num_frames + 1, consumer.num_frames_received()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| |
| StopCapture(); |
| } |
| |
| // Tests that full capture happens on capture resolution change due to oracle, |
| // but only once and resurrected frames are used after that. |
| TEST_F(FrameSinkVideoCapturerTest, |
| RessurectsFramesForChangingCaptureResolution) { |
| frame_sink_.SetCopyOutputColor(YUVColor{0x80, 0x80, 0x80}); |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(&frame_sink_)); |
| |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| |
| MockConsumer consumer; |
| constexpr int num_refresh_frames = 3; // Initial, plus two refreshes after |
| // downscale and upscale. |
| constexpr int num_update_frames = 3; |
| |
| int expected_frames_count = 0; |
| int expected_copy_results = 0; |
| |
| EXPECT_CALL(consumer, OnFrameCapturedMock()) |
| .Times(num_refresh_frames + num_update_frames); |
| EXPECT_CALL(consumer, OnStopped()).Times(1); |
| StartCapture(&consumer); |
| |
| // 1. The first frame captured automatically once the capture stats. |
| // It will be marked as the latest content in the buffer. |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| ++expected_copy_results; |
| ++expected_frames_count; |
| frame_sink_.SendCopyOutputResult(expected_copy_results - 1); |
| ASSERT_EQ(expected_copy_results, frame_sink_.num_copy_results()); |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| consumer.SendDoneNotification(expected_copy_results - 1); |
| |
| // 2. Another frame is captured, but there was no updates since the previous |
| // frame, therefore the marked frame should be resurrected, without making an |
| // actual request. |
| capturer_->RequestRefreshFrame(); |
| AdvanceClockForRefreshTimer(); |
| ++expected_frames_count; |
| EXPECT_EQ(expected_copy_results, frame_sink_.num_copy_results()); |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| // If we do not advance the clock, the oracle will reject capture due to |
| // frame rate limits. |
| AdvanceClockToNextVsync(); |
| |
| // 3. Simulate a change in the oracle imposed capture size (e.g. due to |
| // overuse). This frame is of a different size than the cached frame and will |
| // be captured with a CopyOutputRequest. |
| ForceOracleSize(kSizeSets[3]); |
| capturer_->RequestRefreshFrame(); |
| AdvanceClockForRefreshTimer(); |
| ++expected_copy_results; |
| ++expected_frames_count; |
| frame_sink_.SendCopyOutputResult(expected_copy_results - 1); |
| ASSERT_EQ(expected_copy_results, frame_sink_.num_copy_results()); |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| consumer.SendDoneNotification(expected_copy_results - 1); |
| |
| // 4. Another frame is captured, but there was no updates since the previous |
| // frame, therefore the marked frame should be resurrected, without making an |
| // actual request. |
| capturer_->RequestRefreshFrame(); |
| AdvanceClockForRefreshTimer(); |
| ++expected_frames_count; |
| EXPECT_EQ(expected_copy_results, frame_sink_.num_copy_results()); |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| // If we do not advance the clock, the oracle will reject capture due to |
| // frame rate limits. |
| AdvanceClockToNextVsync(); |
| |
| // 5. Simulate a change in the oracle imposed capture size (e.g. due to |
| // overuse). This frame is of a different size than the cached frame and will |
| // be captured with a CopyOutputRequest. |
| ForceOracleSize(kSizeSets[0]); |
| capturer_->RequestRefreshFrame(); |
| AdvanceClockForRefreshTimer(); |
| ++expected_copy_results; |
| ++expected_frames_count; |
| frame_sink_.SendCopyOutputResult(expected_copy_results - 1); |
| ASSERT_EQ(expected_copy_results, frame_sink_.num_copy_results()); |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| consumer.SendDoneNotification(expected_copy_results - 1); |
| |
| // 6. Another frame is captured, but there was no updates since the previous |
| // frame, therefore the marked frame should be resurrected, without making an |
| // actual request. |
| capturer_->RequestRefreshFrame(); |
| AdvanceClockForRefreshTimer(); |
| ++expected_frames_count; |
| EXPECT_EQ(expected_copy_results, frame_sink_.num_copy_results()); |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| EXPECT_FALSE(IsRefreshRetryTimerRunning()); |
| |
| StopCapture(); |
| } |
| |
| // Tests that CompositorFrameMetadata variables (|device_scale_factor|, |
| // |page_scale_factor| and |root_scroll_offset|) are sent along with each frame, |
| // and refreshes cause variables of the cached CompositorFrameMetadata |
| // (|last_frame_metadata|) to be used. |
| TEST_F(FrameSinkVideoCapturerTest, CompositorFrameMetadataReachesConsumer) { |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(&frame_sink_)); |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| |
| MockConsumer consumer; |
| // Initial refresh frame for starting capture, plus later refresh. |
| const int num_refresh_frames = 2; |
| const int num_update_frames = 1; |
| EXPECT_CALL(consumer, OnFrameCapturedMock()) |
| .Times(num_refresh_frames + num_update_frames); |
| EXPECT_CALL(consumer, OnStopped()).Times(1); |
| StartCapture(&consumer); |
| |
| // With the start, an immediate refresh occurred. Simulate a copy result. |
| // Expect to see the refresh frame delivered to the consumer, along with |
| // default metadata values. |
| int cur_frame_index = 0, expected_frames_count = 1; |
| frame_sink_.SendCopyOutputResult(cur_frame_index); |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| EXPECT_TRUE(CompareVarsInCompositorFrameMetadata( |
| *(consumer.TakeFrame(cur_frame_index)), kDefaultDeviceScaleFactor, |
| kDefaultPageScaleFactor, kDefaultRootScrollOffset)); |
| consumer.SendDoneNotification(cur_frame_index); |
| |
| // The metadata used to signal a frame damage and verify that it reaches the |
| // consumer. |
| const float kNewDeviceScaleFactor = 3.5; |
| const float kNewPageScaleFactor = 1.5; |
| const gfx::Vector2dF kNewRootScrollOffset = gfx::Vector2dF(100, 200); |
| |
| // Notify frame damage with new metadata, and expect that the refresh frame |
| // is delivered to the consumer with this new metadata. |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(gfx::Rect(size_set().source_size), kNewDeviceScaleFactor, |
| kNewPageScaleFactor, kNewRootScrollOffset); |
| frame_sink_.SendCopyOutputResult(++cur_frame_index); |
| ++expected_frames_count; |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| EXPECT_TRUE(CompareVarsInCompositorFrameMetadata( |
| *(consumer.TakeFrame(cur_frame_index)), kNewDeviceScaleFactor, |
| kNewPageScaleFactor, kNewRootScrollOffset)); |
| consumer.SendDoneNotification(cur_frame_index); |
| |
| // Request a refresh frame. Because the refresh request was made just after |
| // the last frame capture, the refresh retry timer should be started. |
| // Expect that the refresh frame is delivered to the consumer with the same |
| // metadata from the previous frame. |
| capturer_->RequestRefreshFrame(); |
| AdvanceClockForRefreshTimer(); |
| ++expected_frames_count; |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| EXPECT_TRUE(CompareVarsInCompositorFrameMetadata( |
| *(consumer.TakeFrame(++cur_frame_index)), kNewDeviceScaleFactor, |
| kNewPageScaleFactor, kNewRootScrollOffset)); |
| StopCapture(); |
| } |
| |
| // Tests that frame metadata CAPTURE_COUNTER and CAPTURE_UPDATE_RECT are sent to |
| // the consumer as part of each frame delivery. |
| TEST_F(FrameSinkVideoCapturerTest, DeliversUpdateRectAndCaptureCounter) { |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(&frame_sink_)); |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| |
| MockConsumer consumer; |
| StartCapture(&consumer); |
| |
| // With the start, an immediate refresh occurred. Simulate a copy result. |
| // Expect to see the refresh frame delivered to the consumer, along with |
| // default metadata values. |
| int cur_capture_index = 0, cur_frame_index = 0, expected_frames_count = 1, |
| previous_capture_counter_received = 0; |
| frame_sink_.SendCopyOutputResult(cur_capture_index); |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| { |
| auto received_frame = consumer.TakeFrame(cur_frame_index); |
| EXPECT_EQ(gfx::Rect(size_set().capture_size), |
| received_frame->metadata()->capture_update_rect); |
| previous_capture_counter_received = |
| *received_frame->metadata()->capture_counter; |
| } |
| consumer.SendDoneNotification(cur_frame_index); |
| |
| const gfx::Rect kSourceDamageRect = gfx::Rect(3, 7, 60, 45); |
| gfx::Rect expected_frame_update_rect = copy_output::ComputeResultRect( |
| kSourceDamageRect, |
| gfx::Vector2d(size_set().source_size.width(), |
| size_set().source_size.height()), |
| gfx::Vector2d(size_set().expected_content_rect.width(), |
| size_set().expected_content_rect.height())); |
| expected_frame_update_rect.Offset( |
| size_set().expected_content_rect.OffsetFromOrigin()); |
| EXPECT_FALSE(AlignsWithI420SubsamplingBoundaries(expected_frame_update_rect)); |
| expected_frame_update_rect = |
| ExpandRectToI420SubsampleBoundaries(expected_frame_update_rect); |
| EXPECT_TRUE(AlignsWithI420SubsamplingBoundaries(expected_frame_update_rect)); |
| |
| // Notify frame damage with custom damage rect, and expect that the refresh |
| // frame is delivered to the consumer with a corresponding |update_rect|. |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(kSourceDamageRect); |
| frame_sink_.SendCopyOutputResult(++cur_capture_index); |
| ++expected_frames_count; |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| { |
| auto received_frame = consumer.TakeFrame(++cur_frame_index); |
| int received_capture_counter = *received_frame->metadata()->capture_counter; |
| EXPECT_EQ(expected_frame_update_rect, |
| *received_frame->metadata()->capture_update_rect); |
| EXPECT_EQ(previous_capture_counter_received + 1, received_capture_counter); |
| previous_capture_counter_received = received_capture_counter; |
| } |
| consumer.SendDoneNotification(cur_frame_index); |
| |
| // Request a refresh frame. Because the refresh request was made just after |
| // the last frame capture, the refresh retry timer should be started. |
| // Since there was no damage to the source, expect that the |update_region| |
| // delivered to the consumer is empty. |
| capturer_->RequestRefreshFrame(); |
| AdvanceClockForRefreshTimer(); |
| ++expected_frames_count; |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| { |
| auto received_frame = consumer.TakeFrame(++cur_frame_index); |
| int received_capture_counter = *received_frame->metadata()->capture_counter; |
| EXPECT_TRUE(received_frame->metadata()->capture_update_rect->IsEmpty()); |
| EXPECT_EQ(previous_capture_counter_received + 1, received_capture_counter); |
| previous_capture_counter_received = received_capture_counter; |
| } |
| consumer.SendDoneNotification(cur_frame_index); |
| |
| // If we do not advance the clock, the oracle will reject capture due to |
| // frame rate limits. |
| AdvanceClockToNextVsync(); |
| // Simulate a change in the source size. |
| // This is expected to trigger a refresh capture. |
| SwitchToSizeSet(kSizeSets[1]); |
| frame_sink_.SendCopyOutputResult(++cur_capture_index); |
| ++expected_frames_count; |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| { |
| auto received_frame = consumer.TakeFrame(++cur_frame_index); |
| int received_capture_counter = *received_frame->metadata()->capture_counter; |
| EXPECT_EQ(gfx::Rect(size_set().capture_size), |
| *received_frame->metadata()->capture_update_rect); |
| EXPECT_EQ(previous_capture_counter_received + 1, received_capture_counter); |
| previous_capture_counter_received = received_capture_counter; |
| } |
| consumer.SendDoneNotification(cur_frame_index); |
| |
| // If we do not advance the clock, the oracle will reject capture due to |
| // frame rate. |
| AdvanceClockToNextVsync(); |
| // Simulate a change in the capture size. |
| // This is expected to trigger a refresh capture. |
| SwitchToSizeSet(kSizeSets[2]); |
| frame_sink_.SendCopyOutputResult(++cur_capture_index); |
| ++expected_frames_count; |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| { |
| auto received_frame = consumer.TakeFrame(++cur_frame_index); |
| int received_capture_counter = *received_frame->metadata()->capture_counter; |
| EXPECT_EQ(gfx::Rect(size_set().capture_size), |
| *received_frame->metadata()->capture_update_rect); |
| EXPECT_EQ(previous_capture_counter_received + 1, received_capture_counter); |
| previous_capture_counter_received = received_capture_counter; |
| } |
| |
| StopCapture(); |
| } |
| |
| // Tests that when captured frames being dropped before delivery, the |
| // CAPTURE_COUNTER metadata value sent to the consumer reflects the frame drops |
| // indicating that CAPTURE_UPDATE_RECT must be ignored. |
| TEST_F(FrameSinkVideoCapturerTest, CaptureCounterSkipsWhenFramesAreDropped) { |
| EXPECT_CALL(frame_sink_manager_, FindCapturableFrameSink(kFrameSinkId)) |
| .WillRepeatedly(Return(&frame_sink_)); |
| capturer_->ChangeTarget(kFrameSinkId, SubtreeCaptureId()); |
| |
| MockConsumer consumer; |
| StartCapture(&consumer); |
| |
| // With the start, an immediate refresh occurred. Simulate a copy result. |
| // Expect to see the refresh frame delivered to the consumer, along with |
| // default metadata values. |
| int cur_capture_frame_index = 0, cur_receive_frame_index = 0, |
| expected_frames_count = 1, previous_capture_counter_received = 0; |
| frame_sink_.SendCopyOutputResult(cur_capture_frame_index); |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| { |
| auto received_frame = consumer.TakeFrame(cur_receive_frame_index); |
| EXPECT_EQ(gfx::Rect(size_set().capture_size), |
| *received_frame->metadata()->capture_update_rect); |
| previous_capture_counter_received = |
| *received_frame->metadata()->capture_counter; |
| } |
| consumer.SendDoneNotification(cur_receive_frame_index); |
| |
| const gfx::Rect kArbitraryDamageRect1 = gfx::Rect(1, 2, 6, 6); |
| const gfx::Rect kArbitraryDamageRect2 = gfx::Rect(3, 7, 5, 5); |
| |
| // Notify frame damage with custom damage rect, but have oracle reject frame |
| // delivery. Expect that no frame is sent to the consumer. |
| oracle_->set_return_false_on_complete_capture(true); |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(kArbitraryDamageRect1); |
| frame_sink_.SendCopyOutputResult(++cur_capture_frame_index); |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| |
| // Another frame damage with a different rect is reported. This time the |
| // oracle accepts frame delivery. |
| oracle_->set_return_false_on_complete_capture(false); |
| AdvanceClockToNextVsync(); |
| NotifyFrameDamaged(kArbitraryDamageRect2); |
| frame_sink_.SendCopyOutputResult(++cur_capture_frame_index); |
| ++expected_frames_count; |
| EXPECT_EQ(expected_frames_count, consumer.num_frames_received()); |
| { |
| auto received_frame = consumer.TakeFrame(++cur_receive_frame_index); |
| EXPECT_NE(previous_capture_counter_received + 1, |
| *received_frame->metadata()->capture_counter); |
| } |
| StopCapture(); |
| } |
| |
| } // namespace viz |