diff --git a/modules/desktop_capture/cropping_window_capturer_win.cc b/modules/desktop_capture/cropping_window_capturer_win.cc index 6e53ca3522..de36adb01e 100644 --- a/modules/desktop_capture/cropping_window_capturer_win.cc +++ b/modules/desktop_capture/cropping_window_capturer_win.cc @@ -154,13 +154,30 @@ class CroppingWindowCapturerWin : public CroppingWindowCapturer { void CroppingWindowCapturerWin::CaptureFrame() { DesktopCapturer* win_capturer = window_capturer(); if (win_capturer) { - // Update the list of available sources and override source to capture if - // FullScreenWindowDetector returns not zero + // Feed the actual list of windows into full screen window detector. if (full_screen_window_detector_) { full_screen_window_detector_->UpdateWindowListIfNeeded( - selected_window(), - [win_capturer](DesktopCapturer::SourceList* sources) { - return win_capturer->GetSourceList(sources); + selected_window(), [this](DesktopCapturer::SourceList* sources) { + // Get the list of top level windows, including ones with empty + // title. win_capturer_->GetSourceList can't be used here + // cause it filters out the windows with empty titles and + // it uses responsiveness check which could lead to performance + // issues. + SourceList result; + if (!webrtc::GetWindowList(GetWindowListFlags::kNone, &result)) + return false; + + // Filter out windows not visible on current desktop + auto it = std::remove_if( + result.begin(), result.end(), [this](const auto& source) { + HWND hwnd = reinterpret_cast(source.id); + return !window_capture_helper_ + .IsWindowVisibleOnCurrentDesktop(hwnd); + }); + result.erase(it, result.end()); + + sources->swap(result); + return true; }); } win_capturer->SelectSource(GetWindowToCapture()); diff --git a/modules/desktop_capture/mac/full_screen_mac_application_handler.cc b/modules/desktop_capture/mac/full_screen_mac_application_handler.cc index 9e6eacce85..36e16cbe54 100644 --- a/modules/desktop_capture/mac/full_screen_mac_application_handler.cc +++ b/modules/desktop_capture/mac/full_screen_mac_application_handler.cc @@ -14,6 +14,7 @@ #include #include #include "absl/strings/match.h" +#include "api/function_view.h" #include "modules/desktop_capture/mac/window_list_utils.h" namespace webrtc { @@ -59,17 +60,17 @@ class FullScreenMacApplicationHandler : public FullScreenApplicationHandler { title_predicate_(title_predicate), owner_pid_(GetWindowOwnerPid(sourceId)) {} + protected: + using CachePredicate = + rtc::FunctionView; + void InvalidateCacheIfNeeded(const DesktopCapturer::SourceList& source_list, - int64_t timestamp) const { - // Copy only sources with the same pid + int64_t timestamp, + CachePredicate predicate) const { if (timestamp != cache_timestamp_) { cache_sources_.clear(); std::copy_if(source_list.begin(), source_list.end(), - std::back_inserter(cache_sources_), - [&](const DesktopCapturer::Source& src) { - return src.id != GetSourceId() && - GetWindowOwnerPid(src.id) == owner_pid_; - }); + std::back_inserter(cache_sources_), predicate); cache_timestamp_ = timestamp; } } @@ -77,7 +78,11 @@ class FullScreenMacApplicationHandler : public FullScreenApplicationHandler { WindowId FindFullScreenWindowWithSamePid( const DesktopCapturer::SourceList& source_list, int64_t timestamp) const { - InvalidateCacheIfNeeded(source_list, timestamp); + InvalidateCacheIfNeeded(source_list, timestamp, + [&](const DesktopCapturer::Source& src) { + return src.id != GetSourceId() && + GetWindowOwnerPid(src.id) == owner_pid_; + }); if (cache_sources_.empty()) return kCGNullWindowID; @@ -119,7 +124,7 @@ class FullScreenMacApplicationHandler : public FullScreenApplicationHandler { : FindFullScreenWindowWithSamePid(source_list, timestamp); } - private: + protected: const TitlePredicate title_predicate_; const int owner_pid_; mutable int64_t cache_timestamp_ = 0; @@ -143,6 +148,52 @@ bool slide_show_title_predicate(const std::string& original_title, return false; } +class OpenOfficeApplicationHandler : public FullScreenMacApplicationHandler { + public: + OpenOfficeApplicationHandler(DesktopCapturer::SourceId sourceId) + : FullScreenMacApplicationHandler(sourceId, nullptr) {} + + DesktopCapturer::SourceId FindFullScreenWindow( + const DesktopCapturer::SourceList& source_list, + int64_t timestamp) const override { + InvalidateCacheIfNeeded(source_list, timestamp, + [&](const DesktopCapturer::Source& src) { + return GetWindowOwnerPid(src.id) == owner_pid_; + }); + + const auto original_window = GetSourceId(); + const std::string original_title = GetWindowTitle(original_window); + + // Check if we have only one document window, otherwise it's not possible + // to securely match a document window and a slide show window which has + // empty title. + if (std::any_of(cache_sources_.begin(), cache_sources_.end(), + [&original_title](const DesktopCapturer::Source& src) { + return src.title.length() && src.title != original_title; + })) { + return kCGNullWindowID; + } + + MacDesktopConfiguration desktop_config = + MacDesktopConfiguration::GetCurrent( + MacDesktopConfiguration::TopLeftOrigin); + + // Looking for slide show window, + // it must be a full screen window with empty title + const auto slide_show_window = std::find_if( + cache_sources_.begin(), cache_sources_.end(), [&](const auto& src) { + return src.title.empty() && + IsWindowFullScreen(desktop_config, src.id); + }); + + if (slide_show_window == cache_sources_.end()) { + return kCGNullWindowID; + } + + return slide_show_window->id; + } +}; + } // namespace std::unique_ptr @@ -154,6 +205,7 @@ CreateFullScreenMacApplicationHandler(DesktopCapturer::SourceId sourceId) { if (path_length > 0) { const char* last_slash = strrchr(buffer, '/'); const std::string name{last_slash ? last_slash + 1 : buffer}; + const std::string owner_name = GetWindowOwnerName(sourceId); FullScreenMacApplicationHandler::TitlePredicate predicate = nullptr; if (name.find("Google Chrome") == 0 || name == "Chromium") { predicate = equal_title_predicate; @@ -161,6 +213,8 @@ CreateFullScreenMacApplicationHandler(DesktopCapturer::SourceId sourceId) { predicate = slide_show_title_predicate; } else if (name == "Keynote") { predicate = equal_title_predicate; + } else if (owner_name == "OpenOffice") { + return std::make_unique(sourceId); } if (predicate) { diff --git a/modules/desktop_capture/mac/window_list_utils.cc b/modules/desktop_capture/mac/window_list_utils.cc index 67cf81c5ce..56d87ceaae 100644 --- a/modules/desktop_capture/mac/window_list_utils.cc +++ b/modules/desktop_capture/mac/window_list_utils.cc @@ -303,7 +303,7 @@ std::string GetWindowOwnerName(CFDictionaryRef window) { std::string GetWindowOwnerName(CGWindowID id) { std::string owner_name; if (GetWindowRef(id, [&owner_name](CFDictionaryRef window) { - owner_name = GetWindowOwnerPid(window); + owner_name = GetWindowOwnerName(window); })) { return owner_name; } diff --git a/modules/desktop_capture/win/full_screen_win_application_handler.cc b/modules/desktop_capture/win/full_screen_win_application_handler.cc index 0b7e3fc437..dd21410b03 100644 --- a/modules/desktop_capture/win/full_screen_win_application_handler.cc +++ b/modules/desktop_capture/win/full_screen_win_application_handler.cc @@ -14,6 +14,9 @@ #include #include #include +#include "absl/strings/match.h" +#include "modules/desktop_capture/win/screen_capture_utils.h" +#include "modules/desktop_capture/win/window_capture_utils.h" #include "rtc_base/arraysize.h" #include "rtc_base/logging.h" // For RTC_LOG_GLE #include "rtc_base/string_utils.h" @@ -21,6 +24,25 @@ namespace webrtc { namespace { +// Utility function to verify that |window| has class name equal to |class_name| +bool CheckWindowClassName(HWND window, const wchar_t* class_name) { + const size_t classNameLength = wcslen(class_name); + + // 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. + constexpr size_t kMaxClassNameLength = 256; + WCHAR buffer[kMaxClassNameLength]; + + const int length = ::GetClassNameW(window, buffer, kMaxClassNameLength); + if (length <= 0) + return false; + + if (static_cast(length) != classNameLength) + return false; + return wcsncmp(buffer, class_name, classNameLength) == 0; +} + std::string WindowText(HWND window) { size_t len = ::GetWindowTextLength(window); if (len == 0) @@ -146,20 +168,7 @@ class FullScreenPowerPointHandler : public FullScreenApplicationHandler { } bool IsEditorWindow(HWND window) const { - constexpr WCHAR kScreenClassName[] = L"PPTFrameClass"; - constexpr size_t kScreenClassNameLength = arraysize(kScreenClassName) - 1; - - // We need to verify that window class is equal to |kScreenClassName|. - // To do that we need a buffer large enough to include a null terminated - // string one code point bigger than |kScreenClassName|. It will help us to - // check that size of class name string returned by GetClassNameW is equal - // to |kScreenClassNameLength| not being limited by size of buffer (case - // when |kScreenClassName| is a prefix for class name string). - WCHAR buffer[arraysize(kScreenClassName) + 3]; - const int length = ::GetClassNameW(window, buffer, arraysize(buffer)); - if (length != kScreenClassNameLength) - return false; - return wcsncmp(buffer, kScreenClassName, kScreenClassNameLength) == 0; + return CheckWindowClassName(window, L"PPTFrameClass"); } bool IsSlideShowWindow(HWND window) const { @@ -170,6 +179,74 @@ class FullScreenPowerPointHandler : public FullScreenApplicationHandler { } }; +class OpenOfficeApplicationHandler : public FullScreenApplicationHandler { + public: + explicit OpenOfficeApplicationHandler(DesktopCapturer::SourceId sourceId) + : FullScreenApplicationHandler(sourceId) {} + + DesktopCapturer::SourceId FindFullScreenWindow( + const DesktopCapturer::SourceList& window_list, + int64_t timestamp) const override { + if (window_list.empty()) + return 0; + + DWORD process_id = WindowProcessId(reinterpret_cast(GetSourceId())); + + DesktopCapturer::SourceList app_windows = + GetProcessWindows(window_list, process_id, nullptr); + + DesktopCapturer::SourceList document_windows; + std::copy_if( + app_windows.begin(), app_windows.end(), + std::back_inserter(document_windows), + [this](const DesktopCapturer::Source& x) { return IsEditorWindow(x); }); + + // Check if we have only one document window, otherwise it's not possible + // to securely match a document window and a slide show window which has + // empty title. + if (document_windows.size() != 1) { + return 0; + } + + // Check if document window has been selected as a source + if (document_windows.front().id != GetSourceId()) { + return 0; + } + + // Check if we have a slide show window. + auto slide_show_window = + std::find_if(app_windows.begin(), app_windows.end(), + [this](const DesktopCapturer::Source& x) { + return IsSlideShowWindow(x); + }); + + if (slide_show_window == app_windows.end()) + return 0; + + return slide_show_window->id; + } + + private: + bool IsEditorWindow(const DesktopCapturer::Source& source) const { + if (source.title.empty()) { + return false; + } + + return CheckWindowClassName(reinterpret_cast(source.id), L"SALFRAME"); + } + + bool IsSlideShowWindow(const DesktopCapturer::Source& source) const { + // Check title size to filter out a Presenter Control window which shares + // window class with Slide Show window but has non empty title. + if (!source.title.empty()) { + return false; + } + + return CheckWindowClassName(reinterpret_cast(source.id), + L"SALTMPSUBFRAME"); + } +}; + std::wstring GetPathByWindowId(HWND window_id) { DWORD process_id = WindowProcessId(window_id); HANDLE process = @@ -193,13 +270,17 @@ std::wstring GetPathByWindowId(HWND window_id) { std::unique_ptr CreateFullScreenWinApplicationHandler(DesktopCapturer::SourceId source_id) { std::unique_ptr result; - std::wstring exe_path = GetPathByWindowId(reinterpret_cast(source_id)); + HWND hwnd = reinterpret_cast(source_id); + std::wstring exe_path = GetPathByWindowId(hwnd); std::wstring file_name = FileNameFromPath(exe_path); std::transform(file_name.begin(), file_name.end(), file_name.begin(), std::towupper); if (file_name == L"POWERPNT.EXE") { result = std::make_unique(source_id); + } else if (file_name == L"SOFFICE.BIN" && + absl::EndsWith(WindowText(hwnd), "OpenOffice Impress")) { + result = std::make_unique(source_id); } return result; diff --git a/modules/desktop_capture/win/window_capture_utils.cc b/modules/desktop_capture/win/window_capture_utils.cc index 006870f3c5..e49c179fd3 100644 --- a/modules/desktop_capture/win/window_capture_utils.cc +++ b/modules/desktop_capture/win/window_capture_utils.cc @@ -24,6 +24,93 @@ 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; +}; + +BOOL CALLBACK GetWindowListHandler(HWND hwnd, LPARAM param) { + GetWindowListParams* params = reinterpret_cast(param); + DesktopCapturer::SourceList* list = params->result; + + // Skip untitled window if ignoreUntitled specified + if (params->ignoreUntitled && GetWindowTextLength(hwnd) == 0) { + return TRUE; + } + + // 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 ignoreUnresponsive is true then skip unresponsive windows. Set timout + // with 50ms, in case system is under heavy load, the check can wait longer + // but wont' be too long to delay the the enumeration. + const UINT uTimeout = 50; // ms + if (params->ignoreUnresponsive && + !SendMessageTimeout(hwnd, WM_NULL, 0, 0, SMTO_ABORTIFHUNG, uTimeout, + nullptr)) { + 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; + + DesktopCapturer::Source window; + window.id = reinterpret_cast(hwnd); + + const size_t kTitleLength = 500; + WCHAR window_title[kTitleLength] = L""; + if (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; + + list->push_back(window); + + return TRUE; +} + +} // namespace + // Prefix used to match the window class for Chrome windows. const wchar_t kChromeWindowClassPrefix[] = L"Chrome_WidgetWin_"; @@ -165,57 +252,10 @@ bool IsWindowValidAndVisible(HWND window) { return IsWindow(window) && IsWindowVisible(window) && !IsIconic(window); } -BOOL CALLBACK FilterUncapturableWindows(HWND hwnd, LPARAM param) { - DesktopCapturer::SourceList* list = - reinterpret_cast(param); - - // Skip windows that are invisible, minimized, have no title, or are owned, - // unless they have the app window style set. - int len = GetWindowTextLength(hwnd); - HWND owner = GetWindow(hwnd, GW_OWNER); - LONG exstyle = GetWindowLong(hwnd, GWL_EXSTYLE); - if (len == 0 || !IsWindowValidAndVisible(hwnd) || - (owner && !(exstyle & WS_EX_APPWINDOW))) { - return TRUE; - } - - // Skip unresponsive windows. Set timout with 50ms, in case system is under - // heavy load. We could wait longer and have a lower false negative, but that - // would delay the the enumeration. - const UINT timeout = 50; // ms - if (!SendMessageTimeout(hwnd, WM_NULL, 0, 0, SMTO_ABORTIFHUNG, timeout, - nullptr)) { - return TRUE; - } - - // Skip the Program Manager window and the Start button. - WCHAR class_name[256]; - const int class_name_length = - GetClassNameW(hwnd, class_name, arraysize(class_name)); - if (class_name_length < 1) - return TRUE; - - // Skip Program Manager window and the Start button. This is the same logic - // that's used in Win32WindowPicker in libjingle. Consider filtering other - // windows as well (e.g. toolbars). - if (wcscmp(class_name, L"Progman") == 0 || wcscmp(class_name, L"Button") == 0) - return TRUE; - - DesktopCapturer::Source window; - window.id = reinterpret_cast(hwnd); - - // Truncate the title if it's longer than 500 characters. - WCHAR window_title[500]; - GetWindowTextW(hwnd, window_title, arraysize(window_title)); - window.title = rtc::ToUtf8(window_title); - - // Skip windows when we failed to convert the title or it is empty. - if (window.title.empty()) - return TRUE; - - list->push_back(window); - - return TRUE; +bool GetWindowList(int flags, DesktopCapturer::SourceList* windows) { + GetWindowListParams params(flags, windows); + return ::EnumWindows(&GetWindowListHandler, + reinterpret_cast(¶ms)) != 0; } // WindowCaptureHelperWin implementation. @@ -374,9 +414,11 @@ bool WindowCaptureHelperWin::IsWindowCloaked(HWND hwnd) { bool WindowCaptureHelperWin::EnumerateCapturableWindows( DesktopCapturer::SourceList* results) { - LPARAM param = reinterpret_cast(results); - if (!EnumWindows(&FilterUncapturableWindows, param)) + if (!webrtc::GetWindowList((GetWindowListFlags::kIgnoreUntitled | + GetWindowListFlags::kIgnoreUnresponsive), + results)) { return false; + } for (auto it = results->begin(); it != results->end();) { if (!IsWindowVisibleOnCurrentDesktop(reinterpret_cast(it->id))) { diff --git a/modules/desktop_capture/win/window_capture_utils.h b/modules/desktop_capture/win/window_capture_utils.h index af55ceb534..6e99ee9678 100644 --- a/modules/desktop_capture/win/window_capture_utils.h +++ b/modules/desktop_capture/win/window_capture_utils.h @@ -71,10 +71,20 @@ bool IsWindowMaximized(HWND window, bool* result); // visible, and that it is not minimized. bool IsWindowValidAndVisible(HWND window); -// This function is passed into the EnumWindows API and filters out windows that -// we don't want to capture, e.g. minimized or unresponsive windows and the -// Start menu. -BOOL CALLBACK FilterUncapturableWindows(HWND hwnd, LPARAM param); +enum GetWindowListFlags { + kNone = 0x00, + kIgnoreUntitled = 1 << 0, + kIgnoreUnresponsive = 1 << 1, +}; + +// Retrieves the list of top-level windows on the screen. +// Some windows will be ignored: +// - Those that are invisible or minimized. +// - Program Manager & Start menu. +// - [with kIgnoreUntitled] windows with no title. +// - [with kIgnoreUnresponsive] windows that unresponsive. +// Returns false if native APIs failed. +bool GetWindowList(int flags, DesktopCapturer::SourceList* windows); typedef HRESULT(WINAPI* DwmIsCompositionEnabledFunc)(BOOL* enabled); typedef HRESULT(WINAPI* DwmGetWindowAttributeFunc)(HWND hwnd, diff --git a/modules/desktop_capture/window_capturer_mac.mm b/modules/desktop_capture/window_capturer_mac.mm index 96f89eb14b..cbbc500613 100644 --- a/modules/desktop_capture/window_capturer_mac.mm +++ b/modules/desktop_capture/window_capturer_mac.mm @@ -161,7 +161,19 @@ void WindowCapturerMac::CaptureFrame() { if (full_screen_window_detector_) { full_screen_window_detector_->UpdateWindowListIfNeeded( window_id_, [](DesktopCapturer::SourceList* sources) { - return webrtc::GetWindowList(sources, true, false); + // Not using webrtc::GetWindowList(sources, true, false) + // as it doesn't allow to have in the result window with + // empty title along with titled window owned by the same pid. + return webrtc::GetWindowList( + [sources](CFDictionaryRef window) { + WindowId window_id = GetWindowId(window); + if (window_id != kNullWindowId) { + sources->push_back(DesktopCapturer::Source{window_id, GetWindowTitle(window)}); + } + return true; + }, + true, + false); }); CGWindowID full_screen_window = full_screen_window_detector_->FindFullScreenWindow(window_id_);