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