• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 <utility>
12
13#include "modules/desktop_capture/mac/screen_capturer_mac.h"
14
15#include "modules/desktop_capture/mac/desktop_frame_provider.h"
16#include "modules/desktop_capture/mac/window_list_utils.h"
17#include "rtc_base/checks.h"
18#include "rtc_base/constructor_magic.h"
19#include "rtc_base/logging.h"
20#include "rtc_base/time_utils.h"
21#include "rtc_base/trace_event.h"
22#include "sdk/objc/helpers/scoped_cftyperef.h"
23
24namespace webrtc {
25
26namespace {
27
28// Scales all coordinates of a rect by a specified factor.
29DesktopRect ScaleAndRoundCGRect(const CGRect& rect, float scale) {
30  return DesktopRect::MakeLTRB(static_cast<int>(floor(rect.origin.x * scale)),
31                               static_cast<int>(floor(rect.origin.y * scale)),
32                               static_cast<int>(ceil((rect.origin.x + rect.size.width) * scale)),
33                               static_cast<int>(ceil((rect.origin.y + rect.size.height) * scale)));
34}
35
36// Copy pixels in the |rect| from |src_place| to |dest_plane|. |rect| should be
37// relative to the origin of |src_plane| and |dest_plane|.
38void CopyRect(const uint8_t* src_plane,
39              int src_plane_stride,
40              uint8_t* dest_plane,
41              int dest_plane_stride,
42              int bytes_per_pixel,
43              const DesktopRect& rect) {
44  // Get the address of the starting point.
45  const int src_y_offset = src_plane_stride * rect.top();
46  const int dest_y_offset = dest_plane_stride * rect.top();
47  const int x_offset = bytes_per_pixel * rect.left();
48  src_plane += src_y_offset + x_offset;
49  dest_plane += dest_y_offset + x_offset;
50
51  // Copy pixels in the rectangle line by line.
52  const int bytes_per_line = bytes_per_pixel * rect.width();
53  const int height = rect.height();
54  for (int i = 0; i < height; ++i) {
55    memcpy(dest_plane, src_plane, bytes_per_line);
56    src_plane += src_plane_stride;
57    dest_plane += dest_plane_stride;
58  }
59}
60
61// Returns an array of CGWindowID for all the on-screen windows except
62// |window_to_exclude|, or NULL if the window is not found or it fails. The
63// caller should release the returned CFArrayRef.
64CFArrayRef CreateWindowListWithExclusion(CGWindowID window_to_exclude) {
65  if (!window_to_exclude) return nullptr;
66
67  CFArrayRef all_windows =
68      CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
69  if (!all_windows) return nullptr;
70
71  CFMutableArrayRef returned_array =
72      CFArrayCreateMutable(nullptr, CFArrayGetCount(all_windows), nullptr);
73
74  bool found = false;
75  for (CFIndex i = 0; i < CFArrayGetCount(all_windows); ++i) {
76    CFDictionaryRef window =
77        reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(all_windows, i));
78
79    CGWindowID id = GetWindowId(window);
80    if (id == window_to_exclude) {
81      found = true;
82      continue;
83    }
84    CFArrayAppendValue(returned_array, reinterpret_cast<void*>(id));
85  }
86  CFRelease(all_windows);
87
88  if (!found) {
89    CFRelease(returned_array);
90    returned_array = nullptr;
91  }
92  return returned_array;
93}
94
95// Returns the bounds of |window| in physical pixels, enlarged by a small amount
96// on four edges to take account of the border/shadow effects.
97DesktopRect GetExcludedWindowPixelBounds(CGWindowID window, float dip_to_pixel_scale) {
98  // The amount of pixels to add to the actual window bounds to take into
99  // account of the border/shadow effects.
100  static const int kBorderEffectSize = 20;
101  CGRect rect;
102  CGWindowID ids[1];
103  ids[0] = window;
104
105  CFArrayRef window_id_array =
106      CFArrayCreate(nullptr, reinterpret_cast<const void**>(&ids), 1, nullptr);
107  CFArrayRef window_array = CGWindowListCreateDescriptionFromArray(window_id_array);
108
109  if (CFArrayGetCount(window_array) > 0) {
110    CFDictionaryRef window =
111        reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(window_array, 0));
112    CFDictionaryRef bounds_ref =
113        reinterpret_cast<CFDictionaryRef>(CFDictionaryGetValue(window, kCGWindowBounds));
114    CGRectMakeWithDictionaryRepresentation(bounds_ref, &rect);
115  }
116
117  CFRelease(window_id_array);
118  CFRelease(window_array);
119
120  rect.origin.x -= kBorderEffectSize;
121  rect.origin.y -= kBorderEffectSize;
122  rect.size.width += kBorderEffectSize * 2;
123  rect.size.height += kBorderEffectSize * 2;
124  // |rect| is in DIP, so convert to physical pixels.
125  return ScaleAndRoundCGRect(rect, dip_to_pixel_scale);
126}
127
128// Create an image of the given region using the given |window_list|.
129// |pixel_bounds| should be in the primary display's coordinate in physical
130// pixels.
131rtc::ScopedCFTypeRef<CGImageRef> CreateExcludedWindowRegionImage(const DesktopRect& pixel_bounds,
132                                                                 float dip_to_pixel_scale,
133                                                                 CFArrayRef window_list) {
134  CGRect window_bounds;
135  // The origin is in DIP while the size is in physical pixels. That's what
136  // CGWindowListCreateImageFromArray expects.
137  window_bounds.origin.x = pixel_bounds.left() / dip_to_pixel_scale;
138  window_bounds.origin.y = pixel_bounds.top() / dip_to_pixel_scale;
139  window_bounds.size.width = pixel_bounds.width();
140  window_bounds.size.height = pixel_bounds.height();
141
142  return rtc::ScopedCFTypeRef<CGImageRef>(
143      CGWindowListCreateImageFromArray(window_bounds, window_list, kCGWindowImageDefault));
144}
145
146}  // namespace
147
148ScreenCapturerMac::ScreenCapturerMac(
149    rtc::scoped_refptr<DesktopConfigurationMonitor> desktop_config_monitor,
150    bool detect_updated_region,
151    bool allow_iosurface)
152    : detect_updated_region_(detect_updated_region),
153      desktop_config_monitor_(desktop_config_monitor),
154      desktop_frame_provider_(allow_iosurface) {
155  RTC_LOG(LS_INFO) << "Allow IOSurface: " << allow_iosurface;
156  thread_checker_.Detach();
157}
158
159ScreenCapturerMac::~ScreenCapturerMac() {
160  RTC_DCHECK(thread_checker_.IsCurrent());
161  ReleaseBuffers();
162  UnregisterRefreshAndMoveHandlers();
163}
164
165bool ScreenCapturerMac::Init() {
166  TRACE_EVENT0("webrtc", "ScreenCapturerMac::Init");
167  desktop_config_ = desktop_config_monitor_->desktop_configuration();
168  return true;
169}
170
171void ScreenCapturerMac::ReleaseBuffers() {
172  // The buffers might be in use by the encoder, so don't delete them here.
173  // Instead, mark them as "needs update"; next time the buffers are used by
174  // the capturer, they will be recreated if necessary.
175  queue_.Reset();
176}
177
178void ScreenCapturerMac::Start(Callback* callback) {
179  RTC_DCHECK(thread_checker_.IsCurrent());
180  RTC_DCHECK(!callback_);
181  RTC_DCHECK(callback);
182  TRACE_EVENT_INSTANT1(
183      "webrtc", "ScreenCapturermac::Start", "target display id ", current_display_);
184
185  callback_ = callback;
186  // Start and operate CGDisplayStream handler all from capture thread.
187  if (!RegisterRefreshAndMoveHandlers()) {
188    RTC_LOG(LS_ERROR) << "Failed to register refresh and move handlers.";
189    callback_->OnCaptureResult(Result::ERROR_PERMANENT, nullptr);
190    return;
191  }
192  ScreenConfigurationChanged();
193}
194
195void ScreenCapturerMac::CaptureFrame() {
196  RTC_DCHECK(thread_checker_.IsCurrent());
197  TRACE_EVENT0("webrtc", "creenCapturerMac::CaptureFrame");
198  int64_t capture_start_time_nanos = rtc::TimeNanos();
199
200  queue_.MoveToNextFrame();
201  RTC_DCHECK(!queue_.current_frame() || !queue_.current_frame()->IsShared());
202
203  MacDesktopConfiguration new_config = desktop_config_monitor_->desktop_configuration();
204  if (!desktop_config_.Equals(new_config)) {
205    desktop_config_ = new_config;
206    // If the display configuraiton has changed then refresh capturer data
207    // structures. Occasionally, the refresh and move handlers are lost when
208    // the screen mode changes, so re-register them here.
209    UnregisterRefreshAndMoveHandlers();
210    if (!RegisterRefreshAndMoveHandlers()) {
211      RTC_LOG(LS_ERROR) << "Failed to register refresh and move handlers.";
212      callback_->OnCaptureResult(Result::ERROR_PERMANENT, nullptr);
213      return;
214    }
215    ScreenConfigurationChanged();
216  }
217
218  // When screen is zoomed in/out, OSX only updates the part of Rects currently
219  // displayed on screen, with relative location to current top-left on screen.
220  // This will cause problems when we copy the dirty regions to the captured
221  // image. So we invalidate the whole screen to copy all the screen contents.
222  // With CGI method, the zooming will be ignored and the whole screen contents
223  // will be captured as before.
224  // With IOSurface method, the zoomed screen contents will be captured.
225  if (UAZoomEnabled()) {
226    helper_.InvalidateScreen(screen_pixel_bounds_.size());
227  }
228
229  DesktopRegion region;
230  helper_.TakeInvalidRegion(&region);
231
232  // If the current buffer is from an older generation then allocate a new one.
233  // Note that we can't reallocate other buffers at this point, since the caller
234  // may still be reading from them.
235  if (!queue_.current_frame()) queue_.ReplaceCurrentFrame(SharedDesktopFrame::Wrap(CreateFrame()));
236
237  DesktopFrame* current_frame = queue_.current_frame();
238
239  if (!CgBlit(*current_frame, region)) {
240    callback_->OnCaptureResult(Result::ERROR_PERMANENT, nullptr);
241    return;
242  }
243  std::unique_ptr<DesktopFrame> new_frame = queue_.current_frame()->Share();
244  if (detect_updated_region_) {
245    *new_frame->mutable_updated_region() = region;
246  } else {
247    new_frame->mutable_updated_region()->AddRect(DesktopRect::MakeSize(new_frame->size()));
248  }
249
250  if (current_display_) {
251    const MacDisplayConfiguration* config =
252        desktop_config_.FindDisplayConfigurationById(current_display_);
253    if (config) {
254      new_frame->set_top_left(
255          config->bounds.top_left().subtract(desktop_config_.bounds.top_left()));
256    }
257  }
258
259  helper_.set_size_most_recent(new_frame->size());
260
261  new_frame->set_capture_time_ms((rtc::TimeNanos() - capture_start_time_nanos) /
262                                 rtc::kNumNanosecsPerMillisec);
263  callback_->OnCaptureResult(Result::SUCCESS, std::move(new_frame));
264}
265
266void ScreenCapturerMac::SetExcludedWindow(WindowId window) {
267  excluded_window_ = window;
268}
269
270bool ScreenCapturerMac::GetSourceList(SourceList* screens) {
271  RTC_DCHECK(screens->size() == 0);
272
273  for (MacDisplayConfigurations::iterator it = desktop_config_.displays.begin();
274       it != desktop_config_.displays.end();
275       ++it) {
276    screens->push_back({it->id, std::string()});
277  }
278  return true;
279}
280
281bool ScreenCapturerMac::SelectSource(SourceId id) {
282  if (id == kFullDesktopScreenId) {
283    current_display_ = 0;
284  } else {
285    const MacDisplayConfiguration* config =
286        desktop_config_.FindDisplayConfigurationById(static_cast<CGDirectDisplayID>(id));
287    if (!config) return false;
288    current_display_ = config->id;
289  }
290
291  ScreenConfigurationChanged();
292  return true;
293}
294
295bool ScreenCapturerMac::CgBlit(const DesktopFrame& frame, const DesktopRegion& region) {
296  // If not all screen region is dirty, copy the entire contents of the previous capture buffer,
297  // to capture over.
298  if (queue_.previous_frame() && !region.Equals(DesktopRegion(screen_pixel_bounds_))) {
299    memcpy(frame.data(), queue_.previous_frame()->data(), frame.stride() * frame.size().height());
300  }
301
302  MacDisplayConfigurations displays_to_capture;
303  if (current_display_) {
304    // Capturing a single screen. Note that the screen id may change when
305    // screens are added or removed.
306    const MacDisplayConfiguration* config =
307        desktop_config_.FindDisplayConfigurationById(current_display_);
308    if (config) {
309      displays_to_capture.push_back(*config);
310    } else {
311      RTC_LOG(LS_ERROR) << "The selected screen cannot be found for capturing.";
312      return false;
313    }
314  } else {
315    // Capturing the whole desktop.
316    displays_to_capture = desktop_config_.displays;
317  }
318
319  // Create the window list once for all displays.
320  CFArrayRef window_list = CreateWindowListWithExclusion(excluded_window_);
321
322  for (size_t i = 0; i < displays_to_capture.size(); ++i) {
323    const MacDisplayConfiguration& display_config = displays_to_capture[i];
324
325    // Capturing mixed-DPI on one surface is hard, so we only return displays
326    // that match the "primary" display's DPI. The primary display is always
327    // the first in the list.
328    if (i > 0 && display_config.dip_to_pixel_scale != displays_to_capture[0].dip_to_pixel_scale) {
329      continue;
330    }
331    // Determine the display's position relative to the desktop, in pixels.
332    DesktopRect display_bounds = display_config.pixel_bounds;
333    display_bounds.Translate(-screen_pixel_bounds_.left(), -screen_pixel_bounds_.top());
334
335    // Determine which parts of the blit region, if any, lay within the monitor.
336    DesktopRegion copy_region = region;
337    copy_region.IntersectWith(display_bounds);
338    if (copy_region.is_empty()) continue;
339
340    // Translate the region to be copied into display-relative coordinates.
341    copy_region.Translate(-display_bounds.left(), -display_bounds.top());
342
343    DesktopRect excluded_window_bounds;
344    rtc::ScopedCFTypeRef<CGImageRef> excluded_image;
345    if (excluded_window_ && window_list) {
346      // Get the region of the excluded window relative the primary display.
347      excluded_window_bounds =
348          GetExcludedWindowPixelBounds(excluded_window_, display_config.dip_to_pixel_scale);
349      excluded_window_bounds.IntersectWith(display_config.pixel_bounds);
350
351      // Create the image under the excluded window first, because it's faster
352      // than captuing the whole display.
353      if (!excluded_window_bounds.is_empty()) {
354        excluded_image = CreateExcludedWindowRegionImage(
355            excluded_window_bounds, display_config.dip_to_pixel_scale, window_list);
356      }
357    }
358
359    std::unique_ptr<DesktopFrame> frame_source =
360        desktop_frame_provider_.TakeLatestFrameForDisplay(display_config.id);
361    if (!frame_source) {
362      continue;
363    }
364
365    const uint8_t* display_base_address = frame_source->data();
366    int src_bytes_per_row = frame_source->stride();
367    RTC_DCHECK(display_base_address);
368
369    // |frame_source| size may be different from display_bounds in case the screen was
370    // resized recently.
371    copy_region.IntersectWith(frame_source->rect());
372
373    // Copy the dirty region from the display buffer into our desktop buffer.
374    uint8_t* out_ptr = frame.GetFrameDataAtPos(display_bounds.top_left());
375    for (DesktopRegion::Iterator i(copy_region); !i.IsAtEnd(); i.Advance()) {
376      CopyRect(display_base_address,
377               src_bytes_per_row,
378               out_ptr,
379               frame.stride(),
380               DesktopFrame::kBytesPerPixel,
381               i.rect());
382    }
383
384    if (excluded_image) {
385      CGDataProviderRef provider = CGImageGetDataProvider(excluded_image.get());
386      rtc::ScopedCFTypeRef<CFDataRef> excluded_image_data(CGDataProviderCopyData(provider));
387      RTC_DCHECK(excluded_image_data);
388      display_base_address = CFDataGetBytePtr(excluded_image_data.get());
389      src_bytes_per_row = CGImageGetBytesPerRow(excluded_image.get());
390
391      // Translate the bounds relative to the desktop, because |frame| data
392      // starts from the desktop top-left corner.
393      DesktopRect window_bounds_relative_to_desktop(excluded_window_bounds);
394      window_bounds_relative_to_desktop.Translate(-screen_pixel_bounds_.left(),
395                                                  -screen_pixel_bounds_.top());
396
397      DesktopRect rect_to_copy = DesktopRect::MakeSize(excluded_window_bounds.size());
398      rect_to_copy.IntersectWith(DesktopRect::MakeWH(CGImageGetWidth(excluded_image.get()),
399                                                     CGImageGetHeight(excluded_image.get())));
400
401      if (CGImageGetBitsPerPixel(excluded_image.get()) / 8 == DesktopFrame::kBytesPerPixel) {
402        CopyRect(display_base_address,
403                 src_bytes_per_row,
404                 frame.GetFrameDataAtPos(window_bounds_relative_to_desktop.top_left()),
405                 frame.stride(),
406                 DesktopFrame::kBytesPerPixel,
407                 rect_to_copy);
408      }
409    }
410  }
411  if (window_list) CFRelease(window_list);
412  return true;
413}
414
415void ScreenCapturerMac::ScreenConfigurationChanged() {
416  if (current_display_) {
417    const MacDisplayConfiguration* config =
418        desktop_config_.FindDisplayConfigurationById(current_display_);
419    screen_pixel_bounds_ = config ? config->pixel_bounds : DesktopRect();
420    dip_to_pixel_scale_ = config ? config->dip_to_pixel_scale : 1.0f;
421  } else {
422    screen_pixel_bounds_ = desktop_config_.pixel_bounds;
423    dip_to_pixel_scale_ = desktop_config_.dip_to_pixel_scale;
424  }
425
426  // Release existing buffers, which will be of the wrong size.
427  ReleaseBuffers();
428
429  // Clear the dirty region, in case the display is down-sizing.
430  helper_.ClearInvalidRegion();
431
432  // Re-mark the entire desktop as dirty.
433  helper_.InvalidateScreen(screen_pixel_bounds_.size());
434
435  // Make sure the frame buffers will be reallocated.
436  queue_.Reset();
437}
438
439bool ScreenCapturerMac::RegisterRefreshAndMoveHandlers() {
440  RTC_DCHECK(thread_checker_.IsCurrent());
441  desktop_config_ = desktop_config_monitor_->desktop_configuration();
442  for (const auto& config : desktop_config_.displays) {
443    size_t pixel_width = config.pixel_bounds.width();
444    size_t pixel_height = config.pixel_bounds.height();
445    if (pixel_width == 0 || pixel_height == 0) continue;
446    CGDirectDisplayID display_id = config.id;
447    DesktopVector display_origin = config.pixel_bounds.top_left();
448
449    CGDisplayStreamFrameAvailableHandler handler = ^(CGDisplayStreamFrameStatus status,
450                                                     uint64_t display_time,
451                                                     IOSurfaceRef frame_surface,
452                                                     CGDisplayStreamUpdateRef updateRef) {
453      RTC_DCHECK(thread_checker_.IsCurrent());
454      if (status == kCGDisplayStreamFrameStatusStopped) return;
455
456      // Only pay attention to frame updates.
457      if (status != kCGDisplayStreamFrameStatusFrameComplete) return;
458
459      size_t count = 0;
460      const CGRect* rects =
461          CGDisplayStreamUpdateGetRects(updateRef, kCGDisplayStreamUpdateDirtyRects, &count);
462      if (count != 0) {
463        // According to CGDisplayStream.h, it's safe to call
464        // CGDisplayStreamStop() from within the callback.
465        ScreenRefresh(display_id, count, rects, display_origin, frame_surface);
466      }
467    };
468
469    rtc::ScopedCFTypeRef<CFDictionaryRef> properties_dict(
470        CFDictionaryCreate(kCFAllocatorDefault,
471                           (const void* []){kCGDisplayStreamShowCursor},
472                           (const void* []){kCFBooleanFalse},
473                           1,
474                           &kCFTypeDictionaryKeyCallBacks,
475                           &kCFTypeDictionaryValueCallBacks));
476
477    CGDisplayStreamRef display_stream = CGDisplayStreamCreate(
478        display_id, pixel_width, pixel_height, 'BGRA', properties_dict.get(), handler);
479
480    if (display_stream) {
481      CGError error = CGDisplayStreamStart(display_stream);
482      if (error != kCGErrorSuccess) return false;
483
484      CFRunLoopSourceRef source = CGDisplayStreamGetRunLoopSource(display_stream);
485      CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
486      display_streams_.push_back(display_stream);
487    }
488  }
489
490  return true;
491}
492
493void ScreenCapturerMac::UnregisterRefreshAndMoveHandlers() {
494  RTC_DCHECK(thread_checker_.IsCurrent());
495
496  for (CGDisplayStreamRef stream : display_streams_) {
497    CFRunLoopSourceRef source = CGDisplayStreamGetRunLoopSource(stream);
498    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
499    CGDisplayStreamStop(stream);
500    CFRelease(stream);
501  }
502  display_streams_.clear();
503
504  // Release obsolete io surfaces.
505  desktop_frame_provider_.Release();
506}
507
508void ScreenCapturerMac::ScreenRefresh(CGDirectDisplayID display_id,
509                                      CGRectCount count,
510                                      const CGRect* rect_array,
511                                      DesktopVector display_origin,
512                                      IOSurfaceRef io_surface) {
513  if (screen_pixel_bounds_.is_empty()) ScreenConfigurationChanged();
514
515  // The refresh rects are in display coordinates. We want to translate to
516  // framebuffer coordinates. If a specific display is being captured, then no
517  // change is necessary. If all displays are being captured, then we want to
518  // translate by the origin of the display.
519  DesktopVector translate_vector;
520  if (!current_display_) translate_vector = display_origin;
521
522  DesktopRegion region;
523  for (CGRectCount i = 0; i < count; ++i) {
524    // All rects are already in physical pixel coordinates.
525    DesktopRect rect = DesktopRect::MakeXYWH(rect_array[i].origin.x,
526                                             rect_array[i].origin.y,
527                                             rect_array[i].size.width,
528                                             rect_array[i].size.height);
529
530    rect.Translate(translate_vector);
531
532    region.AddRect(rect);
533  }
534  // Always having the latest iosurface before invalidating a region.
535  // See https://bugs.chromium.org/p/webrtc/issues/detail?id=8652 for details.
536  desktop_frame_provider_.InvalidateIOSurface(
537      display_id, rtc::ScopedCFTypeRef<IOSurfaceRef>(io_surface, rtc::RetainPolicy::RETAIN));
538  helper_.InvalidateRegion(region);
539}
540
541std::unique_ptr<DesktopFrame> ScreenCapturerMac::CreateFrame() {
542  std::unique_ptr<DesktopFrame> frame(new BasicDesktopFrame(screen_pixel_bounds_.size()));
543  frame->set_dpi(
544      DesktopVector(kStandardDPI * dip_to_pixel_scale_, kStandardDPI * dip_to_pixel_scale_));
545  return frame;
546}
547
548}  // namespace webrtc
549