/* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ #include "modules/desktop_capture/win/wgc_capturer_win.h" #include #include #include #include "modules/desktop_capture/desktop_capture_options.h" #include "modules/desktop_capture/desktop_capture_types.h" #include "modules/desktop_capture/desktop_capturer.h" #include "modules/desktop_capture/win/test_support/test_window.h" #include "modules/desktop_capture/win/window_capture_utils.h" #include "rtc_base/checks.h" #include "rtc_base/logging.h" #include "rtc_base/thread.h" #include "rtc_base/time_utils.h" #include "rtc_base/win/scoped_com_initializer.h" #include "rtc_base/win/windows_version.h" #include "system_wrappers/include/metrics.h" #include "test/gtest.h" namespace webrtc { namespace { const char kWindowThreadName[] = "wgc_capturer_test_window_thread"; const WCHAR kWindowTitle[] = L"WGC Capturer Test Window"; const char kCapturerImplHistogram[] = "WebRTC.DesktopCapture.Win.DesktopCapturerImpl"; const char kCapturerResultHistogram[] = "WebRTC.DesktopCapture.Win.WgcCapturerResult"; const int kSuccess = 0; const int kSessionStartFailure = 4; const char kCaptureSessionResultHistogram[] = "WebRTC.DesktopCapture.Win.WgcCaptureSessionStartResult"; const int kSourceClosed = 1; const char kCaptureTimeHistogram[] = "WebRTC.DesktopCapture.Win.WgcCapturerFrameTime"; const int kSmallWindowWidth = 200; const int kSmallWindowHeight = 100; const int kMediumWindowWidth = 300; const int kMediumWindowHeight = 200; const int kLargeWindowWidth = 400; const int kLargeWindowHeight = 500; // The size of the image we capture is slightly smaller than the actual size of // the window. const int kWindowWidthSubtrahend = 14; const int kWindowHeightSubtrahend = 7; // Custom message constants so we can direct our thread to close windows // and quit running. const UINT kNoOp = WM_APP; const UINT kDestroyWindow = WM_APP + 1; const UINT kQuitRunning = WM_APP + 2; enum CaptureType { kWindowCapture = 0, kScreenCapture = 1 }; } // namespace class WgcCapturerWinTest : public ::testing::TestWithParam, public DesktopCapturer::Callback { public: void SetUp() override { if (rtc::rtc_win::GetVersion() < rtc::rtc_win::Version::VERSION_WIN10_RS5) { RTC_LOG(LS_INFO) << "Skipping WgcCapturerWinTests on Windows versions < RS5."; GTEST_SKIP(); } com_initializer_ = std::make_unique(ScopedCOMInitializer::kMTA); EXPECT_TRUE(com_initializer_->Succeeded()); } void SetUpForWindowCapture(int window_width = kMediumWindowWidth, int window_height = kMediumWindowHeight) { capturer_ = WgcCapturerWin::CreateRawWindowCapturer( DesktopCaptureOptions::CreateDefault()); CreateWindowOnSeparateThread(window_width, window_height); StartWindowThreadMessageLoop(); source_id_ = GetTestWindowIdFromSourceList(); } void SetUpForScreenCapture() { capturer_ = WgcCapturerWin::CreateRawScreenCapturer( DesktopCaptureOptions::CreateDefault()); source_id_ = GetScreenIdFromSourceList(); } void TearDown() override { if (window_open_) { CloseTestWindow(); } } // The window must live on a separate thread so that we can run a message pump // without blocking the test thread. This is necessary if we are interested in // having GraphicsCaptureItem events (i.e. the Closed event) fire, and it more // closely resembles how capture works in the wild. void CreateWindowOnSeparateThread(int window_width, int window_height) { window_thread_ = rtc::Thread::Create(); window_thread_->SetName(kWindowThreadName, nullptr); window_thread_->Start(); window_thread_->Invoke(RTC_FROM_HERE, [this, window_width, window_height]() { window_thread_id_ = GetCurrentThreadId(); window_info_ = CreateTestWindow(kWindowTitle, window_height, window_width); window_open_ = true; while (!IsWindowResponding(window_info_.hwnd)) { RTC_LOG(LS_INFO) << "Waiting for test window to become responsive in " "WgcWindowCaptureTest."; } while (!IsWindowValidAndVisible(window_info_.hwnd)) { RTC_LOG(LS_INFO) << "Waiting for test window to be visible in " "WgcWindowCaptureTest."; } }); ASSERT_TRUE(window_thread_->RunningForTest()); ASSERT_FALSE(window_thread_->IsCurrent()); } void StartWindowThreadMessageLoop() { window_thread_->PostTask(RTC_FROM_HERE, [this]() { MSG msg; BOOL gm; while ((gm = ::GetMessage(&msg, NULL, 0, 0)) != 0 && gm != -1) { ::DispatchMessage(&msg); if (msg.message == kDestroyWindow) { DestroyTestWindow(window_info_); } if (msg.message == kQuitRunning) { PostQuitMessage(0); } } }); } void CloseTestWindow() { ::PostThreadMessage(window_thread_id_, kDestroyWindow, 0, 0); ::PostThreadMessage(window_thread_id_, kQuitRunning, 0, 0); window_thread_->Stop(); window_open_ = false; } DesktopCapturer::SourceId GetTestWindowIdFromSourceList() { // Frequently, the test window will not show up in GetSourceList because it // was created too recently. Since we are confident the window will be found // eventually we loop here until we find it. intptr_t src_id; do { DesktopCapturer::SourceList sources; EXPECT_TRUE(capturer_->GetSourceList(&sources)); auto it = std::find_if( sources.begin(), sources.end(), [&](const DesktopCapturer::Source& src) { return src.id == reinterpret_cast(window_info_.hwnd); }); src_id = it->id; } while (src_id != reinterpret_cast(window_info_.hwnd)); return src_id; } DesktopCapturer::SourceId GetScreenIdFromSourceList() { DesktopCapturer::SourceList sources; EXPECT_TRUE(capturer_->GetSourceList(&sources)); EXPECT_GT(sources.size(), 0ULL); return sources[0].id; } void DoCapture() { // Sometimes the first few frames are empty becaues the capture engine is // still starting up. We also may drop a few frames when the window is // resized or un-minimized. do { capturer_->CaptureFrame(); } while (result_ == DesktopCapturer::Result::ERROR_TEMPORARY); EXPECT_EQ(result_, DesktopCapturer::Result::SUCCESS); EXPECT_TRUE(frame_); EXPECT_GT(metrics::NumEvents(kCapturerResultHistogram, kSuccess), successful_captures_); ++successful_captures_; } void ValidateFrame(int expected_width, int expected_height) { EXPECT_EQ(frame_->size().width(), expected_width - kWindowWidthSubtrahend); EXPECT_EQ(frame_->size().height(), expected_height - kWindowHeightSubtrahend); // Verify the buffer contains as much data as it should, and that the right // colors are found. int data_length = frame_->stride() * frame_->size().height(); // The first and last pixel should have the same color because they will be // from the border of the window. // Pixels have 4 bytes of data so the whole pixel needs a uint32_t to fit. uint32_t first_pixel = static_cast(*frame_->data()); uint32_t last_pixel = static_cast( *(frame_->data() + data_length - DesktopFrame::kBytesPerPixel)); EXPECT_EQ(first_pixel, last_pixel); // Let's also check a pixel from the middle of the content area, which the // TestWindow will paint a consistent color for us to verify. uint8_t* middle_pixel = frame_->data() + (data_length / 2); int sub_pixel_offset = DesktopFrame::kBytesPerPixel / 4; EXPECT_EQ(*middle_pixel, kTestWindowBValue); middle_pixel += sub_pixel_offset; EXPECT_EQ(*middle_pixel, kTestWindowGValue); middle_pixel += sub_pixel_offset; EXPECT_EQ(*middle_pixel, kTestWindowRValue); middle_pixel += sub_pixel_offset; // The window is opaque so we expect 0xFF for the Alpha channel. EXPECT_EQ(*middle_pixel, 0xFF); } // DesktopCapturer::Callback interface // The capturer synchronously invokes this method before |CaptureFrame()| // returns. void OnCaptureResult(DesktopCapturer::Result result, std::unique_ptr frame) override { result_ = result; frame_ = std::move(frame); } protected: std::unique_ptr com_initializer_; DWORD window_thread_id_; std::unique_ptr window_thread_; WindowInfo window_info_; intptr_t source_id_; bool window_open_ = false; DesktopCapturer::Result result_; int successful_captures_ = 0; std::unique_ptr frame_; std::unique_ptr capturer_; }; TEST_P(WgcCapturerWinTest, SelectValidSource) { if (GetParam() == CaptureType::kWindowCapture) { SetUpForWindowCapture(); } else { SetUpForScreenCapture(); } EXPECT_TRUE(capturer_->SelectSource(source_id_)); } TEST_P(WgcCapturerWinTest, SelectInvalidSource) { if (GetParam() == CaptureType::kWindowCapture) { capturer_ = WgcCapturerWin::CreateRawWindowCapturer( DesktopCaptureOptions::CreateDefault()); source_id_ = kNullWindowId; } else { capturer_ = WgcCapturerWin::CreateRawScreenCapturer( DesktopCaptureOptions::CreateDefault()); source_id_ = kInvalidScreenId; } EXPECT_FALSE(capturer_->SelectSource(source_id_)); } TEST_P(WgcCapturerWinTest, Capture) { if (GetParam() == CaptureType::kWindowCapture) { SetUpForWindowCapture(); } else { SetUpForScreenCapture(); } EXPECT_TRUE(capturer_->SelectSource(source_id_)); capturer_->Start(this); EXPECT_GE(metrics::NumEvents(kCapturerImplHistogram, DesktopCapturerId::kWgcCapturerWin), 1); DoCapture(); EXPECT_GT(frame_->size().width(), 0); EXPECT_GT(frame_->size().height(), 0); } TEST_P(WgcCapturerWinTest, CaptureTime) { if (GetParam() == CaptureType::kWindowCapture) { SetUpForWindowCapture(); } else { SetUpForScreenCapture(); } EXPECT_TRUE(capturer_->SelectSource(source_id_)); capturer_->Start(this); int64_t start_time; do { start_time = rtc::TimeNanos(); capturer_->CaptureFrame(); } while (result_ == DesktopCapturer::Result::ERROR_TEMPORARY); int capture_time_ms = (rtc::TimeNanos() - start_time) / rtc::kNumNanosecsPerMillisec; EXPECT_TRUE(frame_); // The test may measure the time slightly differently than the capturer. So we // just check if it's within 5 ms. EXPECT_NEAR(frame_->capture_time_ms(), capture_time_ms, 5); EXPECT_GE( metrics::NumEvents(kCaptureTimeHistogram, frame_->capture_time_ms()), 1); } INSTANTIATE_TEST_SUITE_P(SourceAgnostic, WgcCapturerWinTest, ::testing::Values(CaptureType::kWindowCapture, CaptureType::kScreenCapture)); // Monitor specific tests. TEST_F(WgcCapturerWinTest, FocusOnMonitor) { SetUpForScreenCapture(); EXPECT_TRUE(capturer_->SelectSource(0)); // You can't set focus on a monitor. EXPECT_FALSE(capturer_->FocusOnSelectedSource()); } TEST_F(WgcCapturerWinTest, CaptureAllMonitors) { SetUpForScreenCapture(); EXPECT_TRUE(capturer_->SelectSource(kFullDesktopScreenId)); capturer_->Start(this); DoCapture(); EXPECT_GT(frame_->size().width(), 0); EXPECT_GT(frame_->size().height(), 0); } // Window specific tests. TEST_F(WgcCapturerWinTest, FocusOnWindow) { capturer_ = WgcCapturerWin::CreateRawWindowCapturer( DesktopCaptureOptions::CreateDefault()); window_info_ = CreateTestWindow(kWindowTitle); source_id_ = GetScreenIdFromSourceList(); EXPECT_TRUE(capturer_->SelectSource(source_id_)); EXPECT_TRUE(capturer_->FocusOnSelectedSource()); HWND hwnd = reinterpret_cast(source_id_); EXPECT_EQ(hwnd, ::GetActiveWindow()); EXPECT_EQ(hwnd, ::GetForegroundWindow()); EXPECT_EQ(hwnd, ::GetFocus()); DestroyTestWindow(window_info_); } TEST_F(WgcCapturerWinTest, SelectMinimizedWindow) { SetUpForWindowCapture(); MinimizeTestWindow(reinterpret_cast(source_id_)); EXPECT_FALSE(capturer_->SelectSource(source_id_)); UnminimizeTestWindow(reinterpret_cast(source_id_)); EXPECT_TRUE(capturer_->SelectSource(source_id_)); } TEST_F(WgcCapturerWinTest, SelectClosedWindow) { SetUpForWindowCapture(); EXPECT_TRUE(capturer_->SelectSource(source_id_)); CloseTestWindow(); EXPECT_FALSE(capturer_->SelectSource(source_id_)); } TEST_F(WgcCapturerWinTest, UnsupportedWindowStyle) { // Create a window with the WS_EX_TOOLWINDOW style, which WGC does not // support. window_info_ = CreateTestWindow(kWindowTitle, kMediumWindowWidth, kMediumWindowHeight, WS_EX_TOOLWINDOW); capturer_ = WgcCapturerWin::CreateRawWindowCapturer( DesktopCaptureOptions::CreateDefault()); DesktopCapturer::SourceList sources; EXPECT_TRUE(capturer_->GetSourceList(&sources)); auto it = std::find_if( sources.begin(), sources.end(), [&](const DesktopCapturer::Source& src) { return src.id == reinterpret_cast(window_info_.hwnd); }); // We should not find the window, since we filter for unsupported styles. EXPECT_EQ(it, sources.end()); DestroyTestWindow(window_info_); } TEST_F(WgcCapturerWinTest, IncreaseWindowSizeMidCapture) { SetUpForWindowCapture(kSmallWindowWidth, kSmallWindowHeight); EXPECT_TRUE(capturer_->SelectSource(source_id_)); capturer_->Start(this); DoCapture(); ValidateFrame(kSmallWindowWidth, kSmallWindowHeight); ResizeTestWindow(window_info_.hwnd, kSmallWindowWidth, kMediumWindowHeight); DoCapture(); // We don't expect to see the new size until the next capture, as the frame // pool hadn't had a chance to resize yet to fit the new, larger image. DoCapture(); ValidateFrame(kSmallWindowWidth, kMediumWindowHeight); ResizeTestWindow(window_info_.hwnd, kLargeWindowWidth, kMediumWindowHeight); DoCapture(); DoCapture(); ValidateFrame(kLargeWindowWidth, kMediumWindowHeight); } TEST_F(WgcCapturerWinTest, ReduceWindowSizeMidCapture) { SetUpForWindowCapture(kLargeWindowWidth, kLargeWindowHeight); EXPECT_TRUE(capturer_->SelectSource(source_id_)); capturer_->Start(this); DoCapture(); ValidateFrame(kLargeWindowWidth, kLargeWindowHeight); ResizeTestWindow(window_info_.hwnd, kLargeWindowWidth, kMediumWindowHeight); // We expect to see the new size immediately because the image data has shrunk // and will fit in the existing buffer. DoCapture(); ValidateFrame(kLargeWindowWidth, kMediumWindowHeight); ResizeTestWindow(window_info_.hwnd, kSmallWindowWidth, kMediumWindowHeight); DoCapture(); ValidateFrame(kSmallWindowWidth, kMediumWindowHeight); } TEST_F(WgcCapturerWinTest, MinimizeWindowMidCapture) { SetUpForWindowCapture(); EXPECT_TRUE(capturer_->SelectSource(source_id_)); capturer_->Start(this); // Minmize the window and capture should continue but return temporary errors. MinimizeTestWindow(window_info_.hwnd); for (int i = 0; i < 10; ++i) { capturer_->CaptureFrame(); EXPECT_EQ(result_, DesktopCapturer::Result::ERROR_TEMPORARY); } // Reopen the window and the capture should continue normally. UnminimizeTestWindow(window_info_.hwnd); DoCapture(); // We can't verify the window size here because the test window does not // repaint itself after it is unminimized, but capturing successfully is still // a good test. } TEST_F(WgcCapturerWinTest, CloseWindowMidCapture) { SetUpForWindowCapture(); EXPECT_TRUE(capturer_->SelectSource(source_id_)); capturer_->Start(this); DoCapture(); ValidateFrame(kMediumWindowWidth, kMediumWindowHeight); CloseTestWindow(); // We need to call GetMessage to trigger the Closed event and the capturer's // event handler for it. If we are too early and the Closed event hasn't // arrived yet we should keep trying until the capturer receives it and stops. auto* wgc_capturer = static_cast(capturer_.get()); while (wgc_capturer->IsSourceBeingCaptured(source_id_)) { // Since the capturer handles the Closed message, there will be no message // for us and GetMessage will hang, unless we send ourselves a message // first. ::PostThreadMessage(GetCurrentThreadId(), kNoOp, 0, 0); MSG msg; ::GetMessage(&msg, NULL, 0, 0); ::DispatchMessage(&msg); } // Occasionally, one last frame will have made it into the frame pool before // the window closed. The first call will consume it, and in that case we need // to make one more call to CaptureFrame. capturer_->CaptureFrame(); if (result_ == DesktopCapturer::Result::SUCCESS) capturer_->CaptureFrame(); EXPECT_GE(metrics::NumEvents(kCapturerResultHistogram, kSessionStartFailure), 1); EXPECT_GE(metrics::NumEvents(kCaptureSessionResultHistogram, kSourceClosed), 1); EXPECT_EQ(result_, DesktopCapturer::Result::ERROR_PERMANENT); } } // namespace webrtc