xref: /aosp_15_r20/external/webrtc/modules/desktop_capture/win/window_capture_utils.cc (revision d9f758449e529ab9291ac668be2861e7a55c2422)
1 /*
2  *  Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3  *
4  *  Use of this source code is governed by a BSD-style license
5  *  that can be found in the LICENSE file in the root of the source
6  *  tree. An additional intellectual property rights grant can be found
7  *  in the file PATENTS.  All contributing project authors may
8  *  be found in the AUTHORS file in the root of the source tree.
9  */
10 
11 #include "modules/desktop_capture/win/window_capture_utils.h"
12 
13 // Just for the DWMWINDOWATTRIBUTE enums (DWMWA_CLOAKED).
14 #include <dwmapi.h>
15 
16 #include <algorithm>
17 
18 #include "modules/desktop_capture/win/scoped_gdi_object.h"
19 #include "rtc_base/arraysize.h"
20 #include "rtc_base/checks.h"
21 #include "rtc_base/logging.h"
22 #include "rtc_base/string_utils.h"
23 #include "rtc_base/win/windows_version.h"
24 
25 namespace webrtc {
26 
27 namespace {
28 
29 struct GetWindowListParams {
GetWindowListParamswebrtc::__anon2ad4e86d0111::GetWindowListParams30   GetWindowListParams(int flags,
31                       LONG ex_style_filters,
32                       DesktopCapturer::SourceList* result)
33       : ignore_untitled(flags & GetWindowListFlags::kIgnoreUntitled),
34         ignore_unresponsive(flags & GetWindowListFlags::kIgnoreUnresponsive),
35         ignore_current_process_windows(
36             flags & GetWindowListFlags::kIgnoreCurrentProcessWindows),
37         ex_style_filters(ex_style_filters),
38         result(result) {}
39   const bool ignore_untitled;
40   const bool ignore_unresponsive;
41   const bool ignore_current_process_windows;
42   const LONG ex_style_filters;
43   DesktopCapturer::SourceList* const result;
44 };
45 
IsWindowOwnedByCurrentProcess(HWND hwnd)46 bool IsWindowOwnedByCurrentProcess(HWND hwnd) {
47   DWORD process_id;
48   GetWindowThreadProcessId(hwnd, &process_id);
49   return process_id == GetCurrentProcessId();
50 }
51 
GetWindowListHandler(HWND hwnd,LPARAM param)52 BOOL CALLBACK GetWindowListHandler(HWND hwnd, LPARAM param) {
53   GetWindowListParams* params = reinterpret_cast<GetWindowListParams*>(param);
54   DesktopCapturer::SourceList* list = params->result;
55 
56   // Skip invisible and minimized windows
57   if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) {
58     return TRUE;
59   }
60 
61   // Skip windows which are not presented in the taskbar,
62   // namely owned window if they don't have the app window style set
63   HWND owner = GetWindow(hwnd, GW_OWNER);
64   LONG exstyle = GetWindowLong(hwnd, GWL_EXSTYLE);
65   if (owner && !(exstyle & WS_EX_APPWINDOW)) {
66     return TRUE;
67   }
68 
69   // Filter out windows that match the extended styles the caller has specified,
70   // e.g. WS_EX_TOOLWINDOW for capturers that don't support overlay windows.
71   if (exstyle & params->ex_style_filters) {
72     return TRUE;
73   }
74 
75   if (params->ignore_unresponsive && !IsWindowResponding(hwnd)) {
76     return TRUE;
77   }
78 
79   DesktopCapturer::Source window;
80   window.id = reinterpret_cast<WindowId>(hwnd);
81 
82   // GetWindowText* are potentially blocking operations if `hwnd` is
83   // owned by the current process. The APIs will send messages to the window's
84   // message loop, and if the message loop is waiting on this operation we will
85   // enter a deadlock.
86   // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtexta#remarks
87   //
88   // To help consumers avoid this, there is a DesktopCaptureOption to ignore
89   // windows owned by the current process. Consumers should either ensure that
90   // the thread running their message loop never waits on this operation, or use
91   // the option to exclude these windows from the source list.
92   bool owned_by_current_process = IsWindowOwnedByCurrentProcess(hwnd);
93   if (owned_by_current_process && params->ignore_current_process_windows) {
94     return TRUE;
95   }
96 
97   // Even if consumers request to enumerate windows owned by the current
98   // process, we should not call GetWindowText* on unresponsive windows owned by
99   // the current process because we will hang. Unfortunately, we could still
100   // hang if the window becomes unresponsive after this check, hence the option
101   // to avoid these completely.
102   if (!owned_by_current_process || IsWindowResponding(hwnd)) {
103     const size_t kTitleLength = 500;
104     WCHAR window_title[kTitleLength] = L"";
105     if (GetWindowTextLength(hwnd) != 0 &&
106         GetWindowTextW(hwnd, window_title, kTitleLength) > 0) {
107       window.title = rtc::ToUtf8(window_title);
108     }
109   }
110 
111   // Skip windows when we failed to convert the title or it is empty.
112   if (params->ignore_untitled && window.title.empty())
113     return TRUE;
114 
115   // Capture the window class name, to allow specific window classes to be
116   // skipped.
117   //
118   // https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassa
119   // says lpszClassName field in WNDCLASS is limited by 256 symbols, so we don't
120   // need to have a buffer bigger than that.
121   const size_t kMaxClassNameLength = 256;
122   WCHAR class_name[kMaxClassNameLength] = L"";
123   const int class_name_length =
124       GetClassNameW(hwnd, class_name, kMaxClassNameLength);
125   if (class_name_length < 1)
126     return TRUE;
127 
128   // Skip Program Manager window.
129   if (wcscmp(class_name, L"Progman") == 0)
130     return TRUE;
131 
132   // Skip Start button window on Windows Vista, Windows 7.
133   // On Windows 8, Windows 8.1, Windows 10 Start button is not a top level
134   // window, so it will not be examined here.
135   if (wcscmp(class_name, L"Button") == 0)
136     return TRUE;
137 
138   list->push_back(window);
139 
140   return TRUE;
141 }
142 
143 }  // namespace
144 
145 // Prefix used to match the window class for Chrome windows.
146 const wchar_t kChromeWindowClassPrefix[] = L"Chrome_WidgetWin_";
147 
148 // The hiddgen taskbar will leave a 2 pixel margin on the screen.
149 const int kHiddenTaskbarMarginOnScreen = 2;
150 
GetWindowRect(HWND window,DesktopRect * result)151 bool GetWindowRect(HWND window, DesktopRect* result) {
152   RECT rect;
153   if (!::GetWindowRect(window, &rect)) {
154     return false;
155   }
156   *result = DesktopRect::MakeLTRB(rect.left, rect.top, rect.right, rect.bottom);
157   return true;
158 }
159 
GetCroppedWindowRect(HWND window,bool avoid_cropping_border,DesktopRect * cropped_rect,DesktopRect * original_rect)160 bool GetCroppedWindowRect(HWND window,
161                           bool avoid_cropping_border,
162                           DesktopRect* cropped_rect,
163                           DesktopRect* original_rect) {
164   DesktopRect window_rect;
165   if (!GetWindowRect(window, &window_rect)) {
166     return false;
167   }
168 
169   if (original_rect) {
170     *original_rect = window_rect;
171   }
172   *cropped_rect = window_rect;
173 
174   bool is_maximized = false;
175   if (!IsWindowMaximized(window, &is_maximized)) {
176     return false;
177   }
178 
179   // As of Windows8, transparent resize borders are added by the OS at
180   // left/bottom/right sides of a resizeable window. If the cropped window
181   // doesn't remove these borders, the background will be exposed a bit.
182   if (rtc::rtc_win::GetVersion() >= rtc::rtc_win::Version::VERSION_WIN8 ||
183       is_maximized) {
184     // Only apply this cropping to windows with a resize border (otherwise,
185     // it'd clip the edges of captured pop-up windows without this border).
186     LONG style = GetWindowLong(window, GWL_STYLE);
187     if (style & WS_THICKFRAME || style & DS_MODALFRAME) {
188       int width = GetSystemMetrics(SM_CXSIZEFRAME);
189       int bottom_height = GetSystemMetrics(SM_CYSIZEFRAME);
190       const int visible_border_height = GetSystemMetrics(SM_CYBORDER);
191       int top_height = visible_border_height;
192 
193       // If requested, avoid cropping the visible window border. This is used
194       // for pop-up windows to include their border, but not for the outermost
195       // window (where a partially-transparent border may expose the
196       // background a bit).
197       if (avoid_cropping_border) {
198         width = std::max(0, width - GetSystemMetrics(SM_CXBORDER));
199         bottom_height = std::max(0, bottom_height - visible_border_height);
200         top_height = 0;
201       }
202       cropped_rect->Extend(-width, -top_height, -width, -bottom_height);
203     }
204   }
205 
206   return true;
207 }
208 
GetWindowContentRect(HWND window,DesktopRect * result)209 bool GetWindowContentRect(HWND window, DesktopRect* result) {
210   if (!GetWindowRect(window, result)) {
211     return false;
212   }
213 
214   RECT rect;
215   if (!::GetClientRect(window, &rect)) {
216     return false;
217   }
218 
219   const int width = rect.right - rect.left;
220   // The GetClientRect() is not expected to return a larger area than
221   // GetWindowRect().
222   if (width > 0 && width < result->width()) {
223     // - GetClientRect() always set the left / top of RECT to 0. So we need to
224     //   estimate the border width from GetClientRect() and GetWindowRect().
225     // - Border width of a window varies according to the window type.
226     // - GetClientRect() excludes the title bar, which should be considered as
227     //   part of the content and included in the captured frame. So we always
228     //   estimate the border width according to the window width.
229     // - We assume a window has same border width in each side.
230     // So we shrink half of the width difference from all four sides.
231     const int shrink = ((width - result->width()) / 2);
232     // When `shrink` is negative, DesktopRect::Extend() shrinks itself.
233     result->Extend(shrink, 0, shrink, 0);
234     // Usually this should not happen, just in case we have received a strange
235     // window, which has only left and right borders.
236     if (result->height() > shrink * 2) {
237       result->Extend(0, shrink, 0, shrink);
238     }
239     RTC_DCHECK(!result->is_empty());
240   }
241 
242   return true;
243 }
244 
GetWindowRegionTypeWithBoundary(HWND window,DesktopRect * result)245 int GetWindowRegionTypeWithBoundary(HWND window, DesktopRect* result) {
246   win::ScopedGDIObject<HRGN, win::DeleteObjectTraits<HRGN>> scoped_hrgn(
247       CreateRectRgn(0, 0, 0, 0));
248   const int region_type = GetWindowRgn(window, scoped_hrgn.Get());
249 
250   if (region_type == SIMPLEREGION) {
251     RECT rect;
252     GetRgnBox(scoped_hrgn.Get(), &rect);
253     *result =
254         DesktopRect::MakeLTRB(rect.left, rect.top, rect.right, rect.bottom);
255   }
256   return region_type;
257 }
258 
GetDcSize(HDC hdc,DesktopSize * size)259 bool GetDcSize(HDC hdc, DesktopSize* size) {
260   win::ScopedGDIObject<HGDIOBJ, win::DeleteObjectTraits<HGDIOBJ>> scoped_hgdi(
261       GetCurrentObject(hdc, OBJ_BITMAP));
262   BITMAP bitmap;
263   memset(&bitmap, 0, sizeof(BITMAP));
264   if (GetObject(scoped_hgdi.Get(), sizeof(BITMAP), &bitmap) == 0) {
265     return false;
266   }
267   size->set(bitmap.bmWidth, bitmap.bmHeight);
268   return true;
269 }
270 
IsWindowMaximized(HWND window,bool * result)271 bool IsWindowMaximized(HWND window, bool* result) {
272   WINDOWPLACEMENT placement;
273   memset(&placement, 0, sizeof(WINDOWPLACEMENT));
274   placement.length = sizeof(WINDOWPLACEMENT);
275   if (!::GetWindowPlacement(window, &placement)) {
276     return false;
277   }
278 
279   *result = (placement.showCmd == SW_SHOWMAXIMIZED);
280   return true;
281 }
282 
IsWindowValidAndVisible(HWND window)283 bool IsWindowValidAndVisible(HWND window) {
284   return IsWindow(window) && IsWindowVisible(window) && !IsIconic(window);
285 }
286 
IsWindowResponding(HWND window)287 bool IsWindowResponding(HWND window) {
288   // 50ms is chosen in case the system is under heavy load, but it's also not
289   // too long to delay window enumeration considerably.
290   const UINT uTimeoutMs = 50;
291   return SendMessageTimeout(window, WM_NULL, 0, 0, SMTO_ABORTIFHUNG, uTimeoutMs,
292                             nullptr);
293 }
294 
GetWindowList(int flags,DesktopCapturer::SourceList * windows,LONG ex_style_filters)295 bool GetWindowList(int flags,
296                    DesktopCapturer::SourceList* windows,
297                    LONG ex_style_filters) {
298   GetWindowListParams params(flags, ex_style_filters, windows);
299   return ::EnumWindows(&GetWindowListHandler,
300                        reinterpret_cast<LPARAM>(&params)) != 0;
301 }
302 
303 // WindowCaptureHelperWin implementation.
WindowCaptureHelperWin()304 WindowCaptureHelperWin::WindowCaptureHelperWin() {
305   // Try to load dwmapi.dll dynamically since it is not available on XP.
306   dwmapi_library_ = LoadLibraryW(L"dwmapi.dll");
307   if (dwmapi_library_) {
308     func_ = reinterpret_cast<DwmIsCompositionEnabledFunc>(
309         GetProcAddress(dwmapi_library_, "DwmIsCompositionEnabled"));
310     dwm_get_window_attribute_func_ =
311         reinterpret_cast<DwmGetWindowAttributeFunc>(
312             GetProcAddress(dwmapi_library_, "DwmGetWindowAttribute"));
313   }
314 
315   if (rtc::rtc_win::GetVersion() >= rtc::rtc_win::Version::VERSION_WIN10) {
316     if (FAILED(::CoCreateInstance(__uuidof(VirtualDesktopManager), nullptr,
317                                   CLSCTX_ALL,
318                                   IID_PPV_ARGS(&virtual_desktop_manager_)))) {
319       RTC_LOG(LS_WARNING) << "Fail to create instance of VirtualDesktopManager";
320     }
321   }
322 }
323 
~WindowCaptureHelperWin()324 WindowCaptureHelperWin::~WindowCaptureHelperWin() {
325   if (dwmapi_library_) {
326     FreeLibrary(dwmapi_library_);
327   }
328 }
329 
IsAeroEnabled()330 bool WindowCaptureHelperWin::IsAeroEnabled() {
331   BOOL result = FALSE;
332   if (func_) {
333     func_(&result);
334   }
335   return result != FALSE;
336 }
337 
338 // This is just a best guess of a notification window. Chrome uses the Windows
339 // native framework for showing notifications. So far what we know about such a
340 // window includes: no title, class name with prefix "Chrome_WidgetWin_" and
341 // with certain extended styles.
IsWindowChromeNotification(HWND hwnd)342 bool WindowCaptureHelperWin::IsWindowChromeNotification(HWND hwnd) {
343   const size_t kTitleLength = 32;
344   WCHAR window_title[kTitleLength];
345   GetWindowTextW(hwnd, window_title, kTitleLength);
346   if (wcsnlen_s(window_title, kTitleLength) != 0) {
347     return false;
348   }
349 
350   const size_t kClassLength = 256;
351   WCHAR class_name[kClassLength];
352   const int class_name_length = GetClassNameW(hwnd, class_name, kClassLength);
353   if (class_name_length < 1 ||
354       wcsncmp(class_name, kChromeWindowClassPrefix,
355               wcsnlen_s(kChromeWindowClassPrefix, kClassLength)) != 0) {
356     return false;
357   }
358 
359   const LONG exstyle = GetWindowLong(hwnd, GWL_EXSTYLE);
360   if ((exstyle & WS_EX_NOACTIVATE) && (exstyle & WS_EX_TOOLWINDOW) &&
361       (exstyle & WS_EX_TOPMOST)) {
362     return true;
363   }
364 
365   return false;
366 }
367 
368 // `content_rect` is preferred because,
369 // 1. WindowCapturerWinGdi is using GDI capturer, which cannot capture DX
370 // output.
371 //    So ScreenCapturer should be used as much as possible to avoid
372 //    uncapturable cases. Note: lots of new applications are using DX output
373 //    (hardware acceleration) to improve the performance which cannot be
374 //    captured by WindowCapturerWinGdi. See bug http://crbug.com/741770.
375 // 2. WindowCapturerWinGdi is still useful because we do not want to expose the
376 //    content on other windows if the target window is covered by them.
377 // 3. Shadow and borders should not be considered as "content" on other
378 //    windows because they do not expose any useful information.
379 //
380 // So we can bear the false-negative cases (target window is covered by the
381 // borders or shadow of other windows, but we have not detected it) in favor
382 // of using ScreenCapturer, rather than let the false-positive cases (target
383 // windows is only covered by borders or shadow of other windows, but we treat
384 // it as overlapping) impact the user experience.
AreWindowsOverlapping(HWND hwnd,HWND selected_hwnd,const DesktopRect & selected_window_rect)385 bool WindowCaptureHelperWin::AreWindowsOverlapping(
386     HWND hwnd,
387     HWND selected_hwnd,
388     const DesktopRect& selected_window_rect) {
389   DesktopRect content_rect;
390   if (!GetWindowContentRect(hwnd, &content_rect)) {
391     // Bail out if failed to get the window area.
392     return true;
393   }
394   content_rect.IntersectWith(selected_window_rect);
395 
396   if (content_rect.is_empty()) {
397     return false;
398   }
399 
400   // When the taskbar is automatically hidden, it will leave a 2 pixel margin on
401   // the screen which will overlap the maximized selected window that will use
402   // up the full screen area. Since there is no solid way to identify a hidden
403   // taskbar window, we have to make an exemption here if the overlapping is
404   // 2 x screen_width/height to a maximized window.
405   bool is_maximized = false;
406   IsWindowMaximized(selected_hwnd, &is_maximized);
407   bool overlaps_hidden_horizontal_taskbar =
408       selected_window_rect.width() == content_rect.width() &&
409       content_rect.height() == kHiddenTaskbarMarginOnScreen;
410   bool overlaps_hidden_vertical_taskbar =
411       selected_window_rect.height() == content_rect.height() &&
412       content_rect.width() == kHiddenTaskbarMarginOnScreen;
413   if (is_maximized && (overlaps_hidden_horizontal_taskbar ||
414                        overlaps_hidden_vertical_taskbar)) {
415     return false;
416   }
417 
418   return true;
419 }
420 
IsWindowOnCurrentDesktop(HWND hwnd)421 bool WindowCaptureHelperWin::IsWindowOnCurrentDesktop(HWND hwnd) {
422   // Make sure the window is on the current virtual desktop.
423   if (virtual_desktop_manager_) {
424     BOOL on_current_desktop;
425     if (SUCCEEDED(virtual_desktop_manager_->IsWindowOnCurrentVirtualDesktop(
426             hwnd, &on_current_desktop)) &&
427         !on_current_desktop) {
428       return false;
429     }
430   }
431   return true;
432 }
433 
IsWindowVisibleOnCurrentDesktop(HWND hwnd)434 bool WindowCaptureHelperWin::IsWindowVisibleOnCurrentDesktop(HWND hwnd) {
435   return IsWindowValidAndVisible(hwnd) && IsWindowOnCurrentDesktop(hwnd) &&
436          !IsWindowCloaked(hwnd);
437 }
438 
439 // A cloaked window is composited but not visible to the user.
440 // Example: Cortana or the Action Center when collapsed.
IsWindowCloaked(HWND hwnd)441 bool WindowCaptureHelperWin::IsWindowCloaked(HWND hwnd) {
442   if (!dwm_get_window_attribute_func_) {
443     // Does not apply.
444     return false;
445   }
446 
447   int res = 0;
448   if (dwm_get_window_attribute_func_(hwnd, DWMWA_CLOAKED, &res, sizeof(res)) !=
449       S_OK) {
450     // Cannot tell so assume not cloaked for backward compatibility.
451     return false;
452   }
453 
454   return res != 0;
455 }
456 
EnumerateCapturableWindows(DesktopCapturer::SourceList * results,bool enumerate_current_process_windows,LONG ex_style_filters)457 bool WindowCaptureHelperWin::EnumerateCapturableWindows(
458     DesktopCapturer::SourceList* results,
459     bool enumerate_current_process_windows,
460     LONG ex_style_filters) {
461   int flags = (GetWindowListFlags::kIgnoreUntitled |
462                GetWindowListFlags::kIgnoreUnresponsive);
463   if (!enumerate_current_process_windows) {
464     flags |= GetWindowListFlags::kIgnoreCurrentProcessWindows;
465   }
466 
467   if (!webrtc::GetWindowList(flags, results, ex_style_filters)) {
468     return false;
469   }
470 
471   for (auto it = results->begin(); it != results->end();) {
472     if (!IsWindowVisibleOnCurrentDesktop(reinterpret_cast<HWND>(it->id))) {
473       it = results->erase(it);
474     } else {
475       ++it;
476     }
477   }
478 
479   return true;
480 }
481 
482 }  // namespace webrtc
483