blob: 0dbc7ebc4ba6066eb6b20f8e66f50a1dff4d94f3 [file] [log] [blame]
// Copyright 2024 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/native_screen_capture_picker_mac.h"
#import <ScreenCaptureKit/ScreenCaptureKit.h>
#include <unordered_map>
#include <utility>
#include "base/features.h"
#include "base/metrics/histogram_functions.h"
#include "base/timer/timer.h"
#include "content/browser/media/capture/native_screen_capture_picker.h"
#include "content/browser/media/capture/screen_capture_kit_device_mac.h"
#include "content/public/browser/desktop_media_id.h"
#include "media/capture/video/video_capture_device.h"
using Source = webrtc::DesktopCapturer::Source;
using PickerCallback = base::OnceCallback<void(Source)>;
using PickerCancelCallback = base::OnceClosure;
using PickerErrorCallback = base::OnceClosure;
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class SCContentSharingPickerOperation {
kPresentScreen_Start = 0,
kPresentScreen_Update = 1,
kPresentScreen_Cancel = 2,
kPresentScreen_Error = 3,
kPresentWindow_Start = 4,
kPresentWindow_Update = 5,
kPresentWindow_Cancel = 6,
kPresentWindow_Error = 7,
kMaxValue = kPresentWindow_Error
};
void API_AVAILABLE(macos(14.0))
LogToUma(SCContentSharingPickerOperation operation) {
base::UmaHistogramEnumeration(
"Media.ScreenCaptureKit.SCContentSharingPicker2", operation);
}
void API_AVAILABLE(macos(14.0))
LogUpdateToUma(content::DesktopMediaID::Type type) {
LogToUma(type == content::DesktopMediaID::Type::TYPE_SCREEN
? SCContentSharingPickerOperation::kPresentScreen_Update
: SCContentSharingPickerOperation::kPresentWindow_Update);
}
void API_AVAILABLE(macos(14.0))
LogCancelToUma(content::DesktopMediaID::Type type) {
LogToUma(type == content::DesktopMediaID::Type::TYPE_SCREEN
? SCContentSharingPickerOperation::kPresentScreen_Cancel
: SCContentSharingPickerOperation::kPresentWindow_Cancel);
}
void API_AVAILABLE(macos(14.0))
LogErrorToUma(content::DesktopMediaID::Type type) {
LogToUma(type == content::DesktopMediaID::Type::TYPE_SCREEN
? SCContentSharingPickerOperation::kPresentScreen_Error
: SCContentSharingPickerOperation::kPresentWindow_Error);
}
API_AVAILABLE(macos(14.0))
@interface PickerObserver : NSObject <SCContentSharingPickerObserver>
- (instancetype)initWithPickerCallback:(PickerCallback)pickerCallback
cancelCallback:(PickerCancelCallback)cancelCallback
errorCallback:(PickerErrorCallback)errorCallback
assignSourceId:(int)assignedSourceId
type:(content::DesktopMediaID::Type)type;
@property(strong, readonly) SCContentFilter* contentFilter;
@end
@implementation PickerObserver {
PickerCallback _pickerCallback;
PickerCancelCallback _cancelCallback;
PickerErrorCallback _errorCallback;
int _assignedSourceId;
content::DesktopMediaID::Type _type;
bool _receivedFirstResponse;
}
@synthesize contentFilter;
- (instancetype)initWithPickerCallback:(PickerCallback)pickerCallback
cancelCallback:(PickerCancelCallback)cancelCallback
errorCallback:(PickerErrorCallback)errorCallback
assignSourceId:(int)assignedSourceId
type:(content::DesktopMediaID::Type)type {
if (self = [super init]) {
_pickerCallback = std::move(pickerCallback);
_cancelCallback = std::move(cancelCallback);
_errorCallback = std::move(errorCallback);
_assignedSourceId = assignedSourceId;
_type = type;
_receivedFirstResponse = false;
}
return self;
}
- (void)contentSharingPicker:(SCContentSharingPicker*)picker
didUpdateWithFilter:(SCContentFilter*)filter
forStream:(SCStream*)stream {
VLOG(1) << "NSCPM::contentSharingPicker:didUpdateWithFilter: source_id = "
<< _assignedSourceId;
if (!_receivedFirstResponse) {
_receivedFirstResponse = true;
LogUpdateToUma(_type);
}
contentFilter = filter;
Source source;
source.id = _assignedSourceId;
if (_pickerCallback) {
std::move(_pickerCallback).Run(source);
}
}
- (void)contentSharingPicker:(SCContentSharingPicker*)picker
didCancelForStream:(SCStream*)stream {
VLOG(1) << "NSCPM:contentSharingPicker:didCancelForStream: source_id = "
<< _assignedSourceId;
if (!_receivedFirstResponse) {
_receivedFirstResponse = true;
LogCancelToUma(_type);
}
if (_cancelCallback) {
std::move(_cancelCallback).Run();
}
}
- (void)contentSharingPickerStartDidFailWithError:(NSError*)error {
VLOG(1) << "NSCPM::contentSharingPickerStartDidFailWithError: source_id = "
<< _assignedSourceId << ", code = " << [error code]
<< ", domain = " << [error domain]
<< ", description = " << [error localizedDescription];
if (!_receivedFirstResponse) {
_receivedFirstResponse = true;
LogErrorToUma(_type);
}
if (_errorCallback) {
std::move(_errorCallback).Run();
}
}
@end
namespace content {
// When enabled, this allows you to change the maximum number of streams you can
// share with the native picker to kMaxContentShareCountValue.
BASE_FEATURE(kMaxContentShareCount,
"MaxContentShareCount",
base::FEATURE_DISABLED_BY_DEFAULT);
constexpr base::FeatureParam<int> kMaxContentShareCountValue = {
&kMaxContentShareCount, "max_content_share_count", 50};
class API_AVAILABLE(macos(14.0)) NativeScreenCapturePickerMac
: public NativeScreenCapturePicker {
public:
NativeScreenCapturePickerMac();
~NativeScreenCapturePickerMac() override;
void Open(DesktopMediaID::Type type,
base::OnceCallback<void(DesktopMediaID::Id)> created_callback,
base::OnceCallback<void(Source)> picker_callback,
base::OnceClosure cancel_callback,
base::OnceClosure error_callback) override;
void Close(DesktopMediaID device_id) override;
std::unique_ptr<media::VideoCaptureDevice> CreateDevice(
const DesktopMediaID& source) override;
base::WeakPtr<NativeScreenCapturePicker> GetWeakPtr() override;
private:
void ScheduleCleanup(DesktopMediaID::Id id);
void CleanupContentFilter(DesktopMediaID::Id id);
NSMutableDictionary<NSNumber*, PickerObserver*>* __strong picker_observers_;
// Cached content filters are needed so that a stream can be restarted without
// having to show the native picker again.
NSMutableDictionary<NSNumber*, SCContentFilter*>* __strong
cached_content_filters_;
std::unordered_map<DesktopMediaID::Id, base::OneShotTimer>
cached_content_filters_cleanup_timers_;
DesktopMediaID::Id next_id_ = 0;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<NativeScreenCapturePickerMac> weak_ptr_factory_{this};
};
NativeScreenCapturePickerMac::NativeScreenCapturePickerMac()
: picker_observers_([[NSMutableDictionary alloc] init]),
cached_content_filters_([[NSMutableDictionary alloc] init]) {
DETACH_FROM_SEQUENCE(sequence_checker_);
}
NativeScreenCapturePickerMac::~NativeScreenCapturePickerMac() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void NativeScreenCapturePickerMac::Open(
DesktopMediaID::Type type,
base::OnceCallback<void(DesktopMediaID::Id)> created_callback,
base::OnceCallback<void(Source)> picker_callback,
base::OnceClosure cancel_callback,
base::OnceClosure error_callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(type == DesktopMediaID::Type::TYPE_SCREEN ||
type == DesktopMediaID::Type::TYPE_WINDOW);
if (@available(macOS 14.0, *)) {
NSNumber* source_id = @(next_id_);
PickerObserver* picker_observer = [[PickerObserver alloc]
initWithPickerCallback:std::move(picker_callback)
cancelCallback:std::move(cancel_callback)
errorCallback:std::move(error_callback)
assignSourceId:next_id_
type:type];
picker_observers_[source_id] = picker_observer;
std::move(created_callback).Run(next_id_);
++next_id_;
SCContentSharingPicker* picker = [SCContentSharingPicker sharedPicker];
[picker addObserver:picker_observer];
picker.active = true;
SCContentSharingPickerConfiguration* config = [picker defaultConfiguration];
// TODO(https://crbug.com/360781940): Add support for changing selected
// content. The problem to solve is how this should interact with stream
// restart.
config.allowsChangingSelectedContent = false;
NSNumber* max_stream_count = @(kMaxContentShareCountValue.Get());
if (type == DesktopMediaID::Type::TYPE_SCREEN) {
config.allowedPickerModes = SCContentSharingPickerModeSingleDisplay;
picker.defaultConfiguration = config;
picker.maximumStreamCount = max_stream_count;
[picker presentPickerUsingContentStyle:SCShareableContentStyleDisplay];
VLOG(1) << "NSCPM: Show screen-sharing picker for source_id = "
<< source_id.longValue;
LogToUma(SCContentSharingPickerOperation::kPresentScreen_Start);
} else {
config.allowedPickerModes = SCContentSharingPickerModeSingleWindow;
picker.defaultConfiguration = config;
picker.maximumStreamCount = max_stream_count;
[picker presentPickerUsingContentStyle:SCShareableContentStyleWindow];
VLOG(1) << "NSCPM: Show window-sharing picker for source_id = "
<< source_id.longValue;
LogToUma(SCContentSharingPickerOperation::kPresentWindow_Start);
}
} else {
NOTREACHED();
}
}
void NativeScreenCapturePickerMac::Close(DesktopMediaID device_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (@available(macOS 14.0, *)) {
ScheduleCleanup(device_id.id);
NSNumber* source_id = @(device_id.id);
PickerObserver* picker_observer = picker_observers_[source_id];
if (!picker_observer) {
VLOG(1) << "NSCPM: Closing source_id = " << device_id.id
<< ", picker_observer = null";
return;
}
[picker_observers_ removeObjectForKey:source_id];
SCContentSharingPicker* picker = [SCContentSharingPicker sharedPicker];
[picker removeObserver:picker_observer];
// Don't deactivate the picker if there are any active picker observers.
if ([picker_observers_ count] > 0) {
VLOG(1) << "NSCPM: Closing source_id = " << device_id.id
<< ", picker_observers_.count = " << [picker_observers_ count];
return;
}
picker.active = false;
VLOG(1) << "NSCPM: Closing source_id = " << device_id.id;
} else {
NOTREACHED();
}
}
std::unique_ptr<media::VideoCaptureDevice>
NativeScreenCapturePickerMac::CreateDevice(const DesktopMediaID& source) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
cached_content_filters_cleanup_timers_.erase(source.id);
NSNumber* source_id = @(source.id);
SCContentFilter* filter = cached_content_filters_[source_id];
if (!filter) {
PickerObserver* picker_observer = picker_observers_[source_id];
filter = [picker_observer contentFilter];
cached_content_filters_[source_id] = filter;
}
VLOG(1) << "NSCPM: CreateDevice: source_id = " << source.id
<< ", cached_content_filters_.count = " <<
[cached_content_filters_ count];
return CreateScreenCaptureKitDeviceMac(source, filter);
}
void NativeScreenCapturePickerMac::ScheduleCleanup(DesktopMediaID::Id id) {
// We need to retain the content filter for some time in case the device is
// restarted, e.g., when ApplyConstraints is called on a MediaStreamTrack.
cached_content_filters_cleanup_timers_[id].Start(
FROM_HERE, base::Seconds(60),
base::BindOnce(
&NativeScreenCapturePickerMac::CleanupContentFilter,
// Passing `this` is safe since
// `cached_content_filters_cleanup_timers_` is owned by `this`.
base::Unretained(this), id));
}
void NativeScreenCapturePickerMac::CleanupContentFilter(DesktopMediaID::Id id) {
NSNumber* source_id = @(id);
[cached_content_filters_ removeObjectForKey:source_id];
cached_content_filters_cleanup_timers_.erase(id);
VLOG(1) << "NSCPM: CleanupContentFilter: source_id = " << id
<< ", cached_content_filters_.count = " <<
[cached_content_filters_ count];
}
base::WeakPtr<NativeScreenCapturePicker>
NativeScreenCapturePickerMac::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
std::unique_ptr<NativeScreenCapturePicker>
CreateNativeScreenCapturePickerMac() {
if (@available(macOS 14.0, *)) {
return std::make_unique<NativeScreenCapturePickerMac>();
}
return nullptr;
}
} // namespace content