Browse Source

feat: add support for system picker in setDisplayMediaRequestHandler

Samuel Attard 8 months ago
parent
commit
83c611977c

+ 23 - 0
docs/api/desktop-capturer.md

@@ -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 supporter 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
+`sesssion.setDisplayMediaRequestHandler` 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.

+ 1 - 0
filenames.gni

@@ -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",

+ 8 - 1
lib/browser/api/desktop-capturer.ts

@@ -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,13 @@ function isValid (options: Electron.SourcesOptions) {
   return Array.isArray(options?.types);
 }
 
+const systemPickerVideoSource = Object.create(null);
+systemPickerVideoSource.id = 'window:0:0';
+systemPickerVideoSource.name = '';
+Object.freeze(systemPickerVideoSource);
+
+export { isDisplayMediaSystemPickerAvailable, systemPickerVideoSource };
+
 export async function getSources (args: Electron.SourcesOptions) {
   if (!isValid(args)) throw new Error('Invalid options');
 

+ 1 - 1
patches/chromium/.patches

@@ -131,4 +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
-tmp.patch
+feat_allow_usage_of_sccontentsharingpicker_on_supported_platforms.patch

+ 54 - 52
patches/chromium/tmp.patch → patches/chromium/feat_allow_usage_of_sccontentsharingpicker_on_supported_platforms.patch

@@ -1,14 +1,16 @@
 From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 From: Samuel Attard <[email protected]>
 Date: Thu, 8 Aug 2024 08:39:10 -0700
-Subject: tmp
+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..26b8eac4353395da6ebe50e3a7edf51a3ec6407b 100644
+index 5c09b98b0c0ade9197a73186809ae4da28a12506..6622c39ce6b7750883cd1d0720a580fb1d876d94 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,49 @@
+@@ -25,6 +25,52 @@
                                                      std::optional<gfx::Rect>)>;
  using ErrorCallback = base::RepeatingClosure;
  
@@ -16,11 +18,11 @@ index 5c09b98b0c0ade9197a73186809ae4da28a12506..26b8eac4353395da6ebe50e3a7edf51a
 +@interface ScreenCaptureKitPickerHelper
 +    : NSObject <SCContentSharingPickerObserver>
 +
-+- (void)contentSharingPicker:(SCContentSharingPicker *)picker 
++- (void)contentSharingPicker:(SCContentSharingPicker *)picker
 +          didCancelForStream:(SCStream *)stream;
 +
-+- (void)contentSharingPicker:(SCContentSharingPicker *)picker 
-+         didUpdateWithFilter:(SCContentFilter *)filter 
++- (void)contentSharingPicker:(SCContentSharingPicker *)picker
++         didUpdateWithFilter:(SCContentFilter *)filter
 +                   forStream:(SCStream *)stream;
 +
 +- (void)contentSharingPickerStartDidFailWithError:(NSError *)error;
@@ -28,27 +30,30 @@ index 5c09b98b0c0ade9197a73186809ae4da28a12506..26b8eac4353395da6ebe50e3a7edf51a
 +@end
 +
 +@implementation ScreenCaptureKitPickerHelper {
-+  base::RepeatingCallback<void(SCStream *)> _pickerCallback;
++  base::RepeatingCallback<void(SCContentFilter *)> _pickerCallback;
++  ErrorCallback _errorCallback;
 +}
 +
-+- (void)contentSharingPicker:(SCContentSharingPicker *)picker 
++- (void)contentSharingPicker:(SCContentSharingPicker *)picker
 +          didCancelForStream:(SCStream *)stream {
-+
++  // This is handled elsewhere in a stream cancel observer
 +}
 +
-+- (void)contentSharingPicker:(SCContentSharingPicker *)picker 
-+         didUpdateWithFilter:(SCContentFilter *)filter 
++- (void)contentSharingPicker:(SCContentSharingPicker *)picker
++         didUpdateWithFilter:(SCContentFilter *)filter
 +                   forStream:(SCStream *)stream {
-+  _pickerCallback.Run(stream);
++  _pickerCallback.Run(filter);
 +}
 +
 +- (void)contentSharingPickerStartDidFailWithError:(NSError *)error {
-+
++  _errorCallback.Run();
 +}
 +
-+- (instancetype)initWithStreamPickCallback:(base::RepeatingCallback<void(SCStream *)>)pickerCallback {
++- (instancetype)initWithStreamPickCallback:(base::RepeatingCallback<void(SCContentFilter *)>)pickerCallback
++                             errorCallback:(ErrorCallback)errorCallback {
 +  if (self = [super init]) {
 +    _pickerCallback = pickerCallback;
++    _errorCallback = errorCallback;
 +  }
 +  return self;
 +}
@@ -58,7 +63,7 @@ index 5c09b98b0c0ade9197a73186809ae4da28a12506..26b8eac4353395da6ebe50e3a7edf51a
  API_AVAILABLE(macos(12.3))
  @interface ScreenCaptureKitDeviceHelper
      : NSObject <SCStreamDelegate, SCStreamOutput>
-@@ -141,7 +184,8 @@ + (SCStreamConfiguration*)streamConfigurationWithFrameSize:(gfx::Size)frameSize
+@@ -141,7 +187,8 @@ + (SCStreamConfiguration*)streamConfigurationWithFrameSize:(gfx::Size)frameSize
  
  class API_AVAILABLE(macos(12.3)) ScreenCaptureKitDeviceMac
      : public IOSurfaceCaptureDeviceBase,
@@ -68,7 +73,7 @@ index 5c09b98b0c0ade9197a73186809ae4da28a12506..26b8eac4353395da6ebe50e3a7edf51a
   public:
    explicit ScreenCaptureKitDeviceMac(const DesktopMediaID& source)
        : source_(source),
-@@ -157,6 +201,15 @@ explicit ScreenCaptureKitDeviceMac(const DesktopMediaID& source)
+@@ -157,6 +204,15 @@ explicit ScreenCaptureKitDeviceMac(const DesktopMediaID& source)
      helper_ = [[ScreenCaptureKitDeviceHelper alloc]
          initWithSampleCallback:sample_callback
                   errorCallback:error_callback];
@@ -76,7 +81,7 @@ index 5c09b98b0c0ade9197a73186809ae4da28a12506..26b8eac4353395da6ebe50e3a7edf51a
 +    if (@available(macOS 14.0, *)) {
 +      auto picker_callback = base::BindPostTask(
 +        device_task_runner_,
-+        base::BindRepeating(&ScreenCaptureKitDeviceMac::OnStreamPicked, weak_factory_.GetWeakPtr())
++        base::BindRepeating(&ScreenCaptureKitDeviceMac::OnContentFilterReady, weak_factory_.GetWeakPtr())
 +      );
 +      auto* picker_observer = [[ScreenCaptureKitPickerHelper alloc] initWithStreamPickCallback:picker_callback];
 +      [[SCContentSharingPicker sharedPicker] addObserver:picker_observer];
@@ -84,49 +89,46 @@ index 5c09b98b0c0ade9197a73186809ae4da28a12506..26b8eac4353395da6ebe50e3a7edf51a
    }
    ScreenCaptureKitDeviceMac(const ScreenCaptureKitDeviceMac&) = delete;
    ScreenCaptureKitDeviceMac& operator=(const ScreenCaptureKitDeviceMac&) =
-@@ -238,6 +291,13 @@ void OnShareableContentCreated(SCShareableContent* content) {
+@@ -225,6 +281,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 +297,7 @@ void OnShareableContentCreated(SCShareableContent* content) {
      stream_ = [[SCStream alloc] initWithFilter:filter
                                   configuration:config
                                        delegate:helper_];
-+    OnStreamCreated();
-+  }
-+  void OnStreamPicked(SCStream* stream) {
-+    stream_ = stream;
-+    OnStreamCreated();
-+  }
-+  void OnStreamCreated() {
++
      {
        NSError* error = nil;
        bool add_stream_output_result =
-@@ -405,15 +465,22 @@ void OnUpdateConfigurationError() {
+@@ -405,6 +465,24 @@ void OnUpdateConfigurationError() {
    void OnStart() override {
      DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
  
--    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];
-+    // 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);
-+    // };
-+    // TODO: Use setConfiguration and excludedWindowIDs
 +    if (@available(macOS 14.0, *)) {
-+      LOG(ERROR) << "Presenting picker";
-+      auto* picker = [SCContentSharingPicker sharedPicker];
-+      picker.active = true;
-+      [picker present];
++      if (source_.id == 0 && source_.window_id == 0) {
++        auto* picker = [SCContentSharingPicker sharedPicker];
++        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;
++      }
 +    }
-+    // [SCShareableContent getShareableContentWithCompletionHandler:handler];
-   }
-   void OnStop() override {
-     DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
++
+     auto content_callback = base::BindPostTask(
+         device_task_runner_,
+         base::BindRepeating(

+ 10 - 0
shell/browser/api/electron_api_desktop_capturer.cc

@@ -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

+ 2 - 0
shell/browser/api/electron_api_desktop_capturer.h

@@ -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,

+ 17 - 0
shell/browser/api/electron_api_desktop_capturer_mac.mm

@@ -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

+ 8 - 0
shell/browser/feature_list.cc

@@ -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

+ 1 - 0
shell/browser/feature_list.h

@@ -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_

+ 9 - 0
shell/browser/feature_list_mac.mm

@@ -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

+ 1 - 1
typings/internal-ambient.d.ts

@@ -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 };