tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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