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