/*
 *  Copyright (c) 2024 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

#include "modules/desktop_capture/mac/screen_capturer_sck.h"

#import <ScreenCaptureKit/ScreenCaptureKit.h>

#include <atomic>

#include "absl/strings/str_format.h"
#include "api/sequence_checker.h"
#include "modules/desktop_capture/mac/desktop_frame_iosurface.h"
#include "modules/desktop_capture/shared_desktop_frame.h"
#include "rtc_base/logging.h"
#include "rtc_base/synchronization/mutex.h"
#include "rtc_base/thread_annotations.h"
#include "rtc_base/time_utils.h"
#include "sck_picker_handle.h"
#include "sdk/objc/helpers/scoped_cftyperef.h"

namespace webrtc {
class ScreenCapturerSck;
}  // namespace webrtc

// The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture
// was reported to be broken before macOS 13 - see http://crbug.com/40234870.
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were
// introduced in macOS 14.
API_AVAILABLE(macos(14.0))
@interface SckHelper : NSObject <SCStreamDelegate,
                                 SCStreamOutput,
                                 SCContentSharingPickerObserver>

- (instancetype)initWithCapturer:(webrtc::ScreenCapturerSck*)capturer;

- (void)onShareableContentCreated:(SCShareableContent*)content
                            error:(NSError*)error;

// Called just before the capturer is destroyed. This avoids a dangling pointer,
// and prevents any new calls into a deleted capturer. If any method-call on the
// capturer is currently running on a different thread, this blocks until it
// completes.
- (void)releaseCapturer;

@end

namespace webrtc {

class API_AVAILABLE(macos(14.0)) ScreenCapturerSck final
    : public DesktopCapturer {
 public:
  explicit ScreenCapturerSck(const DesktopCaptureOptions& options);
  ScreenCapturerSck(const DesktopCaptureOptions& options,
                    SCContentSharingPickerMode modes);
  ScreenCapturerSck(const ScreenCapturerSck&) = delete;
  ScreenCapturerSck& operator=(const ScreenCapturerSck&) = delete;

  ~ScreenCapturerSck() override;

  // DesktopCapturer interface. All these methods run on the caller's thread.
  void Start(DesktopCapturer::Callback* callback) override;
  void SetMaxFrameRate(uint32_t max_frame_rate) override;
  void CaptureFrame() override;
  bool GetSourceList(SourceList* sources) override;
  bool SelectSource(SourceId id) override;
  // Creates the SckPickerHandle if needed and not already done.
  void EnsurePickerHandle();
  // Prep for implementing DelegatedSourceListController interface, for now used
  // by Start(). Triggers SCContentSharingPicker. Runs on the caller's thread.
  void EnsureVisible();
  // Helper functions to forward SCContentSharingPickerObserver notifications to
  // source_list_observer_.
  void NotifySourceSelection(SCContentFilter* filter, SCStream* stream);
  void NotifySourceCancelled(SCStream* stream);
  void NotifySourceError();

  // Called after a SCStreamDelegate stop notification.
  void NotifyCaptureStopped(SCStream* stream);

  // Called by SckHelper when shareable content is returned by ScreenCaptureKit.
  // `content` will be nil if an error occurred. May run on an arbitrary thread.
  void OnShareableContentCreated(SCShareableContent* content, NSError* error);

  // Start capture with the given filter. Creates or updates stream_ as needed.
  void StartWithFilter(SCContentFilter* filter)
      RTC_EXCLUSIVE_LOCKS_REQUIRED(lock_);

  // Called by SckHelper to notify of a newly captured frame. May run on an
  // arbitrary thread.
  void OnNewIOSurface(IOSurfaceRef io_surface, NSDictionary* attachment);

 private:
  // Called when starting the capturer or the configuration has changed (either
  // from a SelectSource() call, or the screen-resolution has changed). This
  // tells SCK to fetch new shareable content, and the completion-handler will
  // either start a new stream, or reconfigure the existing stream. Runs on the
  // caller's thread.
  void StartOrReconfigureCapturer();

  // Calls to the public API must happen on a single thread.
  webrtc::SequenceChecker api_checker_;

  // Helper object to receive Objective-C callbacks from ScreenCaptureKit and
  // call into this C++ object. The helper may outlive this C++ instance, if a
  // completion-handler is passed to ScreenCaptureKit APIs and the C++ object is
  // deleted before the handler executes.
  SckHelper* __strong helper_;

  // Callback for returning captured frames, or errors, to the caller.
  Callback* callback_ RTC_GUARDED_BY(api_checker_) = nullptr;

  // Helper class that tracks the number of capturers needing
  // SCContentSharingPicker to stay active.
  std::unique_ptr<SckPickerHandleInterface> picker_handle_
      RTC_GUARDED_BY(api_checker_);

  // Flag to track if we have added ourselves as observer to picker_handle_.
  bool picker_handle_registered_ RTC_GUARDED_BY(api_checker_) = false;

  // Options passed to the constructor. May be accessed on any thread, but the
  // options are unchanged during the capturer's lifetime.
  const DesktopCaptureOptions capture_options_;

  // Modes to use iff using the system picker.
  // See docs on SCContentSharingPickerMode.
  const SCContentSharingPickerMode picker_modes_;

  // Signals that a permanent error occurred. This may be set on any thread, and
  // is read by CaptureFrame() which runs on the caller's thread.
  std::atomic<bool> permanent_error_ = false;

  // Guards some variables that may be accessed on different threads.
  Mutex lock_;

  // Provides captured desktop frames.
  SCStream* __strong stream_ RTC_GUARDED_BY(lock_);

  // Current filter on stream_.
  SCContentFilter* __strong filter_ RTC_GUARDED_BY(lock_);

  // Currently selected display, or 0 if the full desktop is selected. This
  // capturer does not support full-desktop capture, and will fall back to the
  // first display.
  CGDirectDisplayID current_display_ RTC_GUARDED_BY(lock_) = 0;

  // Configured maximum frame rate in frames per second.
  uint32_t max_frame_rate_ RTC_GUARDED_BY(lock_) = 0;

  // Used by CaptureFrame() to detect if the screen configuration has changed.
  MacDesktopConfiguration desktop_config_ RTC_GUARDED_BY(api_checker_);

  Mutex latest_frame_lock_ RTC_ACQUIRED_AFTER(lock_);
  std::unique_ptr<SharedDesktopFrame> latest_frame_
      RTC_GUARDED_BY(latest_frame_lock_);

  int32_t latest_frame_dpi_ RTC_GUARDED_BY(latest_frame_lock_) = kStandardDPI;

  // Tracks whether the latest frame contains new data since it was returned to
  // the caller. This is used to set the DesktopFrame's `updated_region`
  // property. The flag is cleared after the frame is sent to OnCaptureResult(),
  // and is set when SCK reports a new frame with non-empty "dirty" rectangles.
  // TODO: crbug.com/327458809 - Replace this flag with ScreenCapturerHelper to
  // more accurately track the dirty rectangles from the
  // SCStreamFrameInfoDirtyRects attachment.
  bool frame_is_dirty_ RTC_GUARDED_BY(latest_frame_lock_) = true;

  // Tracks whether a reconfigure is needed.
  bool frame_needs_reconfigure_ RTC_GUARDED_BY(latest_frame_lock_) = false;
  // If a reconfigure is needed, this will be set to the size in pixels required
  // to fit the entire source without downscaling.
  std::optional<CGSize> frame_reconfigure_img_size_
      RTC_GUARDED_BY(latest_frame_lock_);
};

/* Helper class for stringifying SCContentSharingPickerMode. Needed as
 * SCContentSharingPickerMode is a typedef to NSUInteger which we cannot add a
 * AbslStringify function for. */
struct StringifiableSCContentSharingPickerMode {
  const SCContentSharingPickerMode modes_;

  template <typename Sink>
  friend void AbslStringify(Sink& sink,
                            const StringifiableSCContentSharingPickerMode& m) {
    auto modes = m.modes_;
    if (@available(macos 14, *)) {
      bool empty = true;
      const std::tuple<SCContentSharingPickerMode, const char*> all_modes[] = {
          {SCContentSharingPickerModeSingleWindow, "SingleWindow"},
          {SCContentSharingPickerModeMultipleWindows, "MultiWindow"},
          {SCContentSharingPickerModeSingleApplication, "SingleApp"},
          {SCContentSharingPickerModeMultipleApplications, "MultiApp"},
          {SCContentSharingPickerModeSingleDisplay, "SingleDisplay"}};
      for (const auto& [mode, text] : all_modes) {
        if (modes & mode) {
          modes = modes & (~mode);
          absl::Format(&sink, "%s%s", empty ? "" : "|", text);
          empty = false;
        }
      }
      if (modes) {
        absl::Format(&sink, "%sRemaining=%v", empty ? "" : "|", modes);
      }
      return;
    }
    absl::Format(&sink, "%v", modes);
  }
};

ScreenCapturerSck::ScreenCapturerSck(const DesktopCaptureOptions& options,
                                     SCContentSharingPickerMode modes)
    : api_checker_(SequenceChecker::kDetached),
      capture_options_(options),
      picker_modes_(modes) {
  helper_ = [[SckHelper alloc] initWithCapturer:this];
}

ScreenCapturerSck::ScreenCapturerSck(const DesktopCaptureOptions& options)
    : ScreenCapturerSck(options, SCContentSharingPickerModeSingleDisplay) {}

ScreenCapturerSck::~ScreenCapturerSck() {
  RTC_DCHECK_RUN_ON(&api_checker_);
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " destroyed.";
  [stream_ stopCaptureWithCompletionHandler:nil];
  [helper_ releaseCapturer];
}

void ScreenCapturerSck::Start(DesktopCapturer::Callback* callback) {
  RTC_DCHECK_RUN_ON(&api_checker_);
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << ".";
  callback_ = callback;
  desktop_config_ =
      capture_options_.configuration_monitor()->desktop_configuration();
  if (capture_options_.allow_sck_system_picker()) {
    EnsureVisible();
    return;
  }
  StartOrReconfigureCapturer();
}

void ScreenCapturerSck::SetMaxFrameRate(uint32_t max_frame_rate) {
  RTC_DCHECK_RUN_ON(&api_checker_);
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " SetMaxFrameRate("
                   << max_frame_rate << ").";
  bool stream_started = false;
  {
    MutexLock lock(&lock_);
    if (max_frame_rate_ == max_frame_rate) {
      return;
    }

    max_frame_rate_ = max_frame_rate;
    stream_started = stream_;
  }
  if (stream_started) {
    StartOrReconfigureCapturer();
  }
}

void ScreenCapturerSck::CaptureFrame() {
  RTC_DCHECK_RUN_ON(&api_checker_);
  int64_t capture_start_time_millis = webrtc::TimeMillis();

  if (permanent_error_) {
    RTC_LOG(LS_VERBOSE) << "ScreenCapturerSck " << this
                        << " CaptureFrame() -> ERROR_PERMANENT";
    callback_->OnCaptureResult(Result::ERROR_PERMANENT, nullptr);
    return;
  }

  MacDesktopConfiguration new_config =
      capture_options_.configuration_monitor()->desktop_configuration();
  if (!desktop_config_.Equals(new_config)) {
    desktop_config_ = new_config;
    StartOrReconfigureCapturer();
  }

  std::unique_ptr<DesktopFrame> frame;
  bool needs_reconfigure = false;
  {
    MutexLock lock(&latest_frame_lock_);
    if (latest_frame_) {
      frame = latest_frame_->Share();
      if (frame_is_dirty_) {
        frame->mutable_updated_region()->AddRect(
            DesktopRect::MakeSize(frame->size()));
        frame_is_dirty_ = false;
      }
    }
    needs_reconfigure = frame_needs_reconfigure_;
    frame_needs_reconfigure_ = false;
  }

  if (needs_reconfigure) {
    StartOrReconfigureCapturer();
  }

  if (frame) {
    RTC_LOG(LS_VERBOSE) << "ScreenCapturerSck " << this
                        << " CaptureFrame() -> SUCCESS";
    frame->set_capture_time_ms(webrtc::TimeSince(capture_start_time_millis));
    callback_->OnCaptureResult(Result::SUCCESS, std::move(frame));
  } else {
    RTC_LOG(LS_VERBOSE) << "ScreenCapturerSck " << this
                        << " CaptureFrame() -> ERROR_TEMPORARY";
    callback_->OnCaptureResult(Result::ERROR_TEMPORARY, nullptr);
  }
}

void ScreenCapturerSck::EnsurePickerHandle() {
  RTC_DCHECK_RUN_ON(&api_checker_);
  if (!picker_handle_ && capture_options_.allow_sck_system_picker()) {
    picker_handle_ = CreateSckPickerHandle();
    RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this
                     << " Created picker handle. allow_sck_system_picker="
                     << capture_options_.allow_sck_system_picker()
                     << ", source="
                     << (picker_handle_ ? picker_handle_->Source() : -1)
                     << ", modes="
                     << StringifiableSCContentSharingPickerMode{
                            .modes_ = picker_modes_};
  }
}

void ScreenCapturerSck::EnsureVisible() {
  RTC_DCHECK_RUN_ON(&api_checker_);
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << ".";
  EnsurePickerHandle();
  if (picker_handle_) {
    if (!picker_handle_registered_) {
      picker_handle_registered_ = true;
      [picker_handle_->GetPicker() addObserver:helper_];
    }
  } else {
    // We reached the maximum number of streams.
    RTC_LOG(LS_ERROR)
        << "ScreenCapturerSck " << this
        << " EnsureVisible() reached the maximum number of streams.";
    permanent_error_ = true;
    return;
  }
  SCContentSharingPicker* picker = picker_handle_->GetPicker();
  SCStream* stream;
  {
    MutexLock lock(&lock_);
    stream = stream_;
    stream_ = nil;
    filter_ = nil;
    MutexLock lock2(&latest_frame_lock_);
    frame_needs_reconfigure_ = false;
    frame_reconfigure_img_size_ = std::nullopt;
  }
  [stream removeStreamOutput:helper_ type:SCStreamOutputTypeScreen error:nil];
  [stream stopCaptureWithCompletionHandler:nil];
  SCContentSharingPickerConfiguration* config = picker.defaultConfiguration;
  config.allowedPickerModes = picker_modes_;
  picker.defaultConfiguration = config;
  SCShareableContentStyle style = SCShareableContentStyleNone;
  // Pick a sensible style to start out with, based on our current mode.
  if (@available(macOS 15, *)) {
    // Stick with None because if we use Display, the picker doesn't let us
    // pick a window when first opened. Behaves like Window in 14 except doesn't
    // change window focus.
  } else {
    // Default to Display because if using Window the picker automatically hides
    // our current window to show others. Saves a click compared to None when
    // picking a display.
    style = SCShareableContentStyleDisplay;
  }
  if (picker_modes_ == SCContentSharingPickerModeSingleDisplay) {
    style = SCShareableContentStyleDisplay;
  } else if (picker_modes_ == SCContentSharingPickerModeSingleWindow ||
             picker_modes_ == SCContentSharingPickerModeMultipleWindows) {
    style = SCShareableContentStyleWindow;
  } else if (picker_modes_ == SCContentSharingPickerModeSingleApplication ||
             picker_modes_ == SCContentSharingPickerModeMultipleApplications) {
    style = SCShareableContentStyleApplication;
  }
  // This dies silently if maximumStreamCount streams are already running. We
  // need our own stream count bookkeeping because of this, and to be able to
  // unset `active`.
  [picker presentPickerForStream:stream usingContentStyle:style];
}

void ScreenCapturerSck::NotifySourceSelection(SCContentFilter* filter,
                                              SCStream* stream) {
  MutexLock lock(&lock_);
  if (stream_ != stream) {
    // The picker selected a source for another capturer.
    RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__
                     << ". stream_ != stream.";
    return;
  }
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__
                   << ". Starting.";
  StartWithFilter(filter);
}

void ScreenCapturerSck::NotifySourceCancelled(SCStream* stream) {
  MutexLock lock(&lock_);
  if (stream_ != stream) {
    // The picker was cancelled for another capturer.
    return;
  }
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << ".";
  if (!stream_) {
    // The initial picker was cancelled. There is no stream to fall back to.
    permanent_error_ = true;
  }
}

void ScreenCapturerSck::NotifySourceError() {
  {
    MutexLock lock(&lock_);
    if (stream_) {
      // The picker failed to start. But fear not, it was not our picker,
      // we already have a stream!
      return;
    }
  }
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << ".";
  permanent_error_ = true;
}

void ScreenCapturerSck::NotifyCaptureStopped(SCStream* stream) {
  MutexLock lock(&lock_);
  if (stream_ != stream) {
    return;
  }
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << ".";
  permanent_error_ = true;
}

bool ScreenCapturerSck::GetSourceList(SourceList* sources) {
  RTC_DCHECK_RUN_ON(&api_checker_);
  sources->clear();
  EnsurePickerHandle();
  if (picker_handle_) {
    sources->push_back({picker_handle_->Source(), std::string()});
  }
  return true;
}

bool ScreenCapturerSck::SelectSource(SourceId id) {
  if (capture_options_.allow_sck_system_picker()) {
    return true;
  }

  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " SelectSource(id=" << id
                   << ").";
  bool stream_started = false;
  {
    MutexLock lock(&lock_);
    if (current_display_ == id) {
      return true;
    }
    current_display_ = id;

    if (stream_) {
      stream_started = true;
    }
  }

  // If the capturer was already started, reconfigure it. Otherwise, wait until
  // Start() gets called.
  if (stream_started) {
    StartOrReconfigureCapturer();
  }

  return true;
}

void ScreenCapturerSck::OnShareableContentCreated(SCShareableContent* content,
                                                  NSError* error) {
  if (!content) {
    RTC_LOG(LS_ERROR) << "ScreenCapturerSck " << this
                      << " getShareableContent failed with error code "
                      << (error ? error.code : 0) << ".";
    permanent_error_ = true;
    return;
  }

  if (!content.displays.count) {
    RTC_LOG(LS_ERROR) << "ScreenCapturerSck " << this
                      << " getShareableContent returned no displays.";
    permanent_error_ = true;
    return;
  }

  MutexLock lock(&lock_);
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__
                   << ". current_display_=" << current_display_;
  SCDisplay* captured_display;
  for (SCDisplay* display in content.displays) {
    if (current_display_ == display.displayID) {
      captured_display = display;
      break;
    }
  }
  if (!captured_display) {
    if (current_display_ ==
        static_cast<CGDirectDisplayID>(kFullDesktopScreenId)) {
      RTC_LOG(LS_WARNING) << "ScreenCapturerSck " << this
                          << " Full screen "
                             "capture is not supported, falling back to first "
                             "display.";
    } else {
      RTC_LOG(LS_WARNING) << "ScreenCapturerSck " << this << " Display "
                          << current_display_
                          << " not found, falling back to "
                             "first display.";
    }
    captured_display = content.displays.firstObject;
  }

  SCContentFilter* filter =
      [[SCContentFilter alloc] initWithDisplay:captured_display
                              excludingWindows:@[]];
  StartWithFilter(filter);
}

void ScreenCapturerSck::StartWithFilter(SCContentFilter* __strong filter) {
  lock_.AssertHeld();
  SCStreamConfiguration* config = [[SCStreamConfiguration alloc] init];
  config.pixelFormat = kCVPixelFormatType_32BGRA;
  config.colorSpaceName = kCGColorSpaceSRGB;
  config.showsCursor = capture_options_.prefer_cursor_embedded();
  config.captureResolution = SCCaptureResolutionNominal;
  config.minimumFrameInterval = max_frame_rate_ > 0 ?
      CMTimeMake(1, static_cast<int32_t>(max_frame_rate_)) :
      kCMTimeZero;

  {
    MutexLock lock(&latest_frame_lock_);
    latest_frame_dpi_ = filter.pointPixelScale * kStandardDPI;
    if (filter_ != filter) {
      frame_reconfigure_img_size_ = std::nullopt;
    }
    auto sourceImgRect = frame_reconfigure_img_size_.value_or(
        CGSizeMake(filter.contentRect.size.width * filter.pointPixelScale,
                   filter.contentRect.size.height * filter.pointPixelScale));
    config.width = sourceImgRect.width;
    config.height = sourceImgRect.height;
  }

  filter_ = filter;

  if (stream_) {
    RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this
                     << " Updating stream configuration to size="
                     << config.width << "x" << config.height
                     << " and max_frame_rate=" << max_frame_rate_ << ".";
    [stream_ updateContentFilter:filter completionHandler:nil];
    [stream_ updateConfiguration:config completionHandler:nil];
  } else {
    RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " Creating new stream.";
    stream_ = [[SCStream alloc] initWithFilter:filter
                                 configuration:config
                                      delegate:helper_];

    // TODO: crbug.com/327458809 - Choose an appropriate sampleHandlerQueue for
    // best performance.
    NSError* add_stream_output_error;
    bool add_stream_output_result =
        [stream_ addStreamOutput:helper_
                            type:SCStreamOutputTypeScreen
              sampleHandlerQueue:nil
                           error:&add_stream_output_error];
    if (!add_stream_output_result) {
      stream_ = nil;
      filter_ = nil;
      RTC_LOG(LS_ERROR) << "ScreenCapturerSck " << this
                        << " addStreamOutput failed.";
      permanent_error_ = true;
      return;
    }

    auto handler = ^(NSError* error) {
      if (error) {
        // It should be safe to access `this` here, because the C++ destructor
        // calls stopCaptureWithCompletionHandler on the stream, which cancels
        // this handler.
        permanent_error_ = true;
        RTC_LOG(LS_ERROR) << "ScreenCapturerSck " << this
                          << " Starting failed.";
      } else {
        RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " Capture started.";
      }
    };

    [stream_ startCaptureWithCompletionHandler:handler];
  }
}

void ScreenCapturerSck::OnNewIOSurface(IOSurfaceRef io_surface,
                                       NSDictionary* attachment) {
  bool has_frame_to_process = false;
  if (auto status_nr = (NSNumber*)attachment[SCStreamFrameInfoStatus]) {
    auto status = (SCFrameStatus)[status_nr integerValue];
    has_frame_to_process =
        status == SCFrameStatusComplete || status == SCFrameStatusStarted;
  }
  if (!has_frame_to_process) {
    return;
  }

  double scale_factor = 1;
  if (auto factor = (NSNumber*)attachment[SCStreamFrameInfoScaleFactor]) {
    scale_factor = [factor floatValue];
  }
  double content_scale = 1;
  if (auto scale = (NSNumber*)attachment[SCStreamFrameInfoContentScale]) {
    content_scale = [scale floatValue];
  }
  CGRect content_rect = {};
  if (const auto* rect_dict =
          (__bridge CFDictionaryRef)attachment[SCStreamFrameInfoContentRect]) {
    if (!CGRectMakeWithDictionaryRepresentation(rect_dict, &content_rect)) {
      content_rect = CGRect();
    }
  }
  CGRect bounding_rect = {};
  if (const auto* rect_dict =
          (__bridge CFDictionaryRef)attachment[SCStreamFrameInfoBoundingRect]) {
    if (!CGRectMakeWithDictionaryRepresentation(rect_dict, &bounding_rect)) {
      bounding_rect = CGRect();
    }
  }
  CGRect overlay_rect = {};
  if (@available(macOS 14.2, *)) {
    if (const auto* rect_dict = (__bridge CFDictionaryRef)
            attachment[SCStreamFrameInfoPresenterOverlayContentRect]) {
      if (!CGRectMakeWithDictionaryRepresentation(rect_dict, &overlay_rect)) {
        overlay_rect = CGRect();
      }
    }
  }
  const auto* dirty_rects = (NSArray*)attachment[SCStreamFrameInfoDirtyRects];

  auto img_bounding_rect = CGRectMake(scale_factor * bounding_rect.origin.x,
                                      scale_factor * bounding_rect.origin.y,
                                      scale_factor * bounding_rect.size.width,
                                      scale_factor * bounding_rect.size.height);

  webrtc::ScopedCFTypeRef<IOSurfaceRef> scoped_io_surface(
      io_surface, webrtc::RetainPolicy::RETAIN);
  std::unique_ptr<DesktopFrameIOSurface> desktop_frame_io_surface =
      DesktopFrameIOSurface::Wrap(scoped_io_surface, img_bounding_rect);
  if (!desktop_frame_io_surface) {
    RTC_LOG(LS_ERROR) << "Failed to lock IOSurface.";
    return;
  }

  const size_t width = IOSurfaceGetWidth(io_surface);
  const size_t height = IOSurfaceGetHeight(io_surface);

  RTC_LOG(LS_VERBOSE) << "ScreenCapturerSck " << this << " " << __func__
                      << ". New surface: width=" << width
                      << ", height=" << height << ", content_rect="
                      << NSStringFromRect(content_rect).UTF8String
                      << ", bounding_rect="
                      << NSStringFromRect(bounding_rect).UTF8String
                      << ", overlay_rect=("
                      << NSStringFromRect(overlay_rect).UTF8String
                      << ", scale_factor=" << scale_factor
                      << ", content_scale=" << content_scale
                      << ". Cropping to rect "
                      << NSStringFromRect(img_bounding_rect).UTF8String << ".";

  std::unique_ptr<SharedDesktopFrame> frame =
      SharedDesktopFrame::Wrap(std::move(desktop_frame_io_surface));

  bool dirty;
  {
    MutexLock lock(&latest_frame_lock_);
    // Mark the frame as dirty if it has a different size, and ignore any
    // DirtyRects attachment in this case. This is because SCK does not apply a
    // correct attachment to the frame in the case where the stream was
    // reconfigured.
    dirty = !latest_frame_ || !latest_frame_->size().equals(frame->size());
  }

  if (!dirty) {
    if (!dirty_rects) {
      // This is never expected to happen - SCK attaches a non-empty dirty-rects
      // list to every frame, even when nothing has changed.
      return;
    }
    for (NSUInteger i = 0; i < dirty_rects.count; i++) {
      const auto* rect_ptr = (__bridge CFDictionaryRef)dirty_rects[i];
      if (CFGetTypeID(rect_ptr) != CFDictionaryGetTypeID()) {
        // This is never expected to happen - the dirty-rects attachment should
        // always be an array of dictionaries.
        return;
      }
      CGRect rect{};
      CGRectMakeWithDictionaryRepresentation(rect_ptr, &rect);
      if (!CGRectIsEmpty(rect)) {
        dirty = true;
        break;
      }
    }
  }

  MutexLock lock(&latest_frame_lock_);
  if (content_scale > 0 && content_scale < 1) {
    frame_needs_reconfigure_ = true;
    double scale = 1 / content_scale;
    frame_reconfigure_img_size_ =
        CGSizeMake(std::ceil(scale * width), std::ceil(scale * height));
  }
  if (dirty) {
    frame->set_dpi(DesktopVector(latest_frame_dpi_, latest_frame_dpi_));
    frame->set_may_contain_cursor(capture_options_.prefer_cursor_embedded());

    frame_is_dirty_ = true;
    std::swap(latest_frame_, frame);
  }
}

void ScreenCapturerSck::StartOrReconfigureCapturer() {
  if (capture_options_.allow_sck_system_picker()) {
    MutexLock lock(&lock_);
    if (filter_) {
      StartWithFilter(filter_);
    }
    return;
  }

  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << this << " " << __func__ << ".";
  // The copy is needed to avoid capturing `this` in the Objective-C block.
  // Accessing `helper_` inside the block is equivalent to `this->helper_` and
  // would crash (UAF) if `this` is deleted before the block is executed.
  SckHelper* local_helper = helper_;
  auto handler = ^(SCShareableContent* content, NSError* error) {
    [local_helper onShareableContentCreated:content error:error];
  };

  [SCShareableContent getShareableContentWithCompletionHandler:handler];
}

bool ScreenCapturerSckAvailable() {
  static bool available = ([] {
    if (@available(macOS 14.0, *)) {
      return true;
    }
    return false;
  })();
  return available;
}

std::unique_ptr<DesktopCapturer> CreateScreenCapturerSck(
    const DesktopCaptureOptions& options) {
  if (@available(macOS 14.0, *)) {
    return std::make_unique<ScreenCapturerSck>(options);
  }
  return nullptr;
}

bool GenericCapturerSckWithPickerAvailable() {
  bool available = false;
  if (@available(macOS 14.0, *)) {
    available = true;
  }
  return available;
}

std::unique_ptr<DesktopCapturer> CreateGenericCapturerSck(
    const DesktopCaptureOptions& options) {
  if (@available(macOS 14.0, *)) {
    if (options.allow_sck_system_picker()) {
      return std::make_unique<ScreenCapturerSck>(
          options,
          SCContentSharingPickerModeSingleDisplay |
              SCContentSharingPickerModeMultipleWindows);
    }
  }
  return nullptr;
}

}  // namespace webrtc

@implementation SckHelper {
  // This lock is to prevent the capturer being destroyed while an instance
  // method is still running on another thread.
  webrtc::Mutex _capturer_lock;
  webrtc::ScreenCapturerSck* _capturer;
}

- (instancetype)initWithCapturer:(webrtc::ScreenCapturerSck*)capturer {
  self = [super init];
  if (self) {
    _capturer = capturer;
  }
  return self;
}

- (void)onShareableContentCreated:(SCShareableContent*)content
                            error:(NSError*)error {
  webrtc::MutexLock lock(&_capturer_lock);
  if (_capturer) {
    _capturer->OnShareableContentCreated(content, error);
  }
}

- (void)stream:(SCStream*)stream didStopWithError:(NSError*)error {
  webrtc::MutexLock lock(&_capturer_lock);
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__
                   << ".";
  if (_capturer) {
    _capturer->NotifyCaptureStopped(stream);
  }
}

- (void)userDidStopStream:(SCStream*)stream NS_SWIFT_NAME(userDidStopStream(_:))
                              API_AVAILABLE(macos(14.4)) {
  webrtc::MutexLock lock(&_capturer_lock);
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__
                   << ".";
  if (_capturer) {
    _capturer->NotifyCaptureStopped(stream);
  }
}

- (void)contentSharingPicker:(SCContentSharingPicker*)picker
         didUpdateWithFilter:(SCContentFilter*)filter
                   forStream:(SCStream*)stream {
  webrtc::MutexLock lock(&_capturer_lock);
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__
                   << ".";
  if (_capturer) {
    _capturer->NotifySourceSelection(filter, stream);
  }
}

- (void)contentSharingPicker:(SCContentSharingPicker*)picker
          didCancelForStream:(SCStream*)stream {
  webrtc::MutexLock lock(&_capturer_lock);
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__
                   << ".";
  if (_capturer) {
    _capturer->NotifySourceCancelled(stream);
  }
}

- (void)contentSharingPickerStartDidFailWithError:(NSError*)error {
  webrtc::MutexLock lock(&_capturer_lock);
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__
                   << ". error.code=" << error.code;
  if (_capturer) {
    _capturer->NotifySourceError();
  }
}

- (void)stream:(SCStream*)stream
    didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
                   ofType:(SCStreamOutputType)type {
  CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
  if (!pixelBuffer) {
    return;
  }

  IOSurfaceRef ioSurface = CVPixelBufferGetIOSurface(pixelBuffer);
  if (!ioSurface) {
    return;
  }

  CFArrayRef attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(
      sampleBuffer, /*createIfNecessary=*/false);
  if (!attachmentsArray || CFArrayGetCount(attachmentsArray) <= 0) {
    RTC_LOG(LS_ERROR) << "Discarding frame with no attachments.";
    return;
  }

  CFDictionaryRef attachment =
      static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(attachmentsArray, 0));

  webrtc::MutexLock lock(&_capturer_lock);
  if (_capturer) {
    _capturer->OnNewIOSurface(ioSurface, (__bridge NSDictionary*)attachment);
  }
}

- (void)releaseCapturer {
  webrtc::MutexLock lock(&_capturer_lock);
  RTC_LOG(LS_INFO) << "ScreenCapturerSck " << _capturer << " " << __func__
                   << ".";
  _capturer = nullptr;
}

@end
