blob: 50a779be2e7d3a95496e2791187a6b56266786eb [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// 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 "base/mac/scoped_nsobject.h"
#include "base/task/bind_post_task.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 "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h"
#include "ui/gfx/native_widget_types.h"
using SampleCallback = base::RepeatingCallback<void(gfx::ScopedInUseIOSurface)>;
using ErrorCallback = base::RepeatingClosure;
API_AVAILABLE(macos(12.3))
@interface ScreenCaptureKitDeviceHelper
: NSObject <SCStreamDelegate, SCStreamOutput> {
SampleCallback _sampleCallback;
ErrorCallback _errorCallback;
}
- (instancetype)initWithSampleCallback:(SampleCallback)sampleCallback
errorCallback:(ErrorCallback)errorCallback;
@end
@implementation ScreenCaptureKitDeviceHelper
- (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;
IOSurfaceRef ioSurface = CVPixelBufferGetIOSurface(pixelBuffer);
if (!ioSurface)
return;
_sampleCallback.Run(
gfx::ScopedInUseIOSurface(ioSurface, base::scoped_policy::RETAIN));
}
- (void)stream:(SCStream*)stream didStopWithError:(NSError*)error {
_errorCallback.Run();
}
@end
namespace content {
namespace {
class API_AVAILABLE(macos(12.3)) ScreenCaptureKitDeviceMac
: public IOSurfaceCaptureDeviceBase {
public:
ScreenCaptureKitDeviceMac(const DesktopMediaID& source)
: source_(source),
device_task_runner_(base::ThreadTaskRunnerHandle::Get()) {
SampleCallback sample_callback = base::BindPostTask(
base::ThreadTaskRunnerHandle::Get(),
base::BindRepeating(&ScreenCaptureKitDeviceMac::OnStreamSample,
weak_factory_.GetWeakPtr()));
ErrorCallback error_callback = base::BindPostTask(
base::ThreadTaskRunnerHandle::Get(),
base::BindRepeating(&ScreenCaptureKitDeviceMac::OnStreamError,
weak_factory_.GetWeakPtr()));
helper_.reset([[ScreenCaptureKitDeviceHelper alloc]
initWithSampleCallback:sample_callback
errorCallback:error_callback]);
}
ScreenCaptureKitDeviceMac(const ScreenCaptureKitDeviceMac&) = delete;
ScreenCaptureKitDeviceMac& operator=(const ScreenCaptureKitDeviceMac&) =
delete;
~ScreenCaptureKitDeviceMac() override = default;
void OnShareableContentCreated(
base::scoped_nsobject<SCShareableContent> content) {
if (!content) {
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitFailedGetShareableContent,
FROM_HERE, "Failed getShareableContentWithCompletionHandler");
return;
}
base::scoped_nsobject<SCContentFilter> filter;
gfx::Size content_size;
switch (source_.type) {
case DesktopMediaID::TYPE_SCREEN:
for (SCDisplay* display : [content displays]) {
if (source_.id == [display displayID]) {
NSArray<SCWindow*>* exclude_windows = nil;
filter.reset([[SCContentFilter alloc]
initWithDisplay:display
excludingWindows:exclude_windows]);
content_size = gfx::Size([display width], [display height]);
break;
}
}
break;
case DesktopMediaID::TYPE_WINDOW:
for (SCWindow* window : [content windows]) {
if (source_.id == [window windowID]) {
filter.reset([[SCContentFilter alloc]
initWithDesktopIndependentWindow:window]);
CGRect frame = [window frame];
content_size = gfx::Size(frame.size);
break;
}
}
break;
default:
NOTREACHED();
break;
}
if (!filter) {
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitFailedToFindSCDisplay,
FROM_HERE, "Failed to find SCDisplay");
return;
}
gfx::RectF dest_rect_in_frame;
actual_capture_format_ = capture_params().requested_format;
actual_capture_format_.pixel_format = media::PIXEL_FORMAT_NV12;
ComputeFrameSizeAndDestRect(content_size, actual_capture_format_.frame_size,
dest_rect_in_frame);
base::scoped_nsobject<SCStreamConfiguration> config(
[[SCStreamConfiguration alloc] init]);
[config setWidth:actual_capture_format_.frame_size.width()];
[config setHeight:actual_capture_format_.frame_size.height()];
[config setPixelFormat:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange];
[config setDestinationRect:dest_rect_in_frame.ToCGRect()];
[config setBackgroundColor:CGColorGetConstantColor(kCGColorBlack)];
[config setScalesToFit:YES];
[config setShowsCursor:YES];
[config setColorSpaceName:kCGColorSpaceSRGB];
[config
setMinimumFrameInterval:CMTimeMakeWithSeconds(
1 / actual_capture_format_.frame_rate, 1)];
stream_.reset([[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_.reset();
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) {
if (error) {
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitFailedStartCapture,
FROM_HERE, "Failed startCaptureWithCompletionHandler");
return;
}
client()->OnStarted();
}
void OnStreamStopped(bool error) {
if (error) {
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitFailedStopCapture,
FROM_HERE, "Failed stopCaptureWithCompletionHandler");
return;
}
}
void OnStreamSample(gfx::ScopedInUseIOSurface io_surface) {
// TODO(https://crbug.com/1309653): Reconfigure the stream if the IOSurface
// should be resized.
OnReceivedIOSurfaceFromStream(io_surface, actual_capture_format_);
}
void OnStreamError() {
client()->OnError(media::VideoCaptureError::kScreenCaptureKitStreamError,
FROM_HERE, "Stream delegate called didStopWithError");
}
// IOSurfaceCaptureDeviceBase:
void OnStart() override {
auto content_callback = base::BindPostTask(
device_task_runner_,
base::BindRepeating(
&ScreenCaptureKitDeviceMac::OnShareableContentCreated,
weak_factory_.GetWeakPtr()));
auto handler = ^(SCShareableContent* content, NSError* error) {
content_callback.Run(base::scoped_nsobject<SCShareableContent>(
content, base::scoped_policy::RETAIN));
};
[SCShareableContent getShareableContentWithCompletionHandler:handler];
}
void OnStop() override {
if (stream_) {
auto stream_started_callback = base::BindPostTask(
device_task_runner_,
base::BindRepeating(&ScreenCaptureKitDeviceMac::OnStreamStopped,
weak_factory_.GetWeakPtr()));
auto handler = ^(NSError* error) {
stream_started_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_.reset();
}
private:
const DesktopMediaID source_;
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_;
// Helper class that acts as output and delegate for `stream_`.
base::scoped_nsobject<ScreenCaptureKitDeviceHelper> helper_;
// The stream that does the capturing.
base::scoped_nsobject<SCStream> stream_;
base::WeakPtrFactory<ScreenCaptureKitDeviceMac> weak_factory_{this};
};
} // namespace
std::unique_ptr<media::VideoCaptureDevice> CreateScreenCaptureKitDeviceMac(
const DesktopMediaID& source) {
switch (source.type) {
case DesktopMediaID::TYPE_SCREEN:
// ScreenCaptureKitDeviceMac only supports a single display at a time. It
// will not stitch desktops together.
// https://crbug.com/1178360
if (source.id == webrtc::kFullDesktopScreenId ||
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);
if (@available(macos 12.3, *))
return std::make_unique<ScreenCaptureKitDeviceMac>(source);
return nullptr;
}
} // namespace content