Compare commits

...

7 Commits

Author SHA1 Message Date
Samuel Attard
a80305df81 seems legit 2024-08-08 17:50:49 -07:00
Samuel Attard
55636eddb4 well... 2024-08-08 17:13:11 -07:00
Samuel Attard
35b9caa4ab stuff 2024-08-08 17:13:11 -07:00
Samuel Attard
ce5cce555f Apply suggestions from code review
Co-authored-by: Erick Zhao <erick@hotmail.ca>
2024-08-08 16:09:58 -07:00
Samuel Attard
3ff42dee57 oops 2024-08-08 13:38:47 -07:00
Samuel Attard
83c611977c feat: add support for system picker in setDisplayMediaRequestHandler 2024-08-08 13:18:33 -07:00
Samuel Attard
85039665f4 tmp 2024-08-08 08:39:52 -07:00
12 changed files with 261 additions and 2 deletions

View File

@@ -16,6 +16,12 @@ app.whenReady().then(() => {
const mainWindow = new BrowserWindow()
session.defaultSession.setDisplayMediaRequestHandler((request, callback) => {
// If we should use the system picker
// Note: this is currently experimental
if (desktopCapturer.isDisplayMediaSystemPickerAvailable()) {
callback({ video: desktopCapturer.systemPickerVideoSource })
return
}
desktopCapturer.getSources({ types: ['screen'] }).then((sources) => {
// Grant access to the first screen found.
callback({ video: sources[0], audio: 'loopback' })
@@ -94,6 +100,23 @@ which can detected by [`systemPreferences.getMediaAccessStatus`][].
[`navigator.mediaDevices.getUserMedia`]: https://developer.mozilla.org/en/docs/Web/API/MediaDevices/getUserMedia
[`systemPreferences.getMediaAccessStatus`]: system-preferences.md#systempreferencesgetmediaaccessstatusmediatype-windows-macos
### `desktopCapturer.isDisplayMediaSystemPickerAvailable()` _Experimental_
Returns `Boolean`, whether or not requesting desktop content via
the system picker is supported on this platform.
Currently this will only return `true` on macOS 14.4 and higher. When
true you should respect this value and use `systemPickerVideoSource` as
otherwise the OS may present scary warning dialogs to your users.
## Properties
### `desktopCapturer.systemPickerVideoSource` _Experimental_ _Readonly_
A `DesktopCapturerSource` property that should be used in conjunction with
[`session.setDisplayMediaRequestHandler`](./session.md#sessetdisplaymediarequesthandlerhandler) to use the system picker instead
of providing a specific video source from `getSources`.
## Caveats
`navigator.mediaDevices.getUserMedia` does not work on macOS for audio capture due to a fundamental limitation whereby apps that want to access the system's audio require a [signed kernel extension](https://developer.apple.com/library/archive/documentation/Security/Conceptual/System_Integrity_Protection_Guide/KernelExtensions/KernelExtensions.html). Chromium, and by extension Electron, does not provide this.

View File

@@ -270,6 +270,7 @@ filenames = {
"shell/browser/api/electron_api_debugger.h",
"shell/browser/api/electron_api_desktop_capturer.cc",
"shell/browser/api/electron_api_desktop_capturer.h",
"shell/browser/api/electron_api_desktop_capturer_mac.mm",
"shell/browser/api/electron_api_dialog.cc",
"shell/browser/api/electron_api_download_item.cc",
"shell/browser/api/electron_api_download_item.h",

View File

@@ -1,5 +1,5 @@
import { BrowserWindow } from 'electron/main';
const { createDesktopCapturer } = process._linkedBinding('electron_browser_desktop_capturer');
const { createDesktopCapturer, isDisplayMediaSystemPickerAvailable } = process._linkedBinding('electron_browser_desktop_capturer');
const deepEqual = (a: ElectronInternal.GetSourcesOptions, b: ElectronInternal.GetSourcesOptions) => JSON.stringify(a) === JSON.stringify(b);
@@ -13,6 +13,19 @@ function isValid (options: Electron.SourcesOptions) {
return Array.isArray(options?.types);
}
// Magic video source that activates the native system picker
let fakeVideoSourceId = -1;
const systemPickerVideoSource = Object.create(null);
Object.defineProperty(systemPickerVideoSource, 'id', {
get () {
return `window:${fakeVideoSourceId--}:0`;
}
});
systemPickerVideoSource.name = '';
Object.freeze(systemPickerVideoSource);
export { isDisplayMediaSystemPickerAvailable, systemPickerVideoSource };
export async function getSources (args: Electron.SourcesOptions) {
if (!isValid(args)) throw new Error('Invalid options');

View File

@@ -131,3 +131,4 @@ feat_enable_passing_exit_code_on_service_process_crash.patch
chore_remove_reference_to_chrome_browser_themes.patch
feat_enable_customizing_symbol_color_in_framecaptionbutton.patch
fix_potential_draggable_region_crash_when_no_mainframeimpl.patch
feat_allow_usage_of_sccontentsharingpicker_on_supported_platforms.patch

View File

@@ -0,0 +1,174 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Samuel Attard <marshallofsound@electronjs.org>
Date: Thu, 8 Aug 2024 08:39:10 -0700
Subject: feat: allow usage of SCContentSharingPicker on supported platforms
This is implemented as a magic "window id" that instead of pulling an SCStream manually
instead farms out to the screen picker.
diff --git a/content/browser/media/capture/screen_capture_kit_device_mac.mm b/content/browser/media/capture/screen_capture_kit_device_mac.mm
index 5c09b98b0c0ade9197a73186809ae4da28a12506..1bafa222164041b011eb0f70d72e0dc217804e9e 100644
--- a/content/browser/media/capture/screen_capture_kit_device_mac.mm
+++ b/content/browser/media/capture/screen_capture_kit_device_mac.mm
@@ -25,6 +25,57 @@
std::optional<gfx::Rect>)>;
using ErrorCallback = base::RepeatingClosure;
+API_AVAILABLE(macos(14.0))
+@interface ScreenCaptureKitPickerHelper
+ : NSObject <SCContentSharingPickerObserver>
+
+- (void)contentSharingPicker:(SCContentSharingPicker *)picker
+ didCancelForStream:(SCStream *)stream;
+
+- (void)contentSharingPicker:(SCContentSharingPicker *)picker
+ didUpdateWithFilter:(SCContentFilter *)filter
+ forStream:(SCStream *)stream;
+
+- (void)contentSharingPickerStartDidFailWithError:(NSError *)error;
+
+@end
+
+@implementation ScreenCaptureKitPickerHelper {
+ base::RepeatingCallback<void(SCContentFilter *)> _pickerCallback;
+ ErrorCallback _errorCallback;
+}
+
+- (void)contentSharingPicker:(SCContentSharingPicker *)picker
+ didCancelForStream:(SCStream *)stream {
+ // TODO: This doesn't appear to be called, we probably need
+ // to wait for apple to fix a thing before we can correctly
+ // handle cancel events
+}
+
+- (void)contentSharingPicker:(SCContentSharingPicker *)picker
+ didUpdateWithFilter:(SCContentFilter *)filter
+ forStream:(SCStream *)stream {
+ if (stream == nil) {
+ _pickerCallback.Run(filter);
+ [picker removeObserver:self];
+ }
+}
+
+- (void)contentSharingPickerStartDidFailWithError:(NSError *)error {
+ _errorCallback.Run();
+}
+
+- (instancetype)initWithStreamPickCallback:(base::RepeatingCallback<void(SCContentFilter *)>)pickerCallback
+ errorCallback:(ErrorCallback)errorCallback {
+ if (self = [super init]) {
+ _pickerCallback = pickerCallback;
+ _errorCallback = errorCallback;
+ }
+ return self;
+}
+
+@end
+
API_AVAILABLE(macos(12.3))
@interface ScreenCaptureKitDeviceHelper
: NSObject <SCStreamDelegate, SCStreamOutput>
@@ -141,7 +192,8 @@ + (SCStreamConfiguration*)streamConfigurationWithFrameSize:(gfx::Size)frameSize
class API_AVAILABLE(macos(12.3)) ScreenCaptureKitDeviceMac
: public IOSurfaceCaptureDeviceBase,
- public ScreenCaptureKitResetStreamInterface {
+ public ScreenCaptureKitResetStreamInterface
+ {
public:
explicit ScreenCaptureKitDeviceMac(const DesktopMediaID& source)
: source_(source),
@@ -157,11 +209,29 @@ explicit ScreenCaptureKitDeviceMac(const DesktopMediaID& source)
helper_ = [[ScreenCaptureKitDeviceHelper alloc]
initWithSampleCallback:sample_callback
errorCallback:error_callback];
+
+ if (@available(macOS 14.0, *)) {
+ auto picker_callback = base::BindPostTask(
+ device_task_runner_,
+ base::BindRepeating(&ScreenCaptureKitDeviceMac::OnContentFilterReady, weak_factory_.GetWeakPtr())
+ );
+ auto* picker_observer = [[ScreenCaptureKitPickerHelper alloc] initWithStreamPickCallback:picker_callback errorCallback:error_callback];
+ [[SCContentSharingPicker sharedPicker] addObserver:picker_observer];
+ }
}
ScreenCaptureKitDeviceMac(const ScreenCaptureKitDeviceMac&) = delete;
ScreenCaptureKitDeviceMac& operator=(const ScreenCaptureKitDeviceMac&) =
delete;
- ~ScreenCaptureKitDeviceMac() override = default;
+ ~ScreenCaptureKitDeviceMac() override {
+ if (@available(macOS 14.0, *)) {
+ auto* picker = [SCContentSharingPicker sharedPicker];
+ ScreenCaptureKitDeviceMac::active_streams_--;
+ picker.maximumStreamCount = @(ScreenCaptureKitDeviceMac::active_streams_);
+ if (ScreenCaptureKitDeviceMac::active_streams_ == 0 && picker.active) {
+ picker.active = false;
+ }
+ }
+ }
void OnShareableContentCreated(SCShareableContent* content) {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
@@ -225,6 +295,9 @@ void OnShareableContentCreated(SCShareableContent* content) {
return;
}
+ OnContentFilterReady(filter);
+ }
+ void OnContentFilterReady(SCContentFilter* filter) {
gfx::RectF dest_rect_in_frame;
actual_capture_format_ = capture_params().requested_format;
actual_capture_format_.pixel_format = media::PIXEL_FORMAT_NV12;
@@ -238,6 +311,7 @@ void OnShareableContentCreated(SCShareableContent* content) {
stream_ = [[SCStream alloc] initWithFilter:filter
configuration:config
delegate:helper_];
+
{
NSError* error = nil;
bool add_stream_output_result =
@@ -405,6 +479,26 @@ void OnUpdateConfigurationError() {
void OnStart() override {
DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
+ if (@available(macOS 14.0, *)) {
+ if (source_.id < 0 && source_.window_id == 0) {
+ auto* picker = [SCContentSharingPicker sharedPicker];
+ ScreenCaptureKitDeviceMac::active_streams_++;
+ picker.maximumStreamCount = @(ScreenCaptureKitDeviceMac::active_streams_);
+ if (!picker.active) {
+ picker.active = true;
+ }
+ NSMutableArray<NSNumber*>* exclude_ns_windows = [NSMutableArray array];
+ [[[[NSApplication sharedApplication] windows] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSWindow* win, NSDictionary *bindings) {
+ return [win sharingType] == NSWindowSharingNone;
+ }]] enumerateObjectsUsingBlock:^(NSWindow* win, NSUInteger idx, BOOL *stop) {
+ [exclude_ns_windows addObject:@([win windowNumber])];
+ }];
+ picker.defaultConfiguration.excludedWindowIDs = exclude_ns_windows;
+ [picker present];
+ return;
+ }
+ }
+
auto content_callback = base::BindPostTask(
device_task_runner_,
base::BindRepeating(
@@ -470,6 +564,8 @@ void ResetStreamTo(SCWindow* window) override {
}
private:
+ static int active_streams_;
+
const DesktopMediaID source_;
const scoped_refptr<base::SingleThreadTaskRunner> device_task_runner_;
@@ -498,6 +594,8 @@ void ResetStreamTo(SCWindow* window) override {
base::WeakPtrFactory<ScreenCaptureKitDeviceMac> weak_factory_{this};
};
+int ScreenCaptureKitDeviceMac::active_streams_ = 0;
+
} // namespace
std::unique_ptr<media::VideoCaptureDevice> CreateScreenCaptureKitDeviceMac(

View File

@@ -489,6 +489,13 @@ gin::Handle<DesktopCapturer> DesktopCapturer::Create(v8::Isolate* isolate) {
return handle;
}
// static
#if !BUILDFLAG(IS_MAC)
bool DesktopCapturer::IsDisplayMediaSystemPickerAvailable() {
return false;
}
#endif
gin::ObjectTemplateBuilder DesktopCapturer::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin::Wrappable<DesktopCapturer>::GetObjectTemplateBuilder(isolate)
@@ -510,6 +517,9 @@ void Initialize(v8::Local<v8::Object> exports,
gin_helper::Dictionary dict(context->GetIsolate(), exports);
dict.SetMethod("createDesktopCapturer",
&electron::api::DesktopCapturer::Create);
dict.SetMethod(
"isDisplayMediaSystemPickerAvailable",
&electron::api::DesktopCapturer::IsDisplayMediaSystemPickerAvailable);
}
} // namespace

View File

@@ -36,6 +36,8 @@ class DesktopCapturer : public gin::Wrappable<DesktopCapturer>,
static gin::Handle<DesktopCapturer> Create(v8::Isolate* isolate);
static bool IsDisplayMediaSystemPickerAvailable();
void StartHandling(bool capture_window,
bool capture_screen,
const gfx::Size& thumbnail_size,

View File

@@ -0,0 +1,17 @@
// Copyright (c) 2024 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/api/electron_api_desktop_capturer.h"
namespace electron::api {
// static
bool DesktopCapturer::IsDisplayMediaSystemPickerAvailable() {
if (@available(macOS 14.4, *)) {
return true;
}
return false;
}
} // namespace electron::api

View File

@@ -58,6 +58,11 @@ void InitializeFeatureList() {
if (platform_specific_enable_features.size() > 0) {
enable_features += std::string(",") + platform_specific_enable_features;
}
std::string platform_specific_disable_features =
DisablePlatformSpecificFeatures();
if (platform_specific_disable_features.size() > 0) {
disable_features += std::string(",") + platform_specific_disable_features;
}
base::FeatureList::InitInstance(enable_features, disable_features);
}
@@ -73,6 +78,9 @@ void InitializeFieldTrials() {
std::string EnablePlatformSpecificFeatures() {
return "";
}
std::string DisablePlatformSpecificFeatures() {
return "";
}
#endif
} // namespace electron

View File

@@ -11,6 +11,7 @@ namespace electron {
void InitializeFeatureList();
void InitializeFieldTrials();
std::string EnablePlatformSpecificFeatures();
std::string DisablePlatformSpecificFeatures();
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_FEATURE_LIST_H_

View File

@@ -31,4 +31,13 @@ std::string EnablePlatformSpecificFeatures() {
return "";
}
std::string DisablePlatformSpecificFeatures() {
if (@available(macOS 14.4, *)) {
// Required to stop timing out getDisplayMedia while waiting for
// the user to select a window with the picker
return "TimeoutHangingVideoCaptureStarts";
}
return "";
}
} // namespace electron

View File

@@ -213,7 +213,7 @@ declare namespace NodeJS {
_linkedBinding(name: 'electron_browser_app'): { app: Electron.App, App: Function };
_linkedBinding(name: 'electron_browser_auto_updater'): { autoUpdater: Electron.AutoUpdater };
_linkedBinding(name: 'electron_browser_crash_reporter'): CrashReporterBinding;
_linkedBinding(name: 'electron_browser_desktop_capturer'): { createDesktopCapturer(): ElectronInternal.DesktopCapturer; };
_linkedBinding(name: 'electron_browser_desktop_capturer'): { createDesktopCapturer(): ElectronInternal.DesktopCapturer; isDisplayMediaSystemPickerAvailable(): boolean; };
_linkedBinding(name: 'electron_browser_event_emitter'): { setEventEmitterPrototype(prototype: Object): void; };
_linkedBinding(name: 'electron_browser_global_shortcut'): { globalShortcut: Electron.GlobalShortcut };
_linkedBinding(name: 'electron_browser_image_view'): { ImageView: any };