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