| /* |
| * 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 <ApplicationServices/ApplicationServices.h> |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <iterator> |
| #include <limits> |
| #include <list> |
| #include <map> |
| #include <memory> |
| #include <utility> |
| |
| #include "rtc_base/checks.h" |
| |
| static_assert(static_cast<webrtc::WindowId>(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<char[]> 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<void(CFDictionaryRef)> 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<void*>(id) } |
| // }; |
| CFArrayRef window_id_array = |
| CFArrayCreate(NULL, reinterpret_cast<const void**>(&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<CFDictionaryRef>( |
| CFArrayGetValueAtIndex(window_array, 0))); |
| result = true; |
| } |
| |
| if (window_array) { |
| CFRelease(window_array); |
| } |
| CFRelease(window_id_array); |
| return result; |
| } |
| |
| } // namespace |
| |
| bool GetWindowList(rtc::FunctionView<bool(CFDictionaryRef)> 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<CFDictionaryRef>( |
| CFArrayGetValueAtIndex(window_array, i)); |
| if (!window) { |
| continue; |
| } |
| |
| CFNumberRef window_id = reinterpret_cast<CFNumberRef>( |
| CFDictionaryGetValue(window, kCGWindowNumber)); |
| if (!window_id) { |
| continue; |
| } |
| |
| CFNumberRef window_layer = reinterpret_cast<CFNumberRef>( |
| 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<CFStringRef>( |
| CFDictionaryGetValue(window, kCGWindowName)); |
| if (!window_title && !IsWindowOnScreen(window) && |
| !IsWindowFullScreen(desktop_config, window)) { |
| continue; |
| } |
| |
| CFStringRef window_owner_name = reinterpret_cast<CFStringRef>( |
| 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<DesktopCapturer::Source> sources; |
| std::map<int, std::list<DesktopCapturer::Source>::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<int, |
| std::list<DesktopCapturer::Source>::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, owner_name}); |
| RTC_DCHECK(!sources.empty()); |
| // Get an iterator on the last valid element in the source list. |
| std::list<DesktopCapturer::Source>::const_iterator last_source = |
| --sources.end(); |
| pid_itr_map.insert( |
| std::pair<int, |
| std::list<DesktopCapturer::Source>::const_iterator>( |
| pid, last_source)); |
| } |
| } else { |
| sources.push_back(DesktopCapturer::Source{window_id, 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<CFDictionaryRef>( |
| 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<CFBooleanRef>( |
| 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<CFStringRef>( |
| 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<CFStringRef>( |
| 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<CFNumberRef>( |
| 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<CFNumberRef>( |
| 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<float>::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<CFDictionaryRef>( |
| 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 |