aboutsummaryrefslogtreecommitdiff
path: root/modules/desktop_capture/win/window_capture_utils.cc
blob: aaaef0a80d4c51d54d8cc17873d5ca7606c159b3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
/*
 *  Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

#include "modules/desktop_capture/win/window_capture_utils.h"

// Just for the DWMWINDOWATTRIBUTE enums (DWMWA_CLOAKED).
#include <dwmapi.h>

#include <algorithm>

#include "modules/desktop_capture/win/scoped_gdi_object.h"
#include "rtc_base/arraysize.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
#include "rtc_base/string_utils.h"
#include "rtc_base/win32.h"

namespace webrtc {

namespace {

struct GetWindowListParams {
  GetWindowListParams(int flags,
                      LONG ex_style_filters,
                      DesktopCapturer::SourceList* result)
      : ignore_untitled(flags & GetWindowListFlags::kIgnoreUntitled),
        ignore_unresponsive(flags & GetWindowListFlags::kIgnoreUnresponsive),
        ignore_current_process_windows(
            flags & GetWindowListFlags::kIgnoreCurrentProcessWindows),
        ex_style_filters(ex_style_filters),
        result(result) {}
  const bool ignore_untitled;
  const bool ignore_unresponsive;
  const bool ignore_current_process_windows;
  const LONG ex_style_filters;
  DesktopCapturer::SourceList* const result;
};

bool IsWindowOwnedByCurrentProcess(HWND hwnd) {
  DWORD process_id;
  GetWindowThreadProcessId(hwnd, &process_id);
  return process_id == GetCurrentProcessId();
}

BOOL CALLBACK GetWindowListHandler(HWND hwnd, LPARAM param) {
  GetWindowListParams* params = reinterpret_cast<GetWindowListParams*>(param);
  DesktopCapturer::SourceList* list = params->result;

  // Skip invisible and minimized windows
  if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) {
    return TRUE;
  }

  // Skip windows which are not presented in the taskbar,
  // namely owned window if they don't have the app window style set
  HWND owner = GetWindow(hwnd, GW_OWNER);
  LONG exstyle = GetWindowLong(hwnd, GWL_EXSTYLE);
  if (owner && !(exstyle & WS_EX_APPWINDOW)) {
    return TRUE;
  }

  // Filter out windows that match the extended styles the caller has specified,
  // e.g. WS_EX_TOOLWINDOW for capturers that don't support overlay windows.
  if (exstyle & params->ex_style_filters) {
    return TRUE;
  }

  if (params->ignore_unresponsive && !IsWindowResponding(hwnd)) {
    return TRUE;
  }

  DesktopCapturer::Source window;
  window.id = reinterpret_cast<WindowId>(hwnd);

  // GetWindowText* are potentially blocking operations if |hwnd| is
  // owned by the current process. The APIs will send messages to the window's
  // message loop, and if the message loop is waiting on this operation we will
  // enter a deadlock.
  // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtexta#remarks
  //
  // To help consumers avoid this, there is a DesktopCaptureOption to ignore
  // windows owned by the current process. Consumers should either ensure that
  // the thread running their message loop never waits on this operation, or use
  // the option to exclude these windows from the source list.
  bool owned_by_current_process = IsWindowOwnedByCurrentProcess(hwnd);
  if (owned_by_current_process && params->ignore_current_process_windows) {
    return TRUE;
  }

  // Even if consumers request to enumerate windows owned by the current
  // process, we should not call GetWindowText* on unresponsive windows owned by
  // the current process because we will hang. Unfortunately, we could still
  // hang if the window becomes unresponsive after this check, hence the option
  // to avoid these completely.
  if (!owned_by_current_process || IsWindowResponding(hwnd)) {
    const size_t kTitleLength = 500;
    WCHAR window_title[kTitleLength] = L"";
    if (GetWindowTextLength(hwnd) != 0 &&
        GetWindowTextW(hwnd, window_title, kTitleLength) > 0) {
      window.title = rtc::ToUtf8(window_title);
    }
  }

  // Skip windows when we failed to convert the title or it is empty.
  if (params->ignore_untitled && window.title.empty())
    return TRUE;

  // Capture the window class name, to allow specific window classes to be
  // skipped.
  //
  // https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassa
  // says lpszClassName field in WNDCLASS is limited by 256 symbols, so we don't
  // need to have a buffer bigger than that.
  const size_t kMaxClassNameLength = 256;
  WCHAR class_name[kMaxClassNameLength] = L"";
  const int class_name_length =
      GetClassNameW(hwnd, class_name, kMaxClassNameLength);
  if (class_name_length < 1)
    return TRUE;

  // Skip Program Manager window.
  if (wcscmp(class_name, L"Progman") == 0)
    return TRUE;

  // Skip Start button window on Windows Vista, Windows 7.
  // On Windows 8, Windows 8.1, Windows 10 Start button is not a top level
  // window, so it will not be examined here.
  if (wcscmp(class_name, L"Button") == 0)
    return TRUE;

  list->push_back(window);

  return TRUE;
}

}  // namespace

// Prefix used to match the window class for Chrome windows.
const wchar_t kChromeWindowClassPrefix[] = L"Chrome_WidgetWin_";

// The hiddgen taskbar will leave a 2 pixel margin on the screen.
const int kHiddenTaskbarMarginOnScreen = 2;

bool GetWindowRect(HWND window, DesktopRect* result) {
  RECT rect;
  if (!::GetWindowRect(window, &rect)) {
    return false;
  }
  *result = DesktopRect::MakeLTRB(rect.left, rect.top, rect.right, rect.bottom);
  return true;
}

bool GetCroppedWindowRect(HWND window,
                          bool avoid_cropping_border,
                          DesktopRect* cropped_rect,
                          DesktopRect* original_rect) {
  DesktopRect window_rect;
  if (!GetWindowRect(window, &window_rect)) {
    return false;
  }

  if (original_rect) {
    *original_rect = window_rect;
  }
  *cropped_rect = window_rect;

  bool is_maximized = false;
  if (!IsWindowMaximized(window, &is_maximized)) {
    return false;
  }

  // As of Windows8, transparent resize borders are added by the OS at
  // left/bottom/right sides of a resizeable window. If the cropped window
  // doesn't remove these borders, the background will be exposed a bit.
  if (rtc::IsWindows8OrLater() || is_maximized) {
    // Only apply this cropping to windows with a resize border (otherwise,
    // it'd clip the edges of captured pop-up windows without this border).
    LONG style = GetWindowLong(window, GWL_STYLE);
    if (style & WS_THICKFRAME || style & DS_MODALFRAME) {
      int width = GetSystemMetrics(SM_CXSIZEFRAME);
      int bottom_height = GetSystemMetrics(SM_CYSIZEFRAME);
      const int visible_border_height = GetSystemMetrics(SM_CYBORDER);
      int top_height = visible_border_height;

      // If requested, avoid cropping the visible window border. This is used
      // for pop-up windows to include their border, but not for the outermost
      // window (where a partially-transparent border may expose the
      // background a bit).
      if (avoid_cropping_border) {
        width = std::max(0, width - GetSystemMetrics(SM_CXBORDER));
        bottom_height = std::max(0, bottom_height - visible_border_height);
        top_height = 0;
      }
      cropped_rect->Extend(-width, -top_height, -width, -bottom_height);
    }
  }

  return true;
}

bool GetWindowContentRect(HWND window, DesktopRect* result) {
  if (!GetWindowRect(window, result)) {
    return false;
  }

  RECT rect;
  if (!::GetClientRect(window, &rect)) {
    return false;
  }

  const int width = rect.right - rect.left;
  // The GetClientRect() is not expected to return a larger area than
  // GetWindowRect().
  if (width > 0 && width < result->width()) {
    // - GetClientRect() always set the left / top of RECT to 0. So we need to
    //   estimate the border width from GetClientRect() and GetWindowRect().
    // - Border width of a window varies according to the window type.
    // - GetClientRect() excludes the title bar, which should be considered as
    //   part of the content and included in the captured frame. So we always
    //   estimate the border width according to the window width.
    // - We assume a window has same border width in each side.
    // So we shrink half of the width difference from all four sides.
    const int shrink = ((width - result->width()) / 2);
    // When |shrink| is negative, DesktopRect::Extend() shrinks itself.
    result->Extend(shrink, 0, shrink, 0);
    // Usually this should not happen, just in case we have received a strange
    // window, which has only left and right borders.
    if (result->height() > shrink * 2) {
      result->Extend(0, shrink, 0, shrink);
    }
    RTC_DCHECK(!result->is_empty());
  }

  return true;
}

int GetWindowRegionTypeWithBoundary(HWND window, DesktopRect* result) {
  win::ScopedGDIObject<HRGN, win::DeleteObjectTraits<HRGN>> scoped_hrgn(
      CreateRectRgn(0, 0, 0, 0));
  const int region_type = GetWindowRgn(window, scoped_hrgn.Get());

  if (region_type == SIMPLEREGION) {
    RECT rect;
    GetRgnBox(scoped_hrgn.Get(), &rect);
    *result =
        DesktopRect::MakeLTRB(rect.left, rect.top, rect.right, rect.bottom);
  }
  return region_type;
}

bool GetDcSize(HDC hdc, DesktopSize* size) {
  win::ScopedGDIObject<HGDIOBJ, win::DeleteObjectTraits<HGDIOBJ>> scoped_hgdi(
      GetCurrentObject(hdc, OBJ_BITMAP));
  BITMAP bitmap;
  memset(&bitmap, 0, sizeof(BITMAP));
  if (GetObject(scoped_hgdi.Get(), sizeof(BITMAP), &bitmap) == 0) {
    return false;
  }
  size->set(bitmap.bmWidth, bitmap.bmHeight);
  return true;
}

bool IsWindowMaximized(HWND window, bool* result) {
  WINDOWPLACEMENT placement;
  memset(&placement, 0, sizeof(WINDOWPLACEMENT));
  placement.length = sizeof(WINDOWPLACEMENT);
  if (!::GetWindowPlacement(window, &placement)) {
    return false;
  }

  *result = (placement.showCmd == SW_SHOWMAXIMIZED);
  return true;
}

bool IsWindowValidAndVisible(HWND window) {
  return IsWindow(window) && IsWindowVisible(window) && !IsIconic(window);
}

bool IsWindowResponding(HWND window) {
  // 50ms is chosen in case the system is under heavy load, but it's also not
  // too long to delay window enumeration considerably.
  const UINT uTimeoutMs = 50;
  return SendMessageTimeout(window, WM_NULL, 0, 0, SMTO_ABORTIFHUNG, uTimeoutMs,
                            nullptr);
}

bool GetWindowList(int flags,
                   DesktopCapturer::SourceList* windows,
                   LONG ex_style_filters) {
  GetWindowListParams params(flags, ex_style_filters, windows);
  return ::EnumWindows(&GetWindowListHandler,
                       reinterpret_cast<LPARAM>(&params)) != 0;
}

// WindowCaptureHelperWin implementation.
WindowCaptureHelperWin::WindowCaptureHelperWin() {
  // Try to load dwmapi.dll dynamically since it is not available on XP.
  dwmapi_library_ = LoadLibraryW(L"dwmapi.dll");
  if (dwmapi_library_) {
    func_ = reinterpret_cast<DwmIsCompositionEnabledFunc>(
        GetProcAddress(dwmapi_library_, "DwmIsCompositionEnabled"));
    dwm_get_window_attribute_func_ =
        reinterpret_cast<DwmGetWindowAttributeFunc>(
            GetProcAddress(dwmapi_library_, "DwmGetWindowAttribute"));
  }

  if (rtc::IsWindows10OrLater()) {
    if (FAILED(::CoCreateInstance(__uuidof(VirtualDesktopManager), nullptr,
                                  CLSCTX_ALL,
                                  IID_PPV_ARGS(&virtual_desktop_manager_)))) {
      RTC_LOG(LS_WARNING) << "Fail to create instance of VirtualDesktopManager";
    }
  }
}

WindowCaptureHelperWin::~WindowCaptureHelperWin() {
  if (dwmapi_library_) {
    FreeLibrary(dwmapi_library_);
  }
}

bool WindowCaptureHelperWin::IsAeroEnabled() {
  BOOL result = FALSE;
  if (func_) {
    func_(&result);
  }
  return result != FALSE;
}

// This is just a best guess of a notification window. Chrome uses the Windows
// native framework for showing notifications. So far what we know about such a
// window includes: no title, class name with prefix "Chrome_WidgetWin_" and
// with certain extended styles.
bool WindowCaptureHelperWin::IsWindowChromeNotification(HWND hwnd) {
  const size_t kTitleLength = 32;
  WCHAR window_title[kTitleLength];
  GetWindowTextW(hwnd, window_title, kTitleLength);
  if (wcsnlen_s(window_title, kTitleLength) != 0) {
    return false;
  }

  const size_t kClassLength = 256;
  WCHAR class_name[kClassLength];
  const int class_name_length = GetClassNameW(hwnd, class_name, kClassLength);
  if (class_name_length < 1 ||
      wcsncmp(class_name, kChromeWindowClassPrefix,
              wcsnlen_s(kChromeWindowClassPrefix, kClassLength)) != 0) {
    return false;
  }

  const LONG exstyle = GetWindowLong(hwnd, GWL_EXSTYLE);
  if ((exstyle & WS_EX_NOACTIVATE) && (exstyle & WS_EX_TOOLWINDOW) &&
      (exstyle & WS_EX_TOPMOST)) {
    return true;
  }

  return false;
}

// |content_rect| is preferred because,
// 1. WindowCapturerWinGdi is using GDI capturer, which cannot capture DX
// output.
//    So ScreenCapturer should be used as much as possible to avoid
//    uncapturable cases. Note: lots of new applications are using DX output
//    (hardware acceleration) to improve the performance which cannot be
//    captured by WindowCapturerWinGdi. See bug http://crbug.com/741770.
// 2. WindowCapturerWinGdi is still useful because we do not want to expose the
//    content on other windows if the target window is covered by them.
// 3. Shadow and borders should not be considered as "content" on other
//    windows because they do not expose any useful information.
//
// So we can bear the false-negative cases (target window is covered by the
// borders or shadow of other windows, but we have not detected it) in favor
// of using ScreenCapturer, rather than let the false-positive cases (target
// windows is only covered by borders or shadow of other windows, but we treat
// it as overlapping) impact the user experience.
bool WindowCaptureHelperWin::AreWindowsOverlapping(
    HWND hwnd,
    HWND selected_hwnd,
    const DesktopRect& selected_window_rect) {
  DesktopRect content_rect;
  if (!GetWindowContentRect(hwnd, &content_rect)) {
    // Bail out if failed to get the window area.
    return true;
  }
  content_rect.IntersectWith(selected_window_rect);

  if (content_rect.is_empty()) {
    return false;
  }

  // When the taskbar is automatically hidden, it will leave a 2 pixel margin on
  // the screen which will overlap the maximized selected window that will use
  // up the full screen area. Since there is no solid way to identify a hidden
  // taskbar window, we have to make an exemption here if the overlapping is
  // 2 x screen_width/height to a maximized window.
  bool is_maximized = false;
  IsWindowMaximized(selected_hwnd, &is_maximized);
  bool overlaps_hidden_horizontal_taskbar =
      selected_window_rect.width() == content_rect.width() &&
      content_rect.height() == kHiddenTaskbarMarginOnScreen;
  bool overlaps_hidden_vertical_taskbar =
      selected_window_rect.height() == content_rect.height() &&
      content_rect.width() == kHiddenTaskbarMarginOnScreen;
  if (is_maximized && (overlaps_hidden_horizontal_taskbar ||
                       overlaps_hidden_vertical_taskbar)) {
    return false;
  }

  return true;
}

bool WindowCaptureHelperWin::IsWindowOnCurrentDesktop(HWND hwnd) {
  // Make sure the window is on the current virtual desktop.
  if (virtual_desktop_manager_) {
    BOOL on_current_desktop;
    if (SUCCEEDED(virtual_desktop_manager_->IsWindowOnCurrentVirtualDesktop(
            hwnd, &on_current_desktop)) &&
        !on_current_desktop) {
      return false;
    }
  }
  return true;
}

bool WindowCaptureHelperWin::IsWindowVisibleOnCurrentDesktop(HWND hwnd) {
  return IsWindowValidAndVisible(hwnd) && IsWindowOnCurrentDesktop(hwnd) &&
         !IsWindowCloaked(hwnd);
}

// A cloaked window is composited but not visible to the user.
// Example: Cortana or the Action Center when collapsed.
bool WindowCaptureHelperWin::IsWindowCloaked(HWND hwnd) {
  if (!dwm_get_window_attribute_func_) {
    // Does not apply.
    return false;
  }

  int res = 0;
  if (dwm_get_window_attribute_func_(hwnd, DWMWA_CLOAKED, &res, sizeof(res)) !=
      S_OK) {
    // Cannot tell so assume not cloaked for backward compatibility.
    return false;
  }

  return res != 0;
}

bool WindowCaptureHelperWin::EnumerateCapturableWindows(
    DesktopCapturer::SourceList* results,
    bool enumerate_current_process_windows,
    LONG ex_style_filters) {
  int flags = (GetWindowListFlags::kIgnoreUntitled |
               GetWindowListFlags::kIgnoreUnresponsive);
  if (!enumerate_current_process_windows) {
    flags |= GetWindowListFlags::kIgnoreCurrentProcessWindows;
  }

  if (!webrtc::GetWindowList(flags, results, ex_style_filters)) {
    return false;
  }

  for (auto it = results->begin(); it != results->end();) {
    if (!IsWindowVisibleOnCurrentDesktop(reinterpret_cast<HWND>(it->id))) {
      it = results->erase(it);
    } else {
      ++it;
    }
  }

  return true;
}

}  // namespace webrtc