1/*
2 *  Copyright (c) 2013 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 "webrtc/modules/desktop_capture/mouse_cursor_monitor.h"
12
13#include <assert.h>
14#include <ApplicationServices/ApplicationServices.h>
15#include <Cocoa/Cocoa.h>
16#include <CoreFoundation/CoreFoundation.h>
17
18#include "webrtc/base/macutils.h"
19#include "webrtc/base/scoped_ptr.h"
20#include "webrtc/base/scoped_ref_ptr.h"
21#include "webrtc/modules/desktop_capture/desktop_capture_options.h"
22#include "webrtc/modules/desktop_capture/desktop_frame.h"
23#include "webrtc/modules/desktop_capture/mac/desktop_configuration.h"
24#include "webrtc/modules/desktop_capture/mac/desktop_configuration_monitor.h"
25#include "webrtc/modules/desktop_capture/mac/full_screen_chrome_window_detector.h"
26#include "webrtc/modules/desktop_capture/mouse_cursor.h"
27#include "webrtc/system_wrappers/include/logging.h"
28
29namespace webrtc {
30
31class MouseCursorMonitorMac : public MouseCursorMonitor {
32 public:
33  MouseCursorMonitorMac(const DesktopCaptureOptions& options,
34                        CGWindowID window_id,
35                        ScreenId screen_id);
36  virtual ~MouseCursorMonitorMac();
37
38  void Init(Callback* callback, Mode mode) override;
39  void Capture() override;
40
41 private:
42  static void DisplaysReconfiguredCallback(CGDirectDisplayID display,
43                                           CGDisplayChangeSummaryFlags flags,
44                                           void *user_parameter);
45  void DisplaysReconfigured(CGDirectDisplayID display,
46                            CGDisplayChangeSummaryFlags flags);
47
48  void CaptureImage();
49
50  rtc::scoped_refptr<DesktopConfigurationMonitor> configuration_monitor_;
51  CGWindowID window_id_;
52  ScreenId screen_id_;
53  Callback* callback_;
54  Mode mode_;
55  rtc::scoped_ptr<MouseCursor> last_cursor_;
56  rtc::scoped_refptr<FullScreenChromeWindowDetector>
57      full_screen_chrome_window_detector_;
58};
59
60MouseCursorMonitorMac::MouseCursorMonitorMac(
61    const DesktopCaptureOptions& options,
62    CGWindowID window_id,
63    ScreenId screen_id)
64    : configuration_monitor_(options.configuration_monitor()),
65      window_id_(window_id),
66      screen_id_(screen_id),
67      callback_(NULL),
68      mode_(SHAPE_AND_POSITION),
69      full_screen_chrome_window_detector_(
70          options.full_screen_chrome_window_detector()) {
71  assert(window_id == kCGNullWindowID || screen_id == kInvalidScreenId);
72  if (screen_id != kInvalidScreenId &&
73      rtc::GetOSVersionName() < rtc::kMacOSLion) {
74    // Single screen capture is not supported on pre OS X 10.7.
75    screen_id_ = kFullDesktopScreenId;
76  }
77}
78
79MouseCursorMonitorMac::~MouseCursorMonitorMac() {}
80
81void MouseCursorMonitorMac::Init(Callback* callback, Mode mode) {
82  assert(!callback_);
83  assert(callback);
84
85  callback_ = callback;
86  mode_ = mode;
87}
88
89void MouseCursorMonitorMac::Capture() {
90  assert(callback_);
91
92  CaptureImage();
93
94  if (mode_ != SHAPE_AND_POSITION)
95    return;
96
97  CursorState state = INSIDE;
98
99  CGEventRef event = CGEventCreate(NULL);
100  CGPoint gc_position = CGEventGetLocation(event);
101  CFRelease(event);
102
103  DesktopVector position(gc_position.x, gc_position.y);
104
105  configuration_monitor_->Lock();
106  MacDesktopConfiguration configuration =
107      configuration_monitor_->desktop_configuration();
108  configuration_monitor_->Unlock();
109  float scale = 1.0f;
110
111  // Find the dpi to physical pixel scale for the screen where the mouse cursor
112  // is.
113  for (MacDisplayConfigurations::iterator it = configuration.displays.begin();
114      it != configuration.displays.end(); ++it) {
115    if (it->bounds.Contains(position)) {
116      scale = it->dip_to_pixel_scale;
117      break;
118    }
119  }
120  // If we are capturing cursor for a specific window then we need to figure out
121  // if the current mouse position is covered by another window and also adjust
122  // |position| to make it relative to the window origin.
123  if (window_id_ != kCGNullWindowID) {
124    CGWindowID on_screen_window = window_id_;
125    if (full_screen_chrome_window_detector_) {
126      CGWindowID full_screen_window =
127          full_screen_chrome_window_detector_->FindFullScreenWindow(window_id_);
128
129      if (full_screen_window != kCGNullWindowID)
130        on_screen_window = full_screen_window;
131    }
132
133    // Get list of windows that may be covering parts of |on_screen_window|.
134    // CGWindowListCopyWindowInfo() returns windows in order from front to back,
135    // so |on_screen_window| is expected to be the last in the list.
136    CFArrayRef window_array =
137        CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly |
138                                       kCGWindowListOptionOnScreenAboveWindow |
139                                       kCGWindowListOptionIncludingWindow,
140                                   on_screen_window);
141    bool found_window = false;
142    if (window_array) {
143      CFIndex count = CFArrayGetCount(window_array);
144      for (CFIndex i = 0; i < count; ++i) {
145        CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(
146            CFArrayGetValueAtIndex(window_array, i));
147
148        // Skip the Dock window. Dock window covers the whole screen, but it is
149        // transparent.
150        CFStringRef window_name = reinterpret_cast<CFStringRef>(
151            CFDictionaryGetValue(window, kCGWindowName));
152        if (window_name && CFStringCompare(window_name, CFSTR("Dock"), 0) == 0)
153          continue;
154
155        CFDictionaryRef window_bounds = reinterpret_cast<CFDictionaryRef>(
156            CFDictionaryGetValue(window, kCGWindowBounds));
157        CFNumberRef window_number = reinterpret_cast<CFNumberRef>(
158            CFDictionaryGetValue(window, kCGWindowNumber));
159
160        if (window_bounds && window_number) {
161          CGRect gc_window_rect;
162          if (!CGRectMakeWithDictionaryRepresentation(window_bounds,
163                                                      &gc_window_rect)) {
164            continue;
165          }
166          DesktopRect window_rect =
167              DesktopRect::MakeXYWH(gc_window_rect.origin.x,
168                                    gc_window_rect.origin.y,
169                                    gc_window_rect.size.width,
170                                    gc_window_rect.size.height);
171
172          CGWindowID window_id;
173          if (!CFNumberGetValue(window_number, kCFNumberIntType, &window_id))
174            continue;
175
176          if (window_id == on_screen_window) {
177            found_window = true;
178            if (!window_rect.Contains(position))
179              state = OUTSIDE;
180            position = position.subtract(window_rect.top_left());
181
182            assert(i == count - 1);
183            break;
184          } else if (window_rect.Contains(position)) {
185            state = OUTSIDE;
186            position.set(-1, -1);
187            break;
188          }
189        }
190      }
191      CFRelease(window_array);
192    }
193    if (!found_window) {
194      // If we failed to get list of windows or the window wasn't in the list
195      // pretend that the cursor is outside the window. This can happen, e.g. if
196      // the window was closed.
197      state = OUTSIDE;
198      position.set(-1, -1);
199    }
200  } else {
201    assert(screen_id_ >= kFullDesktopScreenId);
202    if (screen_id_ != kFullDesktopScreenId) {
203      // For single screen capturing, convert the position to relative to the
204      // target screen.
205      const MacDisplayConfiguration* config =
206          configuration.FindDisplayConfigurationById(
207              static_cast<CGDirectDisplayID>(screen_id_));
208      if (config) {
209        if (!config->pixel_bounds.Contains(position))
210          state = OUTSIDE;
211        position = position.subtract(config->bounds.top_left());
212      } else {
213        // The target screen is no longer valid.
214        state = OUTSIDE;
215        position.set(-1, -1);
216      }
217    } else {
218      position.subtract(configuration.bounds.top_left());
219    }
220  }
221  if (state == INSIDE) {
222    // Convert Density Independent Pixel to physical pixel.
223    position = DesktopVector(round(position.x() * scale),
224                             round(position.y() * scale));
225  }
226  callback_->OnMouseCursorPosition(state, position);
227}
228
229void MouseCursorMonitorMac::CaptureImage() {
230  NSCursor* nscursor = [NSCursor currentSystemCursor];
231
232  NSImage* nsimage = [nscursor image];
233  NSSize nssize = [nsimage size];
234  DesktopSize size(nssize.width, nssize.height);
235  NSPoint nshotspot = [nscursor hotSpot];
236  DesktopVector hotspot(
237      std::max(0, std::min(size.width(), static_cast<int>(nshotspot.x))),
238      std::max(0, std::min(size.height(), static_cast<int>(nshotspot.y))));
239  CGImageRef cg_image =
240      [nsimage CGImageForProposedRect:NULL context:nil hints:nil];
241  if (!cg_image)
242    return;
243
244  if (CGImageGetBitsPerPixel(cg_image) != DesktopFrame::kBytesPerPixel * 8 ||
245      CGImageGetBytesPerRow(cg_image) !=
246          static_cast<size_t>(DesktopFrame::kBytesPerPixel * size.width()) ||
247      CGImageGetBitsPerComponent(cg_image) != 8) {
248    return;
249  }
250
251  CGDataProviderRef provider = CGImageGetDataProvider(cg_image);
252  CFDataRef image_data_ref = CGDataProviderCopyData(provider);
253  if (image_data_ref == NULL)
254    return;
255
256  const uint8_t* src_data =
257      reinterpret_cast<const uint8_t*>(CFDataGetBytePtr(image_data_ref));
258
259  // Compare the cursor with the previous one.
260  if (last_cursor_.get() &&
261      last_cursor_->image()->size().equals(size) &&
262      last_cursor_->hotspot().equals(hotspot) &&
263      memcmp(last_cursor_->image()->data(), src_data,
264             last_cursor_->image()->stride() * size.height()) == 0) {
265    CFRelease(image_data_ref);
266    return;
267  }
268
269  // Create a MouseCursor that describes the cursor and pass it to
270  // the client.
271  rtc::scoped_ptr<DesktopFrame> image(
272      new BasicDesktopFrame(DesktopSize(size.width(), size.height())));
273  memcpy(image->data(), src_data,
274         size.width() * size.height() * DesktopFrame::kBytesPerPixel);
275
276  CFRelease(image_data_ref);
277
278  rtc::scoped_ptr<MouseCursor> cursor(
279      new MouseCursor(image.release(), hotspot));
280  last_cursor_.reset(MouseCursor::CopyOf(*cursor));
281
282  callback_->OnMouseCursor(cursor.release());
283}
284
285MouseCursorMonitor* MouseCursorMonitor::CreateForWindow(
286    const DesktopCaptureOptions& options, WindowId window) {
287  return new MouseCursorMonitorMac(options, window, kInvalidScreenId);
288}
289
290MouseCursorMonitor* MouseCursorMonitor::CreateForScreen(
291    const DesktopCaptureOptions& options,
292    ScreenId screen) {
293  return new MouseCursorMonitorMac(options, kCGNullWindowID, screen);
294}
295
296}  // namespace webrtc
297