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