xref: /aosp_15_r20/external/webrtc/modules/desktop_capture/mac/window_list_utils.cc (revision d9f758449e529ab9291ac668be2861e7a55c2422)
1 /*
2  *  Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3  *
4  *  Use of this source code is governed by a BSD-style license
5  *  that can be found in the LICENSE file in the root of the source
6  *  tree. An additional intellectual property rights grant can be found
7  *  in the file PATENTS.  All contributing project authors may
8  *  be found in the AUTHORS file in the root of the source tree.
9  */
10 
11 #include "modules/desktop_capture/mac/window_list_utils.h"
12 
13 #include <ApplicationServices/ApplicationServices.h>
14 
15 #include <algorithm>
16 #include <cmath>
17 #include <iterator>
18 #include <limits>
19 #include <list>
20 #include <map>
21 #include <memory>
22 #include <utility>
23 
24 #include "rtc_base/checks.h"
25 
26 static_assert(static_cast<webrtc::WindowId>(kCGNullWindowID) ==
27                   webrtc::kNullWindowId,
28               "kNullWindowId needs to equal to kCGNullWindowID.");
29 
30 namespace webrtc {
31 
32 namespace {
33 
34 // WindowName of the status indicator dot shown since Monterey in the taskbar.
35 // Testing on 12.2.1 shows this is independent of system language setting.
36 const CFStringRef kStatusIndicator = CFSTR("StatusIndicator");
37 const CFStringRef kStatusIndicatorOwnerName = CFSTR("Window Server");
38 
ToUtf8(const CFStringRef str16,std::string * str8)39 bool ToUtf8(const CFStringRef str16, std::string* str8) {
40   size_t maxlen = CFStringGetMaximumSizeForEncoding(CFStringGetLength(str16),
41                                                     kCFStringEncodingUTF8) +
42                   1;
43   std::unique_ptr<char[]> buffer(new char[maxlen]);
44   if (!buffer ||
45       !CFStringGetCString(str16, buffer.get(), maxlen, kCFStringEncodingUTF8)) {
46     return false;
47   }
48   str8->assign(buffer.get());
49   return true;
50 }
51 
52 // Get CFDictionaryRef from `id` and call `on_window` against it. This function
53 // returns false if native APIs fail, typically it indicates that the `id` does
54 // not represent a window. `on_window` will not be called if false is returned
55 // from this function.
GetWindowRef(CGWindowID id,rtc::FunctionView<void (CFDictionaryRef)> on_window)56 bool GetWindowRef(CGWindowID id,
57                   rtc::FunctionView<void(CFDictionaryRef)> on_window) {
58   RTC_DCHECK(on_window);
59 
60   // TODO(zijiehe): `id` is a 32-bit integer, casting it to an array seems not
61   // safe enough. Maybe we should create a new
62   // const void* arr[] = {
63   //   reinterpret_cast<void*>(id) }
64   // };
65   CFArrayRef window_id_array =
66       CFArrayCreate(NULL, reinterpret_cast<const void**>(&id), 1, NULL);
67   CFArrayRef window_array =
68       CGWindowListCreateDescriptionFromArray(window_id_array);
69 
70   bool result = false;
71   // TODO(zijiehe): CFArrayGetCount(window_array) should always return 1.
72   // Otherwise, we should treat it as failure.
73   if (window_array && CFArrayGetCount(window_array)) {
74     on_window(reinterpret_cast<CFDictionaryRef>(
75         CFArrayGetValueAtIndex(window_array, 0)));
76     result = true;
77   }
78 
79   if (window_array) {
80     CFRelease(window_array);
81   }
82   CFRelease(window_id_array);
83   return result;
84 }
85 
86 }  // namespace
87 
GetWindowList(rtc::FunctionView<bool (CFDictionaryRef)> on_window,bool ignore_minimized,bool only_zero_layer)88 bool GetWindowList(rtc::FunctionView<bool(CFDictionaryRef)> on_window,
89                    bool ignore_minimized,
90                    bool only_zero_layer) {
91   RTC_DCHECK(on_window);
92 
93   // Only get on screen, non-desktop windows.
94   // According to
95   // https://developer.apple.com/documentation/coregraphics/cgwindowlistoption/1454105-optiononscreenonly
96   // , when kCGWindowListOptionOnScreenOnly is used, the order of windows are in
97   // decreasing z-order.
98   CFArrayRef window_array = CGWindowListCopyWindowInfo(
99       kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
100       kCGNullWindowID);
101   if (!window_array)
102     return false;
103 
104   MacDesktopConfiguration desktop_config = MacDesktopConfiguration::GetCurrent(
105       MacDesktopConfiguration::TopLeftOrigin);
106 
107   // Check windows to make sure they have an id, title, and use window layer
108   // other than 0.
109   CFIndex count = CFArrayGetCount(window_array);
110   for (CFIndex i = 0; i < count; i++) {
111     CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(
112         CFArrayGetValueAtIndex(window_array, i));
113     if (!window) {
114       continue;
115     }
116 
117     CFNumberRef window_id = reinterpret_cast<CFNumberRef>(
118         CFDictionaryGetValue(window, kCGWindowNumber));
119     if (!window_id) {
120       continue;
121     }
122 
123     CFNumberRef window_layer = reinterpret_cast<CFNumberRef>(
124         CFDictionaryGetValue(window, kCGWindowLayer));
125     if (!window_layer) {
126       continue;
127     }
128 
129     // Skip windows with layer!=0 (menu, dock).
130     int layer;
131     if (!CFNumberGetValue(window_layer, kCFNumberIntType, &layer)) {
132       continue;
133     }
134     if (only_zero_layer && layer != 0) {
135       continue;
136     }
137 
138     // Skip windows that are minimized and not full screen.
139     if (ignore_minimized && !IsWindowOnScreen(window) &&
140         !IsWindowFullScreen(desktop_config, window)) {
141       continue;
142     }
143 
144     // If window title is empty, only consider it if it is either on screen or
145     // fullscreen.
146     CFStringRef window_title = reinterpret_cast<CFStringRef>(
147         CFDictionaryGetValue(window, kCGWindowName));
148     if (!window_title && !IsWindowOnScreen(window) &&
149         !IsWindowFullScreen(desktop_config, window)) {
150       continue;
151     }
152 
153     CFStringRef window_owner_name = reinterpret_cast<CFStringRef>(
154         CFDictionaryGetValue(window, kCGWindowOwnerName));
155     // Ignore the red dot status indicator shown in the stats bar. Unlike the
156     // rest of the system UI it has a window_layer of 0, so was otherwise
157     // included. See crbug.com/1297731.
158     if (window_title && CFEqual(window_title, kStatusIndicator) &&
159         window_owner_name &&
160         CFEqual(window_owner_name, kStatusIndicatorOwnerName)) {
161       continue;
162     }
163 
164     if (!on_window(window)) {
165       break;
166     }
167   }
168 
169   CFRelease(window_array);
170   return true;
171 }
172 
GetWindowList(DesktopCapturer::SourceList * windows,bool ignore_minimized,bool only_zero_layer)173 bool GetWindowList(DesktopCapturer::SourceList* windows,
174                    bool ignore_minimized,
175                    bool only_zero_layer) {
176   // Use a std::list so that iterators are preversed upon insertion and
177   // deletion.
178   std::list<DesktopCapturer::Source> sources;
179   std::map<int, std::list<DesktopCapturer::Source>::const_iterator> pid_itr_map;
180   const bool ret = GetWindowList(
181       [&sources, &pid_itr_map](CFDictionaryRef window) {
182         WindowId window_id = GetWindowId(window);
183         if (window_id != kNullWindowId) {
184           const std::string title = GetWindowTitle(window);
185           const int pid = GetWindowOwnerPid(window);
186           // Check if window for the same pid have been already inserted.
187           std::map<int,
188                    std::list<DesktopCapturer::Source>::const_iterator>::iterator
189               itr = pid_itr_map.find(pid);
190 
191           // Only consider empty titles if the app has no other window with a
192           // proper title.
193           if (title.empty()) {
194             std::string owner_name = GetWindowOwnerName(window);
195 
196             // At this time we do not know if there will be other windows
197             // for the same pid unless they have been already inserted, hence
198             // the check in the map. Also skip the window if owner name is
199             // empty too.
200             if (!owner_name.empty() && (itr == pid_itr_map.end())) {
201               sources.push_back(DesktopCapturer::Source{window_id, owner_name});
202               RTC_DCHECK(!sources.empty());
203               // Get an iterator on the last valid element in the source list.
204               std::list<DesktopCapturer::Source>::const_iterator last_source =
205                   --sources.end();
206               pid_itr_map.insert(
207                   std::pair<int,
208                             std::list<DesktopCapturer::Source>::const_iterator>(
209                       pid, last_source));
210             }
211           } else {
212             sources.push_back(DesktopCapturer::Source{window_id, title});
213             // Once the window with empty title has been removed no other empty
214             // windows are allowed for the same pid.
215             if (itr != pid_itr_map.end() && (itr->second != sources.end())) {
216               sources.erase(itr->second);
217               // sdt::list::end() never changes during the lifetime of that
218               // list.
219               itr->second = sources.end();
220             }
221           }
222         }
223         return true;
224       },
225       ignore_minimized, only_zero_layer);
226 
227   if (!ret)
228     return false;
229 
230   RTC_DCHECK(windows);
231   windows->reserve(windows->size() + sources.size());
232   std::copy(std::begin(sources), std::end(sources),
233             std::back_inserter(*windows));
234 
235   return true;
236 }
237 
238 // Returns true if the window is occupying a full screen.
IsWindowFullScreen(const MacDesktopConfiguration & desktop_config,CFDictionaryRef window)239 bool IsWindowFullScreen(const MacDesktopConfiguration& desktop_config,
240                         CFDictionaryRef window) {
241   bool fullscreen = false;
242   CFDictionaryRef bounds_ref = reinterpret_cast<CFDictionaryRef>(
243       CFDictionaryGetValue(window, kCGWindowBounds));
244 
245   CGRect bounds;
246   if (bounds_ref &&
247       CGRectMakeWithDictionaryRepresentation(bounds_ref, &bounds)) {
248     for (MacDisplayConfigurations::const_iterator it =
249              desktop_config.displays.begin();
250          it != desktop_config.displays.end(); it++) {
251       if (it->bounds.equals(
252               DesktopRect::MakeXYWH(bounds.origin.x, bounds.origin.y,
253                                     bounds.size.width, bounds.size.height))) {
254         fullscreen = true;
255         break;
256       }
257     }
258   }
259 
260   return fullscreen;
261 }
262 
IsWindowFullScreen(const MacDesktopConfiguration & desktop_config,CGWindowID id)263 bool IsWindowFullScreen(const MacDesktopConfiguration& desktop_config,
264                         CGWindowID id) {
265   bool fullscreen = false;
266   GetWindowRef(id, [&](CFDictionaryRef window) {
267     fullscreen = IsWindowFullScreen(desktop_config, window);
268   });
269   return fullscreen;
270 }
271 
IsWindowOnScreen(CFDictionaryRef window)272 bool IsWindowOnScreen(CFDictionaryRef window) {
273   CFBooleanRef on_screen = reinterpret_cast<CFBooleanRef>(
274       CFDictionaryGetValue(window, kCGWindowIsOnscreen));
275   return on_screen != NULL && CFBooleanGetValue(on_screen);
276 }
277 
IsWindowOnScreen(CGWindowID id)278 bool IsWindowOnScreen(CGWindowID id) {
279   bool on_screen;
280   if (GetWindowRef(id, [&on_screen](CFDictionaryRef window) {
281         on_screen = IsWindowOnScreen(window);
282       })) {
283     return on_screen;
284   }
285   return false;
286 }
287 
GetWindowTitle(CFDictionaryRef window)288 std::string GetWindowTitle(CFDictionaryRef window) {
289   CFStringRef title = reinterpret_cast<CFStringRef>(
290       CFDictionaryGetValue(window, kCGWindowName));
291   std::string result;
292   if (title && ToUtf8(title, &result)) {
293     return result;
294   }
295 
296   return std::string();
297 }
298 
GetWindowTitle(CGWindowID id)299 std::string GetWindowTitle(CGWindowID id) {
300   std::string title;
301   if (GetWindowRef(id, [&title](CFDictionaryRef window) {
302         title = GetWindowTitle(window);
303       })) {
304     return title;
305   }
306   return std::string();
307 }
308 
GetWindowOwnerName(CFDictionaryRef window)309 std::string GetWindowOwnerName(CFDictionaryRef window) {
310   CFStringRef owner_name = reinterpret_cast<CFStringRef>(
311       CFDictionaryGetValue(window, kCGWindowOwnerName));
312   std::string result;
313   if (owner_name && ToUtf8(owner_name, &result)) {
314     return result;
315   }
316   return std::string();
317 }
318 
GetWindowOwnerName(CGWindowID id)319 std::string GetWindowOwnerName(CGWindowID id) {
320   std::string owner_name;
321   if (GetWindowRef(id, [&owner_name](CFDictionaryRef window) {
322         owner_name = GetWindowOwnerName(window);
323       })) {
324     return owner_name;
325   }
326   return std::string();
327 }
328 
GetWindowId(CFDictionaryRef window)329 WindowId GetWindowId(CFDictionaryRef window) {
330   CFNumberRef window_id = reinterpret_cast<CFNumberRef>(
331       CFDictionaryGetValue(window, kCGWindowNumber));
332   if (!window_id) {
333     return kNullWindowId;
334   }
335 
336   // Note: WindowId is 64-bit on 64-bit system, but CGWindowID is always 32-bit.
337   // CFNumberGetValue() fills only top 32 bits, so we should use CGWindowID to
338   // receive the window id.
339   CGWindowID id;
340   if (!CFNumberGetValue(window_id, kCFNumberIntType, &id)) {
341     return kNullWindowId;
342   }
343 
344   return id;
345 }
346 
GetWindowOwnerPid(CFDictionaryRef window)347 int GetWindowOwnerPid(CFDictionaryRef window) {
348   CFNumberRef window_pid = reinterpret_cast<CFNumberRef>(
349       CFDictionaryGetValue(window, kCGWindowOwnerPID));
350   if (!window_pid) {
351     return 0;
352   }
353 
354   int pid;
355   if (!CFNumberGetValue(window_pid, kCFNumberIntType, &pid)) {
356     return 0;
357   }
358 
359   return pid;
360 }
361 
GetWindowOwnerPid(CGWindowID id)362 int GetWindowOwnerPid(CGWindowID id) {
363   int pid;
364   if (GetWindowRef(id, [&pid](CFDictionaryRef window) {
365         pid = GetWindowOwnerPid(window);
366       })) {
367     return pid;
368   }
369   return 0;
370 }
371 
GetScaleFactorAtPosition(const MacDesktopConfiguration & desktop_config,DesktopVector position)372 float GetScaleFactorAtPosition(const MacDesktopConfiguration& desktop_config,
373                                DesktopVector position) {
374   // Find the dpi to physical pixel scale for the screen where the mouse cursor
375   // is.
376   for (auto it = desktop_config.displays.begin();
377        it != desktop_config.displays.end(); ++it) {
378     if (it->bounds.Contains(position)) {
379       return it->dip_to_pixel_scale;
380     }
381   }
382   return 1;
383 }
384 
GetWindowScaleFactor(CGWindowID id,DesktopSize size)385 float GetWindowScaleFactor(CGWindowID id, DesktopSize size) {
386   DesktopRect window_bounds = GetWindowBounds(id);
387   float scale = 1.0f;
388 
389   if (!window_bounds.is_empty() && !size.is_empty()) {
390     float scale_x = size.width() / window_bounds.width();
391     float scale_y = size.height() / window_bounds.height();
392     // Currently the scale in X and Y directions must be same.
393     if ((std::fabs(scale_x - scale_y) <=
394          std::numeric_limits<float>::epsilon() * std::max(scale_x, scale_y)) &&
395         scale_x > 0.0f) {
396       scale = scale_x;
397     }
398   }
399 
400   return scale;
401 }
402 
GetWindowBounds(CFDictionaryRef window)403 DesktopRect GetWindowBounds(CFDictionaryRef window) {
404   CFDictionaryRef window_bounds = reinterpret_cast<CFDictionaryRef>(
405       CFDictionaryGetValue(window, kCGWindowBounds));
406   if (!window_bounds) {
407     return DesktopRect();
408   }
409 
410   CGRect gc_window_rect;
411   if (!CGRectMakeWithDictionaryRepresentation(window_bounds, &gc_window_rect)) {
412     return DesktopRect();
413   }
414 
415   return DesktopRect::MakeXYWH(gc_window_rect.origin.x, gc_window_rect.origin.y,
416                                gc_window_rect.size.width,
417                                gc_window_rect.size.height);
418 }
419 
GetWindowBounds(CGWindowID id)420 DesktopRect GetWindowBounds(CGWindowID id) {
421   DesktopRect result;
422   if (GetWindowRef(id, [&result](CFDictionaryRef window) {
423         result = GetWindowBounds(window);
424       })) {
425     return result;
426   }
427   return DesktopRect();
428 }
429 
430 }  // namespace webrtc
431