Add Presentation Mode Support for Capturing OpenOffice Impress document windows

* Add OpenOfficeApplicationHandler for MacOS and MS Windows.
* List of available sources for FullScreenWindowDetector on MacOS can include a window with empty title along with titled window for one application.
* List of available sources for FullScreenWindowDetector on MS Windows can include a window with empty title or invisible ones.

Bug: webrtc:11462
Change-Id: Id09537579ef6617dee29759c66dc9f7493166ca8
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/171723
Commit-Queue: Jamie Walch <jamiewalch@chromium.org>
Reviewed-by: Jamie Walch <jamiewalch@chromium.org>
Reviewed-by: Wez <wez@google.com>
Cr-Commit-Position: refs/heads/master@{#32561}
This commit is contained in:
Roman Gaiu
2020-08-14 11:13:11 -07:00
committed by Commit Bot
parent 649648e722
commit 439ffe462a
7 changed files with 304 additions and 88 deletions

View File

@ -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<HWND>(source.id);
return !window_capture_helper_
.IsWindowVisibleOnCurrentDesktop(hwnd);
});
result.erase(it, result.end());
sources->swap(result);
return true;
});
}
win_capturer->SelectSource(GetWindowToCapture());

View File

@ -14,6 +14,7 @@
#include <functional>
#include <string>
#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<bool(const DesktopCapturer::Source&)>;
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<FullScreenApplicationHandler>
@ -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<OpenOfficeApplicationHandler>(sourceId);
}
if (predicate) {

View File

@ -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;
}

View File

@ -14,6 +14,9 @@
#include <memory>
#include <string>
#include <vector>
#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<size_t>(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<HWND>(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<HWND>(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<HWND>(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<FullScreenApplicationHandler>
CreateFullScreenWinApplicationHandler(DesktopCapturer::SourceId source_id) {
std::unique_ptr<FullScreenApplicationHandler> result;
std::wstring exe_path = GetPathByWindowId(reinterpret_cast<HWND>(source_id));
HWND hwnd = reinterpret_cast<HWND>(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<FullScreenPowerPointHandler>(source_id);
} else if (file_name == L"SOFFICE.BIN" &&
absl::EndsWith(WindowText(hwnd), "OpenOffice Impress")) {
result = std::make_unique<OpenOfficeApplicationHandler>(source_id);
}
return result;

View File

@ -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<GetWindowListParams*>(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<WindowId>(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<DesktopCapturer::SourceList*>(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<WindowId>(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<LPARAM>(&params)) != 0;
}
// WindowCaptureHelperWin implementation.
@ -374,9 +414,11 @@ bool WindowCaptureHelperWin::IsWindowCloaked(HWND hwnd) {
bool WindowCaptureHelperWin::EnumerateCapturableWindows(
DesktopCapturer::SourceList* results) {
LPARAM param = reinterpret_cast<LPARAM>(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<HWND>(it->id))) {

View File

@ -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,

View File

@ -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_);