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