blob: 9e33e56c2d865b2b6c0bff6546cf3766d1f85ee7 [file] [log] [blame]
/*
* 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, DesktopCapturer::SourceList* result)
: ignoreUntitled(flags & GetWindowListFlags::kIgnoreUntitled),
ignoreUnresponsive(flags & GetWindowListFlags::kIgnoreUnresponsive),
result(result) {}
const bool ignoreUntitled;
const bool ignoreUnresponsive;
DesktopCapturer::SourceList* const result;
};
// If a window is owned by the current process and unresponsive, then making a
// blocking call such as GetWindowText may lead to a deadlock.
//
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtexta#remarks
bool CanSafelyMakeBlockingCalls(HWND hwnd) {
DWORD process_id;
GetWindowThreadProcessId(hwnd, &process_id);
if (process_id != GetCurrentProcessId() || IsWindowResponding(hwnd)) {
return true;
}
return false;
}
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;
}
if (params->ignoreUnresponsive && !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, and can lead to a deadlock if the message
// pump is waiting on this thread. If we've filtered out unresponsive
// windows, this is not a concern, but otherwise we need to check if we can
// safely make blocking calls.
if (params->ignoreUnresponsive || CanSafelyMakeBlockingCalls(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->ignoreUntitled && 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) {
GetWindowListParams params(flags, 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) {
if (!webrtc::GetWindowList((GetWindowListFlags::kIgnoreUntitled |
GetWindowListFlags::kIgnoreUnresponsive),
results)) {
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