tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>(&params)) != 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