blob: 03bfff17012d60648c7e4c7da1e4e9575948e2d2 [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/task/bind_post_task.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"
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
// Enables the allowsChangingSelectedContent property on the native macOS
// picker (SCContentSharingPicker). This allows users to select a new window or
// screen to share without restarting the stream and enables the capture to
// follow an app into its fullscreen presentation mode.
// TODO(crbug.com/409475502): Remove this feature once it has been rolled out to
// stable for a few milestones.
BASE_FEATURE(kAllowChangingSelectedContent, base::FEATURE_DISABLED_BY_DEFAULT);
using Source = webrtc::DesktopCapturer::Source;
// 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:
(base::RepeatingCallback<void(SCContentFilter*, SCStream*)>)
pickerCallback
cancelCallback:
(base::RepeatingCallback<void(SCStream*)>)cancelCallback
errorCallback:
(base::RepeatingCallback<void(NSError*)>)errorCallback;
@end
@implementation PickerObserver {
base::RepeatingCallback<void(SCContentFilter*, SCStream*)> _pickerCallback;
base::RepeatingCallback<void(SCStream*)> _cancelCallback;
base::RepeatingCallback<void(NSError*)> _errorCallback;
}
- (instancetype)
initWithPickerCallback:
(base::RepeatingCallback<void(SCContentFilter*, SCStream*)>)
pickerCallback
cancelCallback:
(base::RepeatingCallback<void(SCStream*)>)cancelCallback
errorCallback:
(base::RepeatingCallback<void(NSError*)>)errorCallback {
if ((self = [super init])) {
_pickerCallback = std::move(pickerCallback);
_cancelCallback = std::move(cancelCallback);
_errorCallback = std::move(errorCallback);
}
return self;
}
- (void)contentSharingPicker:(SCContentSharingPicker*)picker
didUpdateWithFilter:(SCContentFilter*)filter
forStream:(SCStream*)stream {
_pickerCallback.Run(filter, stream);
}
- (void)contentSharingPicker:(SCContentSharingPicker*)picker
didCancelForStream:(SCStream*)stream {
_cancelCallback.Run(stream);
}
- (void)contentSharingPickerStartDidFailWithError:(NSError*)error {
_errorCallback.Run(error);
}
@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, 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:
using PickerUpdateCallback =
base::RepeatingCallback<void(SCContentFilter*, SCStream*)>;
using PickerCancelCallback = base::RepeatingCallback<void(SCStream*)>;
using PickerErrorCallback = base::RepeatingCallback<void(NSError*)>;
NativeScreenCapturePickerMac();
~NativeScreenCapturePickerMac() override = default;
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:
// Callbacks called by PickerObserver when it receives an event from the OS.
void OnPickerObserverUpdated(SCContentFilter* filter, SCStream* stream);
void OnPickerObserverCancelled(SCStream* stream);
void OnPickerObserverEncounteredError(NSError* error);
void ScheduleCleanup(DesktopMediaID::Id id);
void CleanupContentFilter(DesktopMediaID::Id id);
// Callback called by `ScreenCaptureKitDeviceMac` on creating a new stream.
void UpdateStreamMap(DesktopMediaID::Id id, SCStream* stream);
// There is only one picker observer which has callbacks initialized only
// once.
PickerObserver* __strong picker_observer_;
base::OnceCallback<void(Source)> picker_callback_;
base::OnceClosure cancel_callback_;
base::OnceClosure error_callback_;
absl::flat_hash_set<int> received_first_response_;
// On every new getDisplayMedia request, we increment
// `active_picker_source_id_` and assign `active_picker_type_` the type of the
// request. Since while making a selection of the capture surface the rest of
// the UI becomes non-interactive, it is ensured that the callbacks are called
// for the right selection.
DesktopMediaID::Id active_picker_source_id_ = 0;
DesktopMediaID::Type active_picker_type_;
std::unordered_map<DesktopMediaID::Id, base::OneShotTimer>
cached_content_filters_cleanup_timers_;
// `active_source_ids_` keeps a track of the number of active capture
// sessions.
absl::flat_hash_set<int> active_source_ids_;
absl::flat_hash_map<DesktopMediaID::Id, SCContentFilter*> content_filters_;
absl::flat_hash_map<SCStream*, DesktopMediaID::Id> stream_to_id_map_;
const scoped_refptr<base::SingleThreadTaskRunner> device_task_runner_;
base::WeakPtrFactory<NativeScreenCapturePickerMac> weak_ptr_factory_{this};
};
NativeScreenCapturePickerMac::NativeScreenCapturePickerMac()
: device_task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()) {}
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(device_task_runner_->RunsTasksInCurrentSequence());
CHECK(type == DesktopMediaID::Type::TYPE_SCREEN ||
type == DesktopMediaID::Type::TYPE_WINDOW);
if (@available(macOS 14.0, *)) {
active_picker_source_id_++;
active_picker_type_ = type;
picker_callback_ = std::move(picker_callback);
cancel_callback_ = std::move(cancel_callback);
error_callback_ = std::move(error_callback);
PickerUpdateCallback observer_update_callback = base::BindPostTask(
device_task_runner_,
base::BindRepeating(
&NativeScreenCapturePickerMac::OnPickerObserverUpdated,
weak_ptr_factory_.GetWeakPtr()));
PickerCancelCallback observer_cancel_callback = base::BindPostTask(
device_task_runner_,
base::BindRepeating(
&NativeScreenCapturePickerMac::OnPickerObserverCancelled,
weak_ptr_factory_.GetWeakPtr()));
PickerErrorCallback observer_error_callback = base::BindPostTask(
device_task_runner_,
base::BindRepeating(
&NativeScreenCapturePickerMac::OnPickerObserverEncounteredError,
weak_ptr_factory_.GetWeakPtr()));
SCContentSharingPicker* picker = [SCContentSharingPicker sharedPicker];
if (!picker_observer_) {
picker_observer_ = [[PickerObserver alloc]
initWithPickerCallback:std::move(observer_update_callback)
cancelCallback:std::move(observer_cancel_callback)
errorCallback:std::move(observer_error_callback)];
[picker addObserver:picker_observer_];
}
std::move(created_callback).Run(active_picker_source_id_);
picker.active = true;
SCContentSharingPickerConfiguration* config = [picker defaultConfiguration];
if (base::FeatureList::IsEnabled(kAllowChangingSelectedContent)) {
config.allowsChangingSelectedContent = true;
} else {
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::Open: Show screen-sharing picker for source id = "
<< active_picker_source_id_;
LogToUma(SCContentSharingPickerOperation::kPresentScreen_Start);
} else {
config.allowedPickerModes = SCContentSharingPickerModeSingleWindow;
picker.defaultConfiguration = config;
picker.maximumStreamCount = max_stream_count;
[picker presentPickerUsingContentStyle:SCShareableContentStyleWindow];
VLOG(1) << "NSCPM::Open: Show window-sharing picker for source id = "
<< active_picker_source_id_;
LogToUma(SCContentSharingPickerOperation::kPresentWindow_Start);
}
} else {
NOTREACHED();
}
}
void NativeScreenCapturePickerMac::OnPickerObserverUpdated(
SCContentFilter* filter,
SCStream* stream) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (stream) {
auto it = stream_to_id_map_.find(stream);
if (it != stream_to_id_map_.end()) {
int source_id = it->second;
content_filters_[source_id] = filter;
VLOG(1) << "NSCPM::OnPickerObserverUpdated: "
"stream found in stream_to_id_map_ for source id "
<< source_id;
} else {
VLOG(1) << "NSCPM::OnPickerObserverUpdated: "
"stream not found in stream_to_id_map_";
}
return;
}
if (!picker_callback_) {
VLOG(1) << "NSCPM::OnPickerObserverUpdated: "
"picker_callback_ is null for source id = "
<< active_picker_source_id_;
return;
}
VLOG(1) << "NSCPM::OnPickerObserverUpdated: for source id = "
<< active_picker_source_id_;
if (!received_first_response_.contains(active_picker_source_id_)) {
received_first_response_.insert(active_picker_source_id_);
LogUpdateToUma(active_picker_type_);
}
content_filters_[active_picker_source_id_] = filter;
Source source;
source.id = active_picker_source_id_;
std::move(picker_callback_).Run(source);
}
void NativeScreenCapturePickerMac::OnPickerObserverCancelled(SCStream* stream) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (stream) {
auto it = stream_to_id_map_.find(stream);
if (it != stream_to_id_map_.end()) {
VLOG(1) << "NSCPM::OnPickerObserverCancelled: source id = " << it->second;
// TODO(https://crbug.com/409475502): Decide if we want to add logging
// here or do something else.
} else {
VLOG(1) << "NSCPM::OnPickerObserverCancelled: "
"stream not found in stream_to_id_map_";
}
return;
}
VLOG(1) << "NSCPM::OnPickerObserverCancelled: sourcce id = "
<< active_picker_source_id_;
if (!received_first_response_.contains(active_picker_source_id_)) {
received_first_response_.insert(active_picker_source_id_);
LogCancelToUma(active_picker_type_);
}
if (cancel_callback_) {
std::move(cancel_callback_).Run();
}
}
void NativeScreenCapturePickerMac::OnPickerObserverEncounteredError(
NSError* error) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
VLOG(1) << "NSCPM::OnPickerObserverEncounteredError: source id = "
<< active_picker_source_id_ << ", code = " << [error code]
<< ", domain = " << [error domain]
<< ", description = " << [error localizedDescription];
if (!received_first_response_.contains(active_picker_source_id_)) {
received_first_response_.insert(active_picker_source_id_);
LogErrorToUma(active_picker_type_);
}
if (error_callback_) {
std::move(error_callback_).Run();
}
}
void NativeScreenCapturePickerMac::UpdateStreamMap(DesktopMediaID::Id id,
SCStream* stream) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (@available(macOS 14.0, *)) {
if (!stream) {
return;
}
stream_to_id_map_[stream] = id;
VLOG(1) << "NSCPM::UpdateStreamMap: for source id = " << id;
} else {
NOTREACHED();
}
}
void NativeScreenCapturePickerMac::Close(DesktopMediaID device_id) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
if (@available(macOS 14.0, *)) {
ScheduleCleanup(device_id.id);
active_source_ids_.erase(device_id.id);
// Don't deactivate the picker if there are any active capture sessions.
if (active_source_ids_.size() == 0) {
SCContentSharingPicker* picker = [SCContentSharingPicker sharedPicker];
picker.active = false;
}
VLOG(1) << "NSCPM::Close: for source id = " << device_id.id;
} else {
NOTREACHED();
}
}
std::unique_ptr<media::VideoCaptureDevice>
NativeScreenCapturePickerMac::CreateDevice(const DesktopMediaID& source) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
cached_content_filters_cleanup_timers_.erase(source.id);
active_source_ids_.insert(source.id);
SCContentSharingPicker* picker = [SCContentSharingPicker sharedPicker];
picker.active = true;
VLOG(1) << "NSCPM::CreateDevice: source.id = " << source.id
<< ", contentFilters.count = " << content_filters_.size();
return CreateScreenCaptureKitDeviceMac(
source, content_filters_[source.id],
base::BindPostTask(
device_task_runner_,
base::BindOnce(&NativeScreenCapturePickerMac::UpdateStreamMap,
weak_ptr_factory_.GetWeakPtr())));
}
void NativeScreenCapturePickerMac::ScheduleCleanup(DesktopMediaID::Id id) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
// 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::BindPostTask(
device_task_runner_,
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) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
content_filters_.erase(id);
absl::erase_if(stream_to_id_map_, [&](const auto& stream_to_id_pair) {
return stream_to_id_pair.second == id;
});
cached_content_filters_cleanup_timers_.erase(id);
VLOG(1) << "NSCPM::CleanupContentFilter: source id = " << id
<< ", contentFilters.count = " << content_filters_.size()
<< ", stream_to_id_map_.count = " << stream_to_id_map_.size();
}
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