// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/media/capture/screen_capture_kit_device_mac.h"
#import <ScreenCaptureKit/ScreenCaptureKit.h>
#include <optional>
#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/task/bind_post_task.h"
#import "base/task/single_thread_task_runner.h"
#include "base/threading/thread_checker.h"
#include "base/timer/timer.h"
#include "content/browser/media/capture/io_surface_capture_device_base_mac.h"
#include "content/browser/media/capture/screen_capture_kit_fullscreen_module.h"
#include "content/public/common/content_features.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h"
#include "ui/gfx/native_widget_types.h"
using SampleCallback = base::RepeatingCallback<void(gfx::ScopedInUseIOSurface,
std::optional<gfx::Size>,
std::optional<gfx::Rect>)>;
using ErrorCallback = base::RepeatingClosure;
API_AVAILABLE(macos(12.3))
@interface ScreenCaptureKitDeviceHelper
: NSObject <SCStreamDelegate, SCStreamOutput>
- (instancetype)initWithSampleCallback:(SampleCallback)sampleCallback
errorCallback:(ErrorCallback)errorCallback;
@end
@implementation ScreenCaptureKitDeviceHelper {
SampleCallback _sampleCallback;
ErrorCallback _errorCallback;
}
- (instancetype)initWithSampleCallback:(SampleCallback)sampleCallback
errorCallback:(ErrorCallback)errorCallback {
if (self = [super init]) {
_sampleCallback = sampleCallback;
_errorCallback = errorCallback;
}
return self;
}
- (void)stream:(SCStream*)stream
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
ofType:(SCStreamOutputType)type {
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
if (!pixelBuffer)
return;
// Read out width, height and scaling from metadata to determine
// |contentSize|, which is the size of the content on screen, and
// |visibleRect|, which is the region of the IOSurface that contains the
// captured content. |contentSize| is used to detect when a captured window is
// resized so that the stream configuration can be updated and |visibleRect|
// is needed because the IOSurface may be larger than the captured content.
std::optional<gfx::Size> contentSize;
std::optional<gfx::Rect> visibleRect;
CFArrayRef attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(
sampleBuffer, /*createIfNecessary=*/false);
if (attachmentsArray && CFArrayGetCount(attachmentsArray) > 0) {
CFDictionaryRef attachment = base::apple::CFCast<CFDictionaryRef>(
CFArrayGetValueAtIndex(attachmentsArray, 0));
if (attachment) {
CFDictionaryRef contentRectValue = base::apple::CFCast<CFDictionaryRef>(
CFDictionaryGetValue(attachment, base::apple::NSToCFPtrCast(
SCStreamFrameInfoContentRect)));
CFNumberRef scaleFactorValue = base::apple::CFCast<CFNumberRef>(
CFDictionaryGetValue(attachment, base::apple::NSToCFPtrCast(
SCStreamFrameInfoScaleFactor)));
CFNumberRef contentScaleValue = base::apple::CFCast<CFNumberRef>(
CFDictionaryGetValue(attachment, base::apple::NSToCFPtrCast(
SCStreamFrameInfoContentScale)));
if (contentRectValue && scaleFactorValue && contentScaleValue) {
CGRect contentRect = {};
bool succeed = CGRectMakeWithDictionaryRepresentation(contentRectValue,
&contentRect);
float scaleFactor = 1.0f;
succeed &= CFNumberGetValue(scaleFactorValue, kCFNumberFloatType,
&scaleFactor);
float contentScale = 1.0f;
succeed &= CFNumberGetValue(contentScaleValue, kCFNumberFloatType,
&contentScale);
if (succeed) {
contentRect.size.width *= scaleFactor;
contentRect.size.height *= scaleFactor;
visibleRect.emplace(contentRect);
contentSize.emplace(round(contentRect.size.width / contentScale),
round(contentRect.size.height / contentScale));
}
}
}
}
IOSurfaceRef ioSurface = CVPixelBufferGetIOSurface(pixelBuffer);
if (!ioSurface)
return;
_sampleCallback.Run(
gfx::ScopedInUseIOSurface(ioSurface, base::scoped_policy::RETAIN),
contentSize, visibleRect);
}
- (void)stream:(SCStream*)stream didStopWithError:(NSError*)error {
_errorCallback.Run();
}
+ (SCStreamConfiguration*)streamConfigurationWithFrameSize:(gfx::Size)frameSize
destRectInFrame:
(gfx::RectF)destRectInFrame
frameRate:(float)frameRate {
SCStreamConfiguration* config = [[SCStreamConfiguration alloc] init];
config.width = frameSize.width();
config.height = frameSize.height();
config.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
config.destinationRect = destRectInFrame.ToCGRect();
config.backgroundColor = CGColorGetConstantColor(kCGColorBlack);
config.scalesToFit = YES;
config.showsCursor = YES;
config.colorSpaceName = kCGColorSpaceSRGB;
config.minimumFrameInterval =
CMTimeMake(media::kFrameRatePrecision,
static_cast<int>(frameRate * media::kFrameRatePrecision));
return config;
}
@end
namespace content {
namespace {
BASE_FEATURE(kScreenCaptureKitFullDesktopFallback,
"ScreenCaptureKitFullDesktopFallback",
base::FEATURE_ENABLED_BY_DEFAULT);
class API_AVAILABLE(macos(12.3)) ScreenCaptureKitDeviceMac
: public IOSurfaceCaptureDeviceBase,
public ScreenCaptureKitResetStreamInterface {
public:
explicit ScreenCaptureKitDeviceMac(const DesktopMediaID& source,
SCContentFilter* filter)
: source_(source),
filter_(filter),
device_task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()) {
SampleCallback sample_callback = base::BindPostTask(
device_task_runner_,
base::BindRepeating(&ScreenCaptureKitDeviceMac::OnStreamSample,
weak_factory_.GetWeakPtr()));
ErrorCallback error_callback = base::BindPostTask(
device_task_runner_,
base::BindRepeating(&ScreenCaptureKitDeviceMac::OnStreamError,
weak_factory_.GetWeakPtr()));
helper_ = [[ScreenCaptureKitDeviceHelper alloc]
initWithSampleCallback:sample_callback
errorCallback:error_callback];
}
ScreenCaptureKitDeviceMac(const ScreenCaptureKitDeviceMac&) = delete;
ScreenCaptureKitDeviceMac& operator=(const ScreenCaptureKitDeviceMac&) =
delete;
~ScreenCaptureKitDeviceMac() override = default;
void OnShareableContentCreated(SCShareableContent* content) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (!content) {
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitFailedGetShareableContent,
FROM_HERE, "Failed getShareableContentWithCompletionHandler");
return;
}
SCContentFilter* filter;
switch (source_.type) {
case DesktopMediaID::TYPE_SCREEN:
for (SCDisplay* display in content.displays) {
// There's currently no support for stitching desktops together as
// requested by kFullDesktopScreenId. Capture the first display as a
// fallback. See https://crbug.com/325530044.
if (source_.id == display.displayID ||
source_.id == webrtc::kFullDesktopScreenId) {
filter = [[SCContentFilter alloc] initWithDisplay:display
excludingWindows:@[]];
stream_config_content_size_ =
gfx::Size(display.width, display.height);
break;
}
}
break;
case DesktopMediaID::TYPE_WINDOW:
for (SCWindow* window in content.windows) {
if (source_.id == window.windowID) {
filter = [[SCContentFilter alloc]
initWithDesktopIndependentWindow:window];
CGRect frame = window.frame;
stream_config_content_size_ = gfx::Size(frame.size);
if (!fullscreen_module_) {
fullscreen_module_ = MaybeCreateScreenCaptureKitFullscreenModule(
device_task_runner_, *this, window);
}
}
}
break;
default:
NOTREACHED_IN_MIGRATION();
break;
}
CreateStream(filter);
}
void CreateStream(SCContentFilter* filter) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (!filter) {
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitFailedToFindSCDisplay,
FROM_HERE, "Failed to find SCDisplay");
return;
}
if (@available(macOS 14.0, *)) {
// Update the content size. This step is neccessary when used together
// with SCContentSharingPicker. If the Chrome picker is used, it will
// change to retina resolution if applicable.
stream_config_content_size_ =
gfx::Size(filter.contentRect.size.width * filter.pointPixelScale,
filter.contentRect.size.height * filter.pointPixelScale);
}
gfx::RectF dest_rect_in_frame;
actual_capture_format_ = capture_params().requested_format;
actual_capture_format_.pixel_format = media::PIXEL_FORMAT_NV12;
ComputeFrameSizeAndDestRect(stream_config_content_size_,
actual_capture_format_.frame_size,
dest_rect_in_frame);
SCStreamConfiguration* config = [ScreenCaptureKitDeviceHelper
streamConfigurationWithFrameSize:actual_capture_format_.frame_size
destRectInFrame:dest_rect_in_frame
frameRate:actual_capture_format_.frame_rate];
stream_ = [[SCStream alloc] initWithFilter:filter
configuration:config
delegate:helper_];
{
NSError* error = nil;
bool add_stream_output_result =
[stream_ addStreamOutput:helper_
type:SCStreamOutputTypeScreen
sampleHandlerQueue:dispatch_get_main_queue()
error:&error];
if (!add_stream_output_result) {
stream_ = nil;
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitFailedAddStreamOutput,
FROM_HERE, "Failed addStreamOutput");
return;
}
}
auto stream_started_callback = base::BindPostTask(
device_task_runner_,
base::BindRepeating(&ScreenCaptureKitDeviceMac::OnStreamStarted,
weak_factory_.GetWeakPtr()));
auto handler = ^(NSError* error) {
stream_started_callback.Run(!!error);
};
[stream_ startCaptureWithCompletionHandler:handler];
}
void OnStreamStarted(bool error) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (error) {
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitFailedStartCapture,
FROM_HERE, "Failed startCaptureWithCompletionHandler");
return;
}
client()->OnStarted();
if (fullscreen_module_) {
fullscreen_module_->Start();
}
}
void OnStreamStopped(bool error) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (error) {
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitFailedStopCapture,
FROM_HERE, "Failed stopCaptureWithCompletionHandler");
return;
}
}
void OnStreamSample(gfx::ScopedInUseIOSurface io_surface,
std::optional<gfx::Size> content_size,
std::optional<gfx::Rect> visible_rect) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (requested_capture_format_) {
// Does the size of io_surface match the requested format?
size_t io_surface_width = IOSurfaceGetWidth(io_surface.get());
size_t io_surface_height = IOSurfaceGetHeight(io_surface.get());
DVLOG(3) << "Waiting for new capture format, "
<< requested_capture_format_->frame_size.width() << " x "
<< requested_capture_format_->frame_size.height()
<< ". IO surface size " << io_surface_width << " x "
<< io_surface_height;
if (static_cast<size_t>(requested_capture_format_->frame_size.width()) ==
io_surface_width &&
static_cast<size_t>(requested_capture_format_->frame_size.height()) ==
io_surface_height) {
actual_capture_format_ = requested_capture_format_.value();
requested_capture_format_.reset();
}
} else {
// No current request for new capture format. Check to see if content_size
// has changed and requires an updated configuration. We only track the
// content size for window capturing since the resolution does not
// normally change during a session and because the content scale is wrong
// for retina displays.
if (source_.type == DesktopMediaID::TYPE_WINDOW && content_size &&
(stream_config_content_size_.width() != content_size->width() ||
stream_config_content_size_.height() != content_size->height())) {
DVLOG(3) << "Content size changed to " << content_size->width() << " x "
<< content_size->height() << ". It was "
<< stream_config_content_size_.width() << " x "
<< stream_config_content_size_.height();
stream_config_content_size_ = content_size.value();
gfx::RectF dest_rect_in_frame;
gfx::Size new_frame_size;
ComputeFrameSizeAndDestRect(stream_config_content_size_, new_frame_size,
dest_rect_in_frame);
if (new_frame_size.width() !=
actual_capture_format_.frame_size.width() ||
new_frame_size.height() !=
actual_capture_format_.frame_size.height()) {
DVLOG(3) << "Calling updateConfiguration with new frame size: "
<< new_frame_size.width() << " x "
<< new_frame_size.height();
requested_capture_format_ = actual_capture_format_;
requested_capture_format_->frame_size = new_frame_size;
// Update stream configuration.
SCStreamConfiguration* config = [ScreenCaptureKitDeviceHelper
streamConfigurationWithFrameSize:requested_capture_format_
->frame_size
destRectInFrame:dest_rect_in_frame
frameRate:requested_capture_format_->
frame_rate];
__block base::OnceCallback<void()> on_update_configuration_error =
base::BindPostTask(
device_task_runner_,
base::BindOnce(
&ScreenCaptureKitDeviceMac::OnUpdateConfigurationError,
weak_factory_.GetWeakPtr()));
[stream_
updateConfiguration:config
completionHandler:^(NSError* _Nullable error) {
if (error) {
std::move(on_update_configuration_error).Run();
}
}];
}
}
}
// The IO surface may be larger than the actual content size. Pass on
// visible rect to be able to render/encode the frame correctly.
OnReceivedIOSurfaceFromStream(
io_surface, actual_capture_format_,
visible_rect.value_or(gfx::Rect(actual_capture_format_.frame_size)));
}
void OnStreamError() {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (is_resetting_ || (fullscreen_module_ &&
fullscreen_module_->is_fullscreen_window_active())) {
// Clear `is_resetting_` because the completion handler in ResetStreamTo()
// may not be called if there's an error.
is_resetting_ = false;
// The stream_ is no longer valid. Restart the stream from scratch.
if (fullscreen_module_) {
fullscreen_module_->Reset();
}
OnStart();
} else {
client()->OnError(media::VideoCaptureError::kScreenCaptureKitStreamError,
FROM_HERE, "Stream delegate called didStopWithError");
}
}
void OnUpdateContentFilterCompleted(NSError* _Nullable error) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
is_resetting_ = false;
if (error) {
client()->OnError(media::VideoCaptureError::kScreenCaptureKitStreamError,
FROM_HERE,
"Error on updateContentFilter (fullscreen window).");
}
}
void OnUpdateConfigurationError() {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
client()->OnError(media::VideoCaptureError::kScreenCaptureKitStreamError,
FROM_HERE, "Error on updateConfiguration");
}
// IOSurfaceCaptureDeviceBase:
void OnStart() override {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (filter_) {
// SCContentSharingPicker is used where filter_ is set on creation.
CreateStream(filter_);
} else {
// Chrome picker is used.
auto content_callback = base::BindPostTask(
device_task_runner_,
base::BindRepeating(
&ScreenCaptureKitDeviceMac::OnShareableContentCreated,
weak_factory_.GetWeakPtr()));
auto handler = ^(SCShareableContent* content, NSError* error) {
content_callback.Run(content);
};
[SCShareableContent getShareableContentWithCompletionHandler:handler];
}
}
void OnStop() override {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (stream_) {
auto stream_stopped_callback = base::BindPostTask(
device_task_runner_,
base::BindRepeating(&ScreenCaptureKitDeviceMac::OnStreamStopped,
weak_factory_.GetWeakPtr()));
auto handler = ^(NSError* error) {
stream_stopped_callback.Run(!!error);
};
[stream_ stopCaptureWithCompletionHandler:handler];
NSError* error = nil;
bool remove_stream_output_result =
[stream_ removeStreamOutput:helper_
type:SCStreamOutputTypeScreen
error:&error];
if (!remove_stream_output_result) {
DLOG(ERROR) << "Failed removeStreamOutput";
}
}
weak_factory_.InvalidateWeakPtrs();
stream_ = nil;
}
// ScreenCaptureKitResetStreamInterface.
void ResetStreamTo(SCWindow* window) override {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (!window || is_resetting_) {
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitResetStreamError,
FROM_HERE, "Error on ResetStreamTo.");
return;
}
is_resetting_ = true;
SCContentFilter* filter =
[[SCContentFilter alloc] initWithDesktopIndependentWindow:window];
__block base::OnceCallback<void(NSError*)>
on_update_content_filter_completed = base::BindPostTask(
device_task_runner_,
base::BindOnce(
&ScreenCaptureKitDeviceMac::OnUpdateContentFilterCompleted,
weak_factory_.GetWeakPtr()));
[stream_ updateContentFilter:filter
completionHandler:^(NSError* _Nullable error) {
std::move(on_update_content_filter_completed).Run(error);
}];
}
private:
const DesktopMediaID source_;
SCContentFilter* const filter_;
const scoped_refptr<base::SingleThreadTaskRunner> device_task_runner_;
// The actual format of the video frames that are sent to `client`.
media::VideoCaptureFormat actual_capture_format_;
// The requested format if a request to update the configuration has been
// sent.
std::optional<media::VideoCaptureFormat> requested_capture_format_;
// The size of the content at the time that we configured the stream.
gfx::Size stream_config_content_size_;
// Helper class that acts as output and delegate for `stream_`.
ScreenCaptureKitDeviceHelper* __strong helper_;
// This is used to detect when a captured presentation enters fullscreen mode.
// If this happens, the module will call the ResetStreamTo function.
std::unique_ptr<ScreenCaptureKitFullscreenModule> fullscreen_module_;
bool is_resetting_ = false;
// The stream that does the capturing.
SCStream* __strong stream_;
base::WeakPtrFactory<ScreenCaptureKitDeviceMac> weak_factory_{this};
};
} // namespace
// Although ScreenCaptureKit is available in 12.3 there were some bugs that
// were not fixed until 13.2.
API_AVAILABLE(macos(13.2))
std::unique_ptr<media::VideoCaptureDevice> CreateScreenCaptureKitDeviceMac(
const DesktopMediaID& source,
SCContentFilter* filter) {
switch (source.type) {
case DesktopMediaID::TYPE_SCREEN:
// ScreenCaptureKitDeviceMac only supports a single display at a time.
// It will not stitch desktops together. If
// kScreenCaptureKitFullDesktopFallback is enabled, we will fallback to
// capturing the first display in the list returned from
// getShareableContent. https://crbug.com/1178360 and
// https://crbug.com/325530044
if ((source.id == webrtc::kFullDesktopScreenId &&
!base::FeatureList::IsEnabled(
kScreenCaptureKitFullDesktopFallback)) ||
source.id == webrtc::kInvalidScreenId) {
return nullptr;
}
break;
case DesktopMediaID::TYPE_WINDOW:
break;
default:
// ScreenCaptureKitDeviceMac supports only TYPE_SCREEN and TYPE_WINDOW.
// https://crbug.com/1176900
return nullptr;
}
IncrementDesktopCaptureCounter(SCREEN_CAPTURER_CREATED);
IncrementDesktopCaptureCounter(source.audio_share
? SCREEN_CAPTURER_CREATED_WITH_AUDIO
: SCREEN_CAPTURER_CREATED_WITHOUT_AUDIO);
return std::make_unique<ScreenCaptureKitDeviceMac>(source, filter);
}
} // namespace content