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(®ion); 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