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