/* * 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/mac/window_list_utils.h" #include #include #include #include #include #include #include #include #include #include "rtc_base/checks.h" static_assert(static_cast(kCGNullWindowID) == webrtc::kNullWindowId, "kNullWindowId needs to equal to kCGNullWindowID."); namespace webrtc { namespace { // WindowName of the status indicator dot shown since Monterey in the taskbar. // Testing on 12.2.1 shows this is independent of system language setting. const CFStringRef kStatusIndicator = CFSTR("StatusIndicator"); const CFStringRef kStatusIndicatorOwnerName = CFSTR("Window Server"); bool ToUtf8(const CFStringRef str16, std::string* str8) { size_t maxlen = CFStringGetMaximumSizeForEncoding(CFStringGetLength(str16), kCFStringEncodingUTF8) + 1; std::unique_ptr buffer(new char[maxlen]); if (!buffer || !CFStringGetCString(str16, buffer.get(), maxlen, kCFStringEncodingUTF8)) { return false; } str8->assign(buffer.get()); return true; } // Get CFDictionaryRef from `id` and call `on_window` against it. This function // returns false if native APIs fail, typically it indicates that the `id` does // not represent a window. `on_window` will not be called if false is returned // from this function. bool GetWindowRef(CGWindowID id, rtc::FunctionView on_window) { RTC_DCHECK(on_window); // TODO(zijiehe): `id` is a 32-bit integer, casting it to an array seems not // safe enough. Maybe we should create a new // const void* arr[] = { // reinterpret_cast(id) } // }; CFArrayRef window_id_array = CFArrayCreate(NULL, reinterpret_cast(&id), 1, NULL); CFArrayRef window_array = CGWindowListCreateDescriptionFromArray(window_id_array); bool result = false; // TODO(zijiehe): CFArrayGetCount(window_array) should always return 1. // Otherwise, we should treat it as failure. if (window_array && CFArrayGetCount(window_array)) { on_window(reinterpret_cast( CFArrayGetValueAtIndex(window_array, 0))); result = true; } if (window_array) { CFRelease(window_array); } CFRelease(window_id_array); return result; } } // namespace bool GetWindowList(rtc::FunctionView on_window, bool ignore_minimized, bool only_zero_layer) { RTC_DCHECK(on_window); // Only get on screen, non-desktop windows. // According to // https://developer.apple.com/documentation/coregraphics/cgwindowlistoption/1454105-optiononscreenonly // , when kCGWindowListOptionOnScreenOnly is used, the order of windows are in // decreasing z-order. CFArrayRef window_array = CGWindowListCopyWindowInfo( kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID); if (!window_array) return false; MacDesktopConfiguration desktop_config = MacDesktopConfiguration::GetCurrent( MacDesktopConfiguration::TopLeftOrigin); // Check windows to make sure they have an id, title, and use window layer // other than 0. CFIndex count = CFArrayGetCount(window_array); for (CFIndex i = 0; i < count; i++) { CFDictionaryRef window = reinterpret_cast( CFArrayGetValueAtIndex(window_array, i)); if (!window) { continue; } CFNumberRef window_id = reinterpret_cast( CFDictionaryGetValue(window, kCGWindowNumber)); if (!window_id) { continue; } CFNumberRef window_layer = reinterpret_cast( CFDictionaryGetValue(window, kCGWindowLayer)); if (!window_layer) { continue; } // Skip windows with layer!=0 (menu, dock). int layer; if (!CFNumberGetValue(window_layer, kCFNumberIntType, &layer)) { continue; } if (only_zero_layer && layer != 0) { continue; } // Skip windows that are minimized and not full screen. if (ignore_minimized && !IsWindowOnScreen(window) && !IsWindowFullScreen(desktop_config, window)) { continue; } // If window title is empty, only consider it if it is either on screen or // fullscreen. CFStringRef window_title = reinterpret_cast( CFDictionaryGetValue(window, kCGWindowName)); if (!window_title && !IsWindowOnScreen(window) && !IsWindowFullScreen(desktop_config, window)) { continue; } CFStringRef window_owner_name = reinterpret_cast( CFDictionaryGetValue(window, kCGWindowOwnerName)); // Ignore the red dot status indicator shown in the stats bar. Unlike the // rest of the system UI it has a window_layer of 0, so was otherwise // included. See crbug.com/1297731. if (window_title && CFEqual(window_title, kStatusIndicator) && window_owner_name && CFEqual(window_owner_name, kStatusIndicatorOwnerName)) { continue; } if (!on_window(window)) { break; } } CFRelease(window_array); return true; } bool GetWindowList(DesktopCapturer::SourceList* windows, bool ignore_minimized, bool only_zero_layer) { // Use a std::list so that iterators are preversed upon insertion and // deletion. std::list sources; std::map::const_iterator> pid_itr_map; const bool ret = GetWindowList( [&sources, &pid_itr_map](CFDictionaryRef window) { WindowId window_id = GetWindowId(window); if (window_id != kNullWindowId) { const std::string title = GetWindowTitle(window); const int pid = GetWindowOwnerPid(window); // Check if window for the same pid have been already inserted. std::map::const_iterator>::iterator itr = pid_itr_map.find(pid); // Only consider empty titles if the app has no other window with a // proper title. if (title.empty()) { std::string owner_name = GetWindowOwnerName(window); // At this time we do not know if there will be other windows // for the same pid unless they have been already inserted, hence // the check in the map. Also skip the window if owner name is // empty too. if (!owner_name.empty() && (itr == pid_itr_map.end())) { sources.push_back(DesktopCapturer::Source{window_id, pid, owner_name}); RTC_DCHECK(!sources.empty()); // Get an iterator on the last valid element in the source list. std::list::const_iterator last_source = --sources.end(); pid_itr_map.insert( std::pair::const_iterator>( pid, last_source)); } } else { sources.push_back(DesktopCapturer::Source{window_id, pid, title}); // Once the window with empty title has been removed no other empty // windows are allowed for the same pid. if (itr != pid_itr_map.end() && (itr->second != sources.end())) { sources.erase(itr->second); // sdt::list::end() never changes during the lifetime of that // list. itr->second = sources.end(); } } } return true; }, ignore_minimized, only_zero_layer); if (!ret) return false; RTC_DCHECK(windows); windows->reserve(windows->size() + sources.size()); std::copy(std::begin(sources), std::end(sources), std::back_inserter(*windows)); return true; } // Returns true if the window is occupying a full screen. bool IsWindowFullScreen(const MacDesktopConfiguration& desktop_config, CFDictionaryRef window) { bool fullscreen = false; CFDictionaryRef bounds_ref = reinterpret_cast( CFDictionaryGetValue(window, kCGWindowBounds)); CGRect bounds; if (bounds_ref && CGRectMakeWithDictionaryRepresentation(bounds_ref, &bounds)) { for (MacDisplayConfigurations::const_iterator it = desktop_config.displays.begin(); it != desktop_config.displays.end(); it++) { if (it->bounds.equals( DesktopRect::MakeXYWH(bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height))) { fullscreen = true; break; } } } return fullscreen; } bool IsWindowFullScreen(const MacDesktopConfiguration& desktop_config, CGWindowID id) { bool fullscreen = false; GetWindowRef(id, [&](CFDictionaryRef window) { fullscreen = IsWindowFullScreen(desktop_config, window); }); return fullscreen; } bool IsWindowOnScreen(CFDictionaryRef window) { CFBooleanRef on_screen = reinterpret_cast( CFDictionaryGetValue(window, kCGWindowIsOnscreen)); return on_screen != NULL && CFBooleanGetValue(on_screen); } bool IsWindowOnScreen(CGWindowID id) { bool on_screen; if (GetWindowRef(id, [&on_screen](CFDictionaryRef window) { on_screen = IsWindowOnScreen(window); })) { return on_screen; } return false; } std::string GetWindowTitle(CFDictionaryRef window) { CFStringRef title = reinterpret_cast( CFDictionaryGetValue(window, kCGWindowName)); std::string result; if (title && ToUtf8(title, &result)) { return result; } return std::string(); } std::string GetWindowTitle(CGWindowID id) { std::string title; if (GetWindowRef(id, [&title](CFDictionaryRef window) { title = GetWindowTitle(window); })) { return title; } return std::string(); } std::string GetWindowOwnerName(CFDictionaryRef window) { CFStringRef owner_name = reinterpret_cast( CFDictionaryGetValue(window, kCGWindowOwnerName)); std::string result; if (owner_name && ToUtf8(owner_name, &result)) { return result; } return std::string(); } std::string GetWindowOwnerName(CGWindowID id) { std::string owner_name; if (GetWindowRef(id, [&owner_name](CFDictionaryRef window) { owner_name = GetWindowOwnerName(window); })) { return owner_name; } return std::string(); } WindowId GetWindowId(CFDictionaryRef window) { CFNumberRef window_id = reinterpret_cast( CFDictionaryGetValue(window, kCGWindowNumber)); if (!window_id) { return kNullWindowId; } // Note: WindowId is 64-bit on 64-bit system, but CGWindowID is always 32-bit. // CFNumberGetValue() fills only top 32 bits, so we should use CGWindowID to // receive the window id. CGWindowID id; if (!CFNumberGetValue(window_id, kCFNumberIntType, &id)) { return kNullWindowId; } return id; } int GetWindowOwnerPid(CFDictionaryRef window) { CFNumberRef window_pid = reinterpret_cast( CFDictionaryGetValue(window, kCGWindowOwnerPID)); if (!window_pid) { return 0; } int pid; if (!CFNumberGetValue(window_pid, kCFNumberIntType, &pid)) { return 0; } return pid; } int GetWindowOwnerPid(CGWindowID id) { int pid; if (GetWindowRef(id, [&pid](CFDictionaryRef window) { pid = GetWindowOwnerPid(window); })) { return pid; } return 0; } float GetScaleFactorAtPosition(const MacDesktopConfiguration& desktop_config, DesktopVector position) { // Find the dpi to physical pixel scale for the screen where the mouse cursor // is. for (auto it = desktop_config.displays.begin(); it != desktop_config.displays.end(); ++it) { if (it->bounds.Contains(position)) { return it->dip_to_pixel_scale; } } return 1; } float GetWindowScaleFactor(CGWindowID id, DesktopSize size) { DesktopRect window_bounds = GetWindowBounds(id); float scale = 1.0f; if (!window_bounds.is_empty() && !size.is_empty()) { float scale_x = size.width() / window_bounds.width(); float scale_y = size.height() / window_bounds.height(); // Currently the scale in X and Y directions must be same. if ((std::fabs(scale_x - scale_y) <= std::numeric_limits::epsilon() * std::max(scale_x, scale_y)) && scale_x > 0.0f) { scale = scale_x; } } return scale; } DesktopRect GetWindowBounds(CFDictionaryRef window) { CFDictionaryRef window_bounds = reinterpret_cast( CFDictionaryGetValue(window, kCGWindowBounds)); if (!window_bounds) { return DesktopRect(); } CGRect gc_window_rect; if (!CGRectMakeWithDictionaryRepresentation(window_bounds, &gc_window_rect)) { return DesktopRect(); } return DesktopRect::MakeXYWH(gc_window_rect.origin.x, gc_window_rect.origin.y, gc_window_rect.size.width, gc_window_rect.size.height); } DesktopRect GetWindowBounds(CGWindowID id) { DesktopRect result; if (GetWindowRef(id, [&result](CFDictionaryRef window) { result = GetWindowBounds(window); })) { return result; } return DesktopRect(); } } // namespace webrtc