/* * Copyright (c) 2019 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/full_screen_win_application_handler.h" #include #include #include #include #include #include #include #include #include #include "absl/strings/ascii.h" #include "absl/strings/match.h" #include "absl/strings/string_view.h" #include "modules/desktop_capture/desktop_capturer.h" #include "modules/desktop_capture/full_screen_application_handler.h" #include "rtc_base/logging.h" // For RTC_LOG_GLE #include "rtc_base/string_utils.h" #include "system_wrappers/include/metrics.h" void RecordFullScreenDetectorResult(FullScreenDetectorResult result) { RTC_HISTOGRAM_ENUMERATION( "WebRTC.Screenshare.FullScreenDetectorResult", static_cast(result), static_cast(FullScreenDetectorResult::kMaxValue)); } 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; } bool IsFullScreenWindow(HWND wnd) { // Get the monitor info of the display monitor where the window is. MONITORINFO monitor_info = {sizeof(monitor_info)}; if (!::GetMonitorInfo(::MonitorFromWindow(wnd, MONITOR_DEFAULTTONEAREST), &monitor_info)) { return false; } // Verifies if the window rectangle is same as the monitor. RECT wnd_rect; if (!::GetWindowRect(wnd, &wnd_rect) || !::EqualRect(&wnd_rect, &monitor_info.rcMonitor)) { return false; } // Check if the window style does not have WS_OVERLAPPEDWINDOW as the full // screen window should not have a title bar or border. return !(::GetWindowLongPtr(wnd, GWL_STYLE) & WS_OVERLAPPEDWINDOW); } std::string WindowText(HWND window) { size_t len = ::GetWindowTextLength(window); if (len == 0) { return std::string(); } std::vector buffer(len + 1, 0); size_t copied = ::GetWindowTextW(window, buffer.data(), buffer.size()); if (copied == 0) { return std::string(); } return ToUtf8(buffer.data(), copied); } DWORD WindowProcessId(HWND window) { DWORD dwProcessId = 0; ::GetWindowThreadProcessId(window, &dwProcessId); return dwProcessId; } std::wstring FileNameFromPath(const std::wstring& path) { auto found = path.rfind(L"\\"); if (found == std::string::npos) { return path; } return path.substr(found + 1); } // Returns windows which belong to given process id // `sources` is a full list of available windows // `processId` is a process identifier (window owner) // `window_to_exclude` is a window to be exluded from result DesktopCapturer::SourceList GetProcessWindows( const DesktopCapturer::SourceList& sources, DWORD processId, HWND window_to_exclude) { DesktopCapturer::SourceList result; std::copy_if(sources.begin(), sources.end(), std::back_inserter(result), [&](DesktopCapturer::Source source) { const HWND source_hwnd = reinterpret_cast(source.id); return window_to_exclude != source_hwnd && WindowProcessId(source_hwnd) == processId; }); return result; } } // namespace FullScreenPowerPointHandler::FullScreenPowerPointHandler( DesktopCapturer::SourceId sourceId) : FullScreenApplicationHandler(sourceId), was_slide_show_created_after_capture_started_(false), full_screen_detector_result_(FullScreenDetectorResult::kUnknown) {} DesktopCapturer::SourceId FullScreenPowerPointHandler::FindFullScreenWindow( const DesktopCapturer::SourceList& window_list, int64_t timestamp) const { if (!UseHeuristicFullscreenPowerPointWindows() || window_list.empty()) { return 0; } HWND original_window = reinterpret_cast(GetSourceId()); if (GetWindowType(original_window) != WindowType::kEditor) { return 0; } DesktopCapturer::SourceList powerpoint_windows = GetProcessWindows( window_list, WindowProcessId(original_window), original_window); // No relevant windows with the same process id as the `original_window` were // found. if (powerpoint_windows.empty()) { was_slide_show_created_after_capture_started_ = true; return 0; } bool do_same_title_editors_exist = false; bool does_slide_show_exist = false; DesktopCapturer::SourceId full_screen_slide_show_id = 0; const std::string original_document_title = GetDocumentTitleFromEditor(original_window); auto result = full_screen_detector_result_; for (const auto& source : powerpoint_windows) { HWND window = reinterpret_cast(source.id); // If another PowerPoint editor window with the same title exists, then we // don't use the heuristic as we don't know which editor has opened the // slide show. if (GetWindowType(window) == WindowType::kEditor && GetDocumentTitleFromEditor(window) == original_document_title) { do_same_title_editors_exist = true; result = FullScreenDetectorResult::kFailureDueToSameTitleWindows; } // Looking for fullscreen slide show window for the corresponding editor // document. if (GetWindowType(window) == WindowType::kSlideShow && GetDocumentTitleFromSlideShow(window) == original_document_title) { does_slide_show_exist = true; full_screen_slide_show_id = source.id; } } if (does_slide_show_exist) { if (!was_slide_show_created_after_capture_started_) { full_screen_slide_show_id = 0; result = FullScreenDetectorResult::kFailureDueToSlideShowWasNotChosen; } else if (do_same_title_editors_exist) { full_screen_slide_show_id = 0; result = FullScreenDetectorResult::kFailureDueToSameTitleWindows; } else { result = FullScreenDetectorResult::kSuccess; } } else { was_slide_show_created_after_capture_started_ = true; } if (full_screen_detector_result_ != result) { full_screen_detector_result_ = result; RecordFullScreenDetectorResult(result); } return full_screen_slide_show_id; } void FullScreenPowerPointHandler::SetSlideShowCreationStateForTest( bool fullscreen_slide_show_started_after_capture_start) { was_slide_show_created_after_capture_started_ = fullscreen_slide_show_started_after_capture_start; } FullScreenPowerPointHandler::WindowType FullScreenPowerPointHandler::GetWindowType(HWND window) const { if (IsEditorWindow(window)) { return WindowType::kEditor; } else if (IsSlideShowWindow(window)) { return WindowType::kSlideShow; } return WindowType::kOther; } constexpr static char kDocumentTitleSeparator = '-'; std::string FullScreenPowerPointHandler::GetDocumentTitleFromEditor( HWND window) const { std::string title = WindowText(window); return std::string(absl::StripAsciiWhitespace(absl::string_view(title).substr( 0, title.rfind(kDocumentTitleSeparator)))); } std::string FullScreenPowerPointHandler::GetDocumentTitleFromSlideShow( HWND window) const { std::string title = WindowText(window); size_t position = title.find(kDocumentTitleSeparator); if (position != std::string::npos) { title = absl::StripAsciiWhitespace( absl::string_view(title).substr(position + 1, std::wstring::npos)); } size_t left_bracket_pos = title.find("["); size_t right_bracket_pos = title.rfind("]"); if (left_bracket_pos == std::string::npos || right_bracket_pos == std::string::npos || right_bracket_pos <= left_bracket_pos) { return title; } return std::string(absl::StripAsciiWhitespace(title.substr( left_bracket_pos + 1, right_bracket_pos - left_bracket_pos - 1))); } bool FullScreenPowerPointHandler::IsEditorWindow(HWND window) const { return CheckWindowClassName(window, L"PPTFrameClass"); } bool FullScreenPowerPointHandler::IsSlideShowWindow(HWND window) const { return CheckWindowClassName(window, L"screenClass") && IsFullScreenWindow(window); } 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 = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_id); if (process == NULL) { return L""; } DWORD path_len = MAX_PATH; WCHAR path[MAX_PATH]; std::wstring result; if (::QueryFullProcessImageNameW(process, 0, path, &path_len)) { result = std::wstring(path, path_len); } else { RTC_LOG_GLE(LS_ERROR) << "QueryFullProcessImageName failed."; } ::CloseHandle(process); return result; } std::unique_ptr CreateFullScreenWinApplicationHandler(DesktopCapturer::SourceId source_id) { std::unique_ptr result; 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; } } // namespace webrtc