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