screen_capturer_sck.mm (32588B)
1 /* 2 * Copyright (c) 2024 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/screen_capturer_sck.h" 12 13 #import <ScreenCaptureKit/ScreenCaptureKit.h> 14 15 #include <atomic> 16 17 #include "absl/strings/str_format.h" 18 #include "api/sequence_checker.h" 19 #include "modules/desktop_capture/mac/desktop_frame_iosurface.h" 20 #include "modules/desktop_capture/shared_desktop_frame.h" 21 #include "rtc_base/logging.h" 22 #include "rtc_base/synchronization/mutex.h" 23 #include "rtc_base/thread_annotations.h" 24 #include "rtc_base/time_utils.h" 25 #include "sck_picker_handle.h" 26 #include "sdk/objc/helpers/scoped_cftyperef.h" 27 28 using webrtc::DesktopFrameIOSurface; 29 30 namespace webrtc { 31 class ScreenCapturerSck; 32 } // namespace webrtc 33 34 // The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture 35 // was reported to be broken before macOS 13 - see http://crbug.com/40234870. 36 // Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were 37 // introduced in macOS 14. 38 API_AVAILABLE(macos(14.0)) 39 @interface SckHelper : NSObject <SCStreamDelegate, 40 SCStreamOutput, 41 SCContentSharingPickerObserver> 42 43 - (instancetype)initWithCapturer:(webrtc::ScreenCapturerSck*)capturer; 44 45 - (void)onShareableContentCreated:(SCShareableContent*)content 46 error:(NSError*)error; 47 48 // Called just before the capturer is destroyed. This avoids a dangling pointer, 49 // and prevents any new calls into a deleted capturer. If any method-call on the 50 // capturer is currently running on a different thread, this blocks until it 51 // completes. 52 - (void)releaseCapturer; 53 54 @end 55 56 namespace webrtc { 57 58 class API_AVAILABLE(macos(14.0)) ScreenCapturerSck final 59 : public DesktopCapturer { 60 public: 61 explicit ScreenCapturerSck(const DesktopCaptureOptions& options); 62 ScreenCapturerSck(const DesktopCaptureOptions& options, 63 SCContentSharingPickerMode modes); 64 ScreenCapturerSck(const ScreenCapturerSck&) = delete; 65 ScreenCapturerSck& operator=(const ScreenCapturerSck&) = delete; 66 67 ~ScreenCapturerSck() override; 68 69 // DesktopCapturer interface. All these methods run on the caller's thread. 70 void Start(DesktopCapturer::Callback* callback) override; 71 void SetMaxFrameRate(uint32_t max_frame_rate) override; 72 void CaptureFrame() override; 73 bool GetSourceList(SourceList* sources) override; 74 bool SelectSource(SourceId id) override; 75 // Creates the SckPickerHandle if needed and not already done. 76 void EnsurePickerHandle(); 77 // Prep for implementing DelegatedSourceListController interface, for now used 78 // by Start(). Triggers SCContentSharingPicker. Runs on the caller's thread. 79 void EnsureVisible(); 80 // Helper functions to forward SCContentSharingPickerObserver notifications to 81 // source_list_observer_. 82 void NotifySourceSelection(SCContentFilter* filter, SCStream* stream); 83 void NotifySourceCancelled(SCStream* stream); 84 void NotifySourceError(); 85 86 // Called after a SCStreamDelegate stop notification. 87 void NotifyCaptureStopped(SCStream* stream); 88 89 // Called by SckHelper when shareable content is returned by ScreenCaptureKit. 90 // `content` will be nil if an error occurred. May run on an arbitrary thread. 91 void OnShareableContentCreated(SCShareableContent* content, NSError* error); 92 93 // Start capture with the given filter. Creates or updates stream_ as needed. 94 void StartWithFilter(SCContentFilter* filter) 95 RTC_EXCLUSIVE_LOCKS_REQUIRED(lock_); 96 97 // Called by SckHelper to notify of a newly captured frame. May run on an 98 // arbitrary thread. 99 void OnNewIOSurface(IOSurfaceRef io_surface, NSDictionary* attachment); 100 101 private: 102 // Called when starting the capturer or the configuration has changed (either 103 // from a SelectSource() call, or the screen-resolution has changed). This 104 // tells SCK to fetch new shareable content, and the completion-handler will 105 // either start a new stream, or reconfigure the existing stream. Runs on the 106 // caller's thread. 107 void StartOrReconfigureCapturer(); 108 109 // Calls to the public API must happen on a single thread. 110 webrtc::SequenceChecker api_checker_; 111 112 // Helper object to receive Objective-C callbacks from ScreenCaptureKit and 113 // call into this C++ object. The helper may outlive this C++ instance, if a 114 // completion-handler is passed to ScreenCaptureKit APIs and the C++ object is 115 // deleted before the handler executes. 116 SckHelper* __strong helper_; 117 118 // Callback for returning captured frames, or errors, to the caller. 119 Callback* callback_ RTC_GUARDED_BY(api_checker_) = nullptr; 120 121 // Helper class that tracks the number of capturers needing 122 // SCContentSharingPicker to stay active. 123 std::unique_ptr<SckPickerHandleInterface> picker_handle_ 124 RTC_GUARDED_BY(api_checker_); 125 126 // Flag to track if we have added ourselves as observer to picker_handle_. 127 bool picker_handle_registered_ RTC_GUARDED_BY(api_checker_) = false; 128 129 // Options passed to the constructor. May be accessed on any thread, but the 130 // options are unchanged during the capturer's lifetime. 131 const DesktopCaptureOptions capture_options_; 132 133 // Modes to use iff using the system picker. 134 // See docs on SCContentSharingPickerMode. 135 const SCContentSharingPickerMode picker_modes_; 136 137 // Signals that a permanent error occurred. This may be set on any thread, and 138 // is read by CaptureFrame() which runs on the caller's thread. 139 std::atomic<bool> permanent_error_ = false; 140 141 // Guards some variables that may be accessed on different threads. 142 Mutex lock_; 143 144 // Provides captured desktop frames. 145 SCStream* __strong stream_ RTC_GUARDED_BY(lock_); 146 147 // Current filter on stream_. 148 SCContentFilter* __strong filter_ RTC_GUARDED_BY(lock_); 149 150 // Currently selected display, or 0 if the full desktop is selected. This 151 // capturer does not support full-desktop capture, and will fall back to the 152 // first display. 153 CGDirectDisplayID current_display_ RTC_GUARDED_BY(lock_) = 0; 154 155 // Configured maximum frame rate in frames per second. 156 uint32_t max_frame_rate_ RTC_GUARDED_BY(lock_) = 0; 157 158 // Used by CaptureFrame() to detect if the screen configuration has changed. 159 MacDesktopConfiguration desktop_config_ RTC_GUARDED_BY(api_checker_); 160 161 Mutex latest_frame_lock_ RTC_ACQUIRED_AFTER(lock_); 162 std::unique_ptr<SharedDesktopFrame> latest_frame_ 163 RTC_GUARDED_BY(latest_frame_lock_); 164 165 int32_t latest_frame_dpi_ RTC_GUARDED_BY(latest_frame_lock_) = kStandardDPI; 166 167 // Tracks whether the latest frame contains new data since it was returned to 168 // the caller. This is used to set the DesktopFrame's `updated_region` 169 // property. The flag is cleared after the frame is sent to OnCaptureResult(), 170 // and is set when SCK reports a new frame with non-empty "dirty" rectangles. 171 // TODO: crbug.com/327458809 - Replace this flag with ScreenCapturerHelper to 172 // more accurately track the dirty rectangles from the 173 // SCStreamFrameInfoDirtyRects attachment. 174 bool frame_is_dirty_ RTC_GUARDED_BY(latest_frame_lock_) = true; 175 176 // Tracks whether a reconfigure is needed. 177 bool frame_needs_reconfigure_ RTC_GUARDED_BY(latest_frame_lock_) = false; 178 // If a reconfigure is needed, this will be set to the size in pixels required 179 // to fit the entire source without downscaling. 180 std::optional<CGSize> frame_reconfigure_img_size_ 181 RTC_GUARDED_BY(latest_frame_lock_); 182 }; 183 184 /* Helper class for stringifying SCContentSharingPickerMode. Needed as 185 * SCContentSharingPickerMode is a typedef to NSUInteger which we cannot add a 186 * AbslStringify function for. */ 187 struct StringifiableSCContentSharingPickerMode { 188 const SCContentSharingPickerMode modes_; 189 190 template <typename Sink> 191 friend void AbslStringify(Sink& sink, 192 const StringifiableSCContentSharingPickerMode& m) { 193 auto modes = m.modes_; 194 if (@available(macos 14, *)) { 195 bool empty = true; 196 const std::tuple<SCContentSharingPickerMode, const char*> all_modes[] = { 197 {SCContentSharingPickerModeSingleWindow, "SingleWindow"}, 198 {SCContentSharingPickerModeMultipleWindows, "MultiWindow"}, 199 {SCContentSharingPickerModeSingleApplication, "SingleApp"}, 200 {SCContentSharingPickerModeMultipleApplications, "MultiApp"}, 201 {SCContentSharingPickerModeSingleDisplay, "SingleDisplay"}}; 202 for (const auto& [mode, text] : all_modes) { 203 if (modes & mode) { 204 modes = modes & (~mode); 205 absl::Format(&sink, "%s%s", empty ? "" : "|", text); 206 empty = false; 207 } 208 } 209 if (modes) { 210 absl::Format(&sink, "%sRemaining=%v", empty ? "" : "|", modes); 211 } 212 return; 213 } 214 absl::Format(&sink, "%v", modes); 215 } 216 }; 217 218 ScreenCapturerSck::ScreenCapturerSck(const DesktopCaptureOptions& options, 219 SCContentSharingPickerMode modes) 220 : api_checker_(SequenceChecker::kDetached), 221 capture_options_(options), 222 picker_modes_(modes) { 223 helper_ = [[SckHelper alloc] initWithCapturer:this]; 224 } 225 226 ScreenCapturerSck::ScreenCapturerSck(const DesktopCaptureOptions& options) 227 : ScreenCapturerSck(options, SCContentSharingPickerModeSingleDisplay) {} 228 229 ScreenCapturerSck::~ScreenCapturerSck() { 230 RTC_DCHECK_RUN_ON(&api_checker_); 231 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " destroyed."; 232 [stream_ stopCaptureWithCompletionHandler:nil]; 233 [helper_ releaseCapturer]; 234 } 235 236 void ScreenCapturerSck::Start(DesktopCapturer::Callback* callback) { 237 RTC_DCHECK_RUN_ON(&api_checker_); 238 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << "."; 239 callback_ = callback; 240 desktop_config_ = 241 capture_options_.configuration_monitor()->desktop_configuration(); 242 if (capture_options_.allow_sck_system_picker()) { 243 EnsureVisible(); 244 return; 245 } 246 StartOrReconfigureCapturer(); 247 } 248 249 void ScreenCapturerSck::SetMaxFrameRate(uint32_t max_frame_rate) { 250 RTC_DCHECK_RUN_ON(&api_checker_); 251 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " SetMaxFrameRate(" 252 << max_frame_rate << ")."; 253 bool stream_started = false; 254 { 255 MutexLock lock(&lock_); 256 if (max_frame_rate_ == max_frame_rate) { 257 return; 258 } 259 260 max_frame_rate_ = max_frame_rate; 261 stream_started = stream_; 262 } 263 if (stream_started) { 264 StartOrReconfigureCapturer(); 265 } 266 } 267 268 void ScreenCapturerSck::CaptureFrame() { 269 RTC_DCHECK_RUN_ON(&api_checker_); 270 int64_t capture_start_time_millis = webrtc::TimeMillis(); 271 272 if (permanent_error_) { 273 RTC_LOG(LS_VERBOSE) << "ScreenCapturerSck " << this 274 << " CaptureFrame() -> ERROR_PERMANENT"; 275 callback_->OnCaptureResult(Result::ERROR_PERMANENT, nullptr); 276 return; 277 } 278 279 MacDesktopConfiguration new_config = 280 capture_options_.configuration_monitor()->desktop_configuration(); 281 if (!desktop_config_.Equals(new_config)) { 282 desktop_config_ = new_config; 283 StartOrReconfigureCapturer(); 284 } 285 286 std::unique_ptr<DesktopFrame> frame; 287 bool needs_reconfigure = false; 288 { 289 MutexLock lock(&latest_frame_lock_); 290 if (latest_frame_) { 291 frame = latest_frame_->Share(); 292 if (frame_is_dirty_) { 293 frame->mutable_updated_region()->AddRect( 294 DesktopRect::MakeSize(frame->size())); 295 frame_is_dirty_ = false; 296 } 297 } 298 needs_reconfigure = frame_needs_reconfigure_; 299 frame_needs_reconfigure_ = false; 300 } 301 302 if (needs_reconfigure) { 303 StartOrReconfigureCapturer(); 304 } 305 306 if (frame) { 307 RTC_LOG(LS_VERBOSE) << "ScreenCapturerSck " << this 308 << " CaptureFrame() -> SUCCESS"; 309 frame->set_capture_time_ms(webrtc::TimeSince(capture_start_time_millis)); 310 callback_->OnCaptureResult(Result::SUCCESS, std::move(frame)); 311 } else { 312 RTC_LOG(LS_VERBOSE) << "ScreenCapturerSck " << this 313 << " CaptureFrame() -> ERROR_TEMPORARY"; 314 callback_->OnCaptureResult(Result::ERROR_TEMPORARY, nullptr); 315 } 316 } 317 318 void ScreenCapturerSck::EnsurePickerHandle() { 319 RTC_DCHECK_RUN_ON(&api_checker_); 320 if (!picker_handle_ && capture_options_.allow_sck_system_picker()) { 321 picker_handle_ = CreateSckPickerHandle(); 322 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this 323 << " Created picker handle. allow_sck_system_picker=" 324 << capture_options_.allow_sck_system_picker() 325 << ", source=" 326 << (picker_handle_ ? picker_handle_->Source() : -1) 327 << ", modes=" 328 << StringifiableSCContentSharingPickerMode{ 329 .modes_ = picker_modes_}; 330 } 331 } 332 333 void ScreenCapturerSck::EnsureVisible() { 334 RTC_DCHECK_RUN_ON(&api_checker_); 335 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << "."; 336 EnsurePickerHandle(); 337 if (picker_handle_) { 338 if (!picker_handle_registered_) { 339 picker_handle_registered_ = true; 340 [picker_handle_->GetPicker() addObserver:helper_]; 341 } 342 } else { 343 // We reached the maximum number of streams. 344 RTC_LOG(LS_ERROR) 345 << "ScreenCapturerSck " << this 346 << " EnsureVisible() reached the maximum number of streams."; 347 permanent_error_ = true; 348 return; 349 } 350 SCContentSharingPicker* picker = picker_handle_->GetPicker(); 351 SCStream* stream; 352 { 353 MutexLock lock(&lock_); 354 stream = stream_; 355 stream_ = nil; 356 filter_ = nil; 357 MutexLock lock2(&latest_frame_lock_); 358 frame_needs_reconfigure_ = false; 359 frame_reconfigure_img_size_ = std::nullopt; 360 } 361 [stream removeStreamOutput:helper_ type:SCStreamOutputTypeScreen error:nil]; 362 [stream stopCaptureWithCompletionHandler:nil]; 363 SCContentSharingPickerConfiguration* config = picker.defaultConfiguration; 364 config.allowedPickerModes = picker_modes_; 365 picker.defaultConfiguration = config; 366 SCShareableContentStyle style = SCShareableContentStyleNone; 367 // Pick a sensible style to start out with, based on our current mode. 368 if (@available(macOS 15, *)) { 369 // Stick with None because if we use Display, the picker doesn't let us 370 // pick a window when first opened. Behaves like Window in 14 except doesn't 371 // change window focus. 372 } else { 373 // Default to Display because if using Window the picker automatically hides 374 // our current window to show others. Saves a click compared to None when 375 // picking a display. 376 style = SCShareableContentStyleDisplay; 377 } 378 if (picker_modes_ == SCContentSharingPickerModeSingleDisplay) { 379 style = SCShareableContentStyleDisplay; 380 } else if (picker_modes_ == SCContentSharingPickerModeSingleWindow || 381 picker_modes_ == SCContentSharingPickerModeMultipleWindows) { 382 style = SCShareableContentStyleWindow; 383 } else if (picker_modes_ == SCContentSharingPickerModeSingleApplication || 384 picker_modes_ == SCContentSharingPickerModeMultipleApplications) { 385 style = SCShareableContentStyleApplication; 386 } 387 // This dies silently if maximumStreamCount streams are already running. We 388 // need our own stream count bookkeeping because of this, and to be able to 389 // unset `active`. 390 [picker presentPickerForStream:stream usingContentStyle:style]; 391 } 392 393 void ScreenCapturerSck::NotifySourceSelection(SCContentFilter* filter, 394 SCStream* stream) { 395 MutexLock lock(&lock_); 396 if (stream_ != stream) { 397 // The picker selected a source for another capturer. 398 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ 399 << ". stream_ != stream."; 400 return; 401 } 402 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ 403 << ". Starting."; 404 StartWithFilter(filter); 405 } 406 407 void ScreenCapturerSck::NotifySourceCancelled(SCStream* stream) { 408 MutexLock lock(&lock_); 409 if (stream_ != stream) { 410 // The picker was cancelled for another capturer. 411 return; 412 } 413 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << "."; 414 if (!stream_) { 415 // The initial picker was cancelled. There is no stream to fall back to. 416 permanent_error_ = true; 417 } 418 } 419 420 void ScreenCapturerSck::NotifySourceError() { 421 { 422 MutexLock lock(&lock_); 423 if (stream_) { 424 // The picker failed to start. But fear not, it was not our picker, 425 // we already have a stream! 426 return; 427 } 428 } 429 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << "."; 430 permanent_error_ = true; 431 } 432 433 void ScreenCapturerSck::NotifyCaptureStopped(SCStream* stream) { 434 MutexLock lock(&lock_); 435 if (stream_ != stream) { 436 return; 437 } 438 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << "."; 439 permanent_error_ = true; 440 } 441 442 bool ScreenCapturerSck::GetSourceList(SourceList* sources) { 443 RTC_DCHECK_RUN_ON(&api_checker_); 444 sources->clear(); 445 EnsurePickerHandle(); 446 if (picker_handle_) { 447 sources->push_back({picker_handle_->Source(), 0, std::string()}); 448 } 449 return true; 450 } 451 452 bool ScreenCapturerSck::SelectSource(SourceId id) { 453 if (capture_options_.allow_sck_system_picker()) { 454 return true; 455 } 456 457 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " SelectSource(id=" << id 458 << ")."; 459 bool stream_started = false; 460 { 461 MutexLock lock(&lock_); 462 if (current_display_ == id) { 463 return true; 464 } 465 current_display_ = id; 466 467 if (stream_) { 468 stream_started = true; 469 } 470 } 471 472 // If the capturer was already started, reconfigure it. Otherwise, wait until 473 // Start() gets called. 474 if (stream_started) { 475 StartOrReconfigureCapturer(); 476 } 477 478 return true; 479 } 480 481 void ScreenCapturerSck::OnShareableContentCreated(SCShareableContent* content, 482 NSError* error) { 483 if (!content) { 484 RTC_LOG(LS_ERROR) << "ScreenCapturerSck " << this 485 << " getShareableContent failed with error code " 486 << (error ? error.code : 0) << "."; 487 permanent_error_ = true; 488 return; 489 } 490 491 if (!content.displays.count) { 492 RTC_LOG(LS_ERROR) << "ScreenCapturerSck " << this 493 << " getShareableContent returned no displays."; 494 permanent_error_ = true; 495 return; 496 } 497 498 MutexLock lock(&lock_); 499 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ 500 << ". current_display_=" << current_display_; 501 SCDisplay* captured_display; 502 for (SCDisplay* display in content.displays) { 503 if (current_display_ == display.displayID) { 504 captured_display = display; 505 break; 506 } 507 } 508 if (!captured_display) { 509 if (current_display_ == 510 static_cast<CGDirectDisplayID>(kFullDesktopScreenId)) { 511 RTC_LOG(LS_WARNING) << "ScreenCapturerSck " << this 512 << " Full screen " 513 "capture is not supported, falling back to first " 514 "display."; 515 } else { 516 RTC_LOG(LS_WARNING) << "ScreenCapturerSck " << this << " Display " 517 << current_display_ 518 << " not found, falling back to " 519 "first display."; 520 } 521 captured_display = content.displays.firstObject; 522 } 523 524 SCContentFilter* filter = 525 [[SCContentFilter alloc] initWithDisplay:captured_display 526 excludingWindows:@[]]; 527 StartWithFilter(filter); 528 } 529 530 void ScreenCapturerSck::StartWithFilter(SCContentFilter* __strong filter) { 531 lock_.AssertHeld(); 532 SCStreamConfiguration* config = [[SCStreamConfiguration alloc] init]; 533 config.pixelFormat = kCVPixelFormatType_32BGRA; 534 config.colorSpaceName = kCGColorSpaceSRGB; 535 config.showsCursor = capture_options_.prefer_cursor_embedded(); 536 config.captureResolution = SCCaptureResolutionNominal; 537 config.minimumFrameInterval = max_frame_rate_ > 0 ? 538 CMTimeMake(1, static_cast<int32_t>(max_frame_rate_)) : 539 kCMTimeZero; 540 541 { 542 MutexLock lock(&latest_frame_lock_); 543 latest_frame_dpi_ = filter.pointPixelScale * kStandardDPI; 544 if (filter_ != filter) { 545 frame_reconfigure_img_size_ = std::nullopt; 546 } 547 auto sourceImgRect = frame_reconfigure_img_size_.value_or( 548 CGSizeMake(filter.contentRect.size.width * filter.pointPixelScale, 549 filter.contentRect.size.height * filter.pointPixelScale)); 550 config.width = sourceImgRect.width; 551 config.height = sourceImgRect.height; 552 } 553 554 filter_ = filter; 555 556 if (stream_) { 557 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this 558 << " Updating stream configuration to size=" 559 << config.width << "x" << config.height 560 << " and max_frame_rate=" << max_frame_rate_ << "."; 561 [stream_ updateContentFilter:filter completionHandler:nil]; 562 [stream_ updateConfiguration:config completionHandler:nil]; 563 } else { 564 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " Creating new stream."; 565 stream_ = [[SCStream alloc] initWithFilter:filter 566 configuration:config 567 delegate:helper_]; 568 569 // TODO: crbug.com/327458809 - Choose an appropriate sampleHandlerQueue for 570 // best performance. 571 NSError* add_stream_output_error; 572 bool add_stream_output_result = 573 [stream_ addStreamOutput:helper_ 574 type:SCStreamOutputTypeScreen 575 sampleHandlerQueue:nil 576 error:&add_stream_output_error]; 577 if (!add_stream_output_result) { 578 stream_ = nil; 579 filter_ = nil; 580 RTC_LOG(LS_ERROR) << "ScreenCapturerSck " << this 581 << " addStreamOutput failed."; 582 permanent_error_ = true; 583 return; 584 } 585 586 auto handler = ^(NSError* error) { 587 if (error) { 588 // It should be safe to access `this` here, because the C++ destructor 589 // calls stopCaptureWithCompletionHandler on the stream, which cancels 590 // this handler. 591 permanent_error_ = true; 592 RTC_LOG(LS_ERROR) << "ScreenCapturerSck " << this 593 << " Starting failed with error code " << error.code 594 << "."; 595 } else { 596 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " Capture started."; 597 } 598 }; 599 600 [stream_ startCaptureWithCompletionHandler:handler]; 601 } 602 } 603 604 void ScreenCapturerSck::OnNewIOSurface(IOSurfaceRef io_surface, 605 NSDictionary* attachment) { 606 bool has_frame_to_process = false; 607 if (auto status_nr = (NSNumber*)attachment[SCStreamFrameInfoStatus]) { 608 auto status = (SCFrameStatus)[status_nr integerValue]; 609 has_frame_to_process = 610 status == SCFrameStatusComplete || status == SCFrameStatusStarted; 611 } 612 if (!has_frame_to_process) { 613 return; 614 } 615 616 double scale_factor = 1; 617 if (auto factor = (NSNumber*)attachment[SCStreamFrameInfoScaleFactor]) { 618 scale_factor = [factor floatValue]; 619 } 620 double content_scale = 1; 621 if (auto scale = (NSNumber*)attachment[SCStreamFrameInfoContentScale]) { 622 content_scale = [scale floatValue]; 623 } 624 CGRect content_rect = {}; 625 if (const auto* rect_dict = 626 (__bridge CFDictionaryRef)attachment[SCStreamFrameInfoContentRect]) { 627 if (!CGRectMakeWithDictionaryRepresentation(rect_dict, &content_rect)) { 628 content_rect = CGRect(); 629 } 630 } 631 CGRect bounding_rect = {}; 632 if (const auto* rect_dict = 633 (__bridge CFDictionaryRef)attachment[SCStreamFrameInfoBoundingRect]) { 634 if (!CGRectMakeWithDictionaryRepresentation(rect_dict, &bounding_rect)) { 635 bounding_rect = CGRect(); 636 } 637 } 638 CGRect overlay_rect = {}; 639 if (@available(macOS 14.2, *)) { 640 if (const auto* rect_dict = (__bridge CFDictionaryRef) 641 attachment[SCStreamFrameInfoPresenterOverlayContentRect]) { 642 if (!CGRectMakeWithDictionaryRepresentation(rect_dict, &overlay_rect)) { 643 overlay_rect = CGRect(); 644 } 645 } 646 } 647 const auto* dirty_rects = (NSArray*)attachment[SCStreamFrameInfoDirtyRects]; 648 649 auto img_bounding_rect = CGRectMake(scale_factor * bounding_rect.origin.x, 650 scale_factor * bounding_rect.origin.y, 651 scale_factor * bounding_rect.size.width, 652 scale_factor * bounding_rect.size.height); 653 654 webrtc::ScopedCFTypeRef<IOSurfaceRef> scoped_io_surface( 655 io_surface, webrtc::RetainPolicy::RETAIN); 656 std::unique_ptr<DesktopFrameIOSurface> desktop_frame_io_surface = 657 DesktopFrameIOSurface::Wrap(scoped_io_surface, img_bounding_rect); 658 if (!desktop_frame_io_surface) { 659 RTC_LOG(LS_ERROR) << "Failed to lock IOSurface."; 660 return; 661 } 662 663 const size_t width = IOSurfaceGetWidth(io_surface); 664 const size_t height = IOSurfaceGetHeight(io_surface); 665 666 RTC_LOG(LS_VERBOSE) << "ScreenCapturerSck " << this << " " << __func__ 667 << ". New surface: width=" << width 668 << ", height=" << height << ", content_rect=" 669 << NSStringFromRect(content_rect).UTF8String 670 << ", bounding_rect=" 671 << NSStringFromRect(bounding_rect).UTF8String 672 << ", overlay_rect=(" 673 << NSStringFromRect(overlay_rect).UTF8String 674 << ", scale_factor=" << scale_factor 675 << ", content_scale=" << content_scale 676 << ". Cropping to rect " 677 << NSStringFromRect(img_bounding_rect).UTF8String << "."; 678 679 std::unique_ptr<SharedDesktopFrame> frame = 680 SharedDesktopFrame::Wrap(std::move(desktop_frame_io_surface)); 681 682 bool dirty; 683 { 684 MutexLock lock(&latest_frame_lock_); 685 // Mark the frame as dirty if it has a different size, and ignore any 686 // DirtyRects attachment in this case. This is because SCK does not apply a 687 // correct attachment to the frame in the case where the stream was 688 // reconfigured. 689 dirty = !latest_frame_ || !latest_frame_->size().equals(frame->size()); 690 } 691 692 if (!dirty) { 693 if (!dirty_rects) { 694 // This is never expected to happen - SCK attaches a non-empty dirty-rects 695 // list to every frame, even when nothing has changed. 696 return; 697 } 698 for (NSUInteger i = 0; i < dirty_rects.count; i++) { 699 const auto* rect_ptr = (__bridge CFDictionaryRef)dirty_rects[i]; 700 if (CFGetTypeID(rect_ptr) != CFDictionaryGetTypeID()) { 701 // This is never expected to happen - the dirty-rects attachment should 702 // always be an array of dictionaries. 703 return; 704 } 705 CGRect rect{}; 706 CGRectMakeWithDictionaryRepresentation(rect_ptr, &rect); 707 if (!CGRectIsEmpty(rect)) { 708 dirty = true; 709 break; 710 } 711 } 712 } 713 714 MutexLock lock(&latest_frame_lock_); 715 if (content_scale > 0 && content_scale < 1) { 716 frame_needs_reconfigure_ = true; 717 double scale = 1 / content_scale; 718 frame_reconfigure_img_size_ = 719 CGSizeMake(std::ceil(scale * width), std::ceil(scale * height)); 720 } 721 if (dirty) { 722 frame->set_dpi(DesktopVector(latest_frame_dpi_, latest_frame_dpi_)); 723 frame->set_may_contain_cursor(capture_options_.prefer_cursor_embedded()); 724 725 frame_is_dirty_ = true; 726 std::swap(latest_frame_, frame); 727 } 728 } 729 730 void ScreenCapturerSck::StartOrReconfigureCapturer() { 731 if (capture_options_.allow_sck_system_picker()) { 732 MutexLock lock(&lock_); 733 if (filter_) { 734 StartWithFilter(filter_); 735 } 736 return; 737 } 738 739 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << "."; 740 // The copy is needed to avoid capturing `this` in the Objective-C block. 741 // Accessing `helper_` inside the block is equivalent to `this->helper_` and 742 // would crash (UAF) if `this` is deleted before the block is executed. 743 SckHelper* local_helper = helper_; 744 auto handler = ^(SCShareableContent* content, NSError* error) { 745 [local_helper onShareableContentCreated:content error:error]; 746 }; 747 748 [SCShareableContent getShareableContentWithCompletionHandler:handler]; 749 } 750 751 bool ScreenCapturerSckAvailable() { 752 static bool available = ([] { 753 if (@available(macOS 14.0, *)) { 754 return true; 755 } 756 return false; 757 })(); 758 return available; 759 } 760 761 std::unique_ptr<DesktopCapturer> CreateScreenCapturerSck( 762 const DesktopCaptureOptions& options) { 763 if (@available(macOS 14.0, *)) { 764 return std::make_unique<ScreenCapturerSck>(options); 765 } 766 return nullptr; 767 } 768 769 bool GenericCapturerSckWithPickerAvailable() { 770 bool available = false; 771 if (@available(macOS 14.0, *)) { 772 available = true; 773 } 774 return available; 775 } 776 777 std::unique_ptr<DesktopCapturer> CreateGenericCapturerSck( 778 const DesktopCaptureOptions& options) { 779 if (@available(macOS 14.0, *)) { 780 if (options.allow_sck_system_picker()) { 781 return std::make_unique<ScreenCapturerSck>( 782 options, 783 SCContentSharingPickerModeSingleDisplay | 784 SCContentSharingPickerModeMultipleWindows); 785 } 786 } 787 return nullptr; 788 } 789 790 } // namespace webrtc 791 792 @implementation SckHelper { 793 // This lock is to prevent the capturer being destroyed while an instance 794 // method is still running on another thread. 795 webrtc::Mutex _capturer_lock; 796 webrtc::ScreenCapturerSck* _capturer; 797 } 798 799 - (instancetype)initWithCapturer:(webrtc::ScreenCapturerSck*)capturer { 800 self = [super init]; 801 if (self) { 802 _capturer = capturer; 803 } 804 return self; 805 } 806 807 - (void)onShareableContentCreated:(SCShareableContent*)content 808 error:(NSError*)error { 809 webrtc::MutexLock lock(&_capturer_lock); 810 if (_capturer) { 811 _capturer->OnShareableContentCreated(content, error); 812 } 813 } 814 815 - (void)stream:(SCStream*)stream didStopWithError:(NSError*)error { 816 webrtc::MutexLock lock(&_capturer_lock); 817 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__ 818 << "."; 819 if (_capturer) { 820 _capturer->NotifyCaptureStopped(stream); 821 } 822 } 823 824 - (void)userDidStopStream:(SCStream*)stream NS_SWIFT_NAME(userDidStopStream(_:)) 825 API_AVAILABLE(macos(14.4)) { 826 webrtc::MutexLock lock(&_capturer_lock); 827 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__ 828 << "."; 829 if (_capturer) { 830 _capturer->NotifyCaptureStopped(stream); 831 } 832 } 833 834 - (void)contentSharingPicker:(SCContentSharingPicker*)picker 835 didUpdateWithFilter:(SCContentFilter*)filter 836 forStream:(SCStream*)stream { 837 webrtc::MutexLock lock(&_capturer_lock); 838 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__ 839 << "."; 840 if (_capturer) { 841 _capturer->NotifySourceSelection(filter, stream); 842 } 843 } 844 845 - (void)contentSharingPicker:(SCContentSharingPicker*)picker 846 didCancelForStream:(SCStream*)stream { 847 webrtc::MutexLock lock(&_capturer_lock); 848 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__ 849 << "."; 850 if (_capturer) { 851 _capturer->NotifySourceCancelled(stream); 852 } 853 } 854 855 - (void)contentSharingPickerStartDidFailWithError:(NSError*)error { 856 webrtc::MutexLock lock(&_capturer_lock); 857 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__ 858 << ". error.code=" << error.code; 859 if (_capturer) { 860 _capturer->NotifySourceError(); 861 } 862 } 863 864 - (void)stream:(SCStream*)stream 865 didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer 866 ofType:(SCStreamOutputType)type { 867 CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); 868 if (!pixelBuffer) { 869 return; 870 } 871 872 IOSurfaceRef ioSurface = CVPixelBufferGetIOSurface(pixelBuffer); 873 if (!ioSurface) { 874 return; 875 } 876 877 CFArrayRef attachmentsArray = CMSampleBufferGetSampleAttachmentsArray( 878 sampleBuffer, /*createIfNecessary=*/false); 879 if (!attachmentsArray || CFArrayGetCount(attachmentsArray) <= 0) { 880 RTC_LOG(LS_ERROR) << "Discarding frame with no attachments."; 881 return; 882 } 883 884 CFDictionaryRef attachment = 885 static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(attachmentsArray, 0)); 886 887 webrtc::MutexLock lock(&_capturer_lock); 888 if (_capturer) { 889 _capturer->OnNewIOSurface(ioSurface, (__bridge NSDictionary*)attachment); 890 } 891 } 892 893 - (void)releaseCapturer { 894 webrtc::MutexLock lock(&_capturer_lock); 895 RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__ 896 << "."; 897 _capturer = nullptr; 898 } 899 900 @end