Browse Source

feat: add support for system picker in setDisplayMediaRequestHandler (#43581)

* tmp

* feat: add support for system picker in setDisplayMediaRequestHandler

* oops

* Apply suggestions from code review

Co-authored-by: Erick Zhao <[email protected]>

* stuff

* well...

* seems legit

* chore: update patch to handle screenCapturer

* feat: modify API to use useSystemPicker

* fix: gate ScreenCaptureKitPicker to macos 15 or higher

* fix: don't use native picker with legacy media selection

* chore: code review, boolean set & docs update

* fix: add cancelCallback

* docs: clarify session & desktopCapturer docs

---------

Co-authored-by: Samuel Attard <[email protected]>
Co-authored-by: Samuel Attard <[email protected]>
Co-authored-by: Erick Zhao <[email protected]>
Keeley Hammond 7 months ago
parent
commit
309d5dade3

+ 5 - 1
docs/api/desktop-capturer.md

@@ -20,7 +20,11 @@ app.whenReady().then(() => {
       // Grant access to the first screen found.
       callback({ video: sources[0], audio: 'loopback' })
     })
-  })
+    // If true, use the system picker if available.
+    // Note: this is currently experimental. If the system picker
+    // is available, it will be used and the media request handler
+    // will not be invoked.
+  }, { useSystemPicker: true })
 
   mainWindow.loadFile('index.html')
 })

+ 12 - 2
docs/api/session.md

@@ -953,7 +953,7 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents,
 })
 ```
 
-#### `ses.setDisplayMediaRequestHandler(handler)`
+#### `ses.setDisplayMediaRequestHandler(handler[, opts])`
 
 * `handler` Function | null
   * `request` Object
@@ -980,12 +980,18 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents,
          and this is set to `true`, then local playback of audio will not be muted (e.g. using `MediaRecorder`
          to record `WebFrameMain` with this flag set to `true` will allow audio to pass through to the speakers
          while recording). Default is `false`.
+* `opts` Object (optional) _macOS_ _Experimental_
+  * `useSystemPicker` Boolean - true if the available native system picker should be used. Default is `false`. _macOS_ _Experimental_
 
 This handler will be called when web content requests access to display media
 via the `navigator.mediaDevices.getDisplayMedia` API. Use the
 [desktopCapturer](desktop-capturer.md) API to choose which stream(s) to grant
 access to.
 
+`useSystemPicker` allows an application to use the system picker instead of providing a specific video source from `getSources`.
+This option is experimental, and currently available for MacOS 15+ only. If the system picker is available and `useSystemPicker`
+is set to `true`, the handler will not be invoked.
+
 ```js
 const { session, desktopCapturer } = require('electron')
 
@@ -994,7 +1000,11 @@ session.defaultSession.setDisplayMediaRequestHandler((request, callback) => {
     // Grant access to the first screen found.
     callback({ video: sources[0] })
   })
-})
+  // Use the system picker if available.
+  // Note: this is currently experimental. If the system picker
+  // is available, it will be used and the media request handler
+  // will not be invoked.
+}, { useSystemPicker: true })
 ```
 
 Passing a [WebFrameMain](web-frame-main.md) object as a video or audio stream

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

+ 3 - 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,8 @@ function isValid (options: Electron.SourcesOptions) {
   return Array.isArray(options?.types);
 }
 
+export { isDisplayMediaSystemPickerAvailable };
+
 export async function getSources (args: Electron.SourcesOptions) {
   if (!isValid(args)) throw new Error('Invalid options');
 

+ 26 - 0
lib/browser/api/session.ts

@@ -1,11 +1,37 @@
 import { fetchWithSession } from '@electron/internal/browser/api/net-fetch';
 import { net } from 'electron/main';
 const { fromPartition, fromPath, Session } = process._linkedBinding('electron_browser_session');
+const { isDisplayMediaSystemPickerAvailable } = process._linkedBinding('electron_browser_desktop_capturer');
+
+// Fake video source that activates the native system picker
+// This is used to get around the need for a screen/window
+// id in Chrome's desktopCapturer.
+let fakeVideoSourceId = -1;
+const systemPickerVideoSource = Object.create(null);
+Object.defineProperty(systemPickerVideoSource, 'id', {
+  get () {
+    return `window:${fakeVideoSourceId--}:0`;
+  }
+});
+systemPickerVideoSource.name = '';
+Object.freeze(systemPickerVideoSource);
 
 Session.prototype.fetch = function (input: RequestInfo, init?: RequestInit) {
   return fetchWithSession(input, init, this, net.request);
 };
 
+Session.prototype.setDisplayMediaRequestHandler = function (handler, opts) {
+  if (!handler) return this._setDisplayMediaRequestHandler(handler, opts);
+
+  this._setDisplayMediaRequestHandler(async (req, callback) => {
+    if (opts && opts.useSystemPicker && isDisplayMediaSystemPickerAvailable()) {
+      return callback({ video: systemPickerVideoSource });
+    }
+
+    return handler(req, callback);
+  }, opts);
+};
+
 export default {
   fromPartition,
   fromPath,

+ 1 - 0
patches/chromium/.patches

@@ -130,3 +130,4 @@ chore_remove_reference_to_chrome_browser_themes.patch
 feat_enable_customizing_symbol_color_in_framecaptionbutton.patch
 build_expose_webplugininfo_interface_to_electron.patch
 osr_shared_texture_remove_keyed_mutex_on_win_dxgi.patch
+feat_allow_usage_of_sccontentsharingpicker_on_supported_platforms.patch

+ 331 - 0
patches/chromium/feat_allow_usage_of_sccontentsharingpicker_on_supported_platforms.patch

@@ -0,0 +1,331 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Samuel Attard <[email protected]>
+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/desktop_capture_device_mac.cc b/content/browser/media/capture/desktop_capture_device_mac.cc
+index 88c56f4dfcc1f8517ef1e8b6f1d37f5ba4d0b2c7..a75493a6d4d8ce8340a2d820eff5eed4e6a95109 100644
+--- a/content/browser/media/capture/desktop_capture_device_mac.cc
++++ b/content/browser/media/capture/desktop_capture_device_mac.cc
+@@ -28,7 +28,7 @@ class DesktopCaptureDeviceMac : public IOSurfaceCaptureDeviceBase {
+   ~DesktopCaptureDeviceMac() override = default;
+ 
+   // IOSurfaceCaptureDeviceBase:
+-  void OnStart() override {
++  void OnStart(std::optional<bool> use_native_picker) override {
+     requested_format_ = capture_params().requested_format;
+     requested_format_.pixel_format = media::PIXEL_FORMAT_NV12;
+     DCHECK_GT(requested_format_.frame_size.GetArea(), 0);
+diff --git a/content/browser/media/capture/io_surface_capture_device_base_mac.cc b/content/browser/media/capture/io_surface_capture_device_base_mac.cc
+index 8a774911ce0f610b2c993976d108f840696c1d02..5ead7287e2d765d043f8b9c0229a2ee825d9f544 100644
+--- a/content/browser/media/capture/io_surface_capture_device_base_mac.cc
++++ b/content/browser/media/capture/io_surface_capture_device_base_mac.cc
+@@ -20,7 +20,7 @@ void IOSurfaceCaptureDeviceBase::AllocateAndStart(
+   client_ = std::move(client);
+   capture_params_ = params;
+ 
+-  OnStart();
++  OnStart(params.use_native_picker);
+ }
+ 
+ void IOSurfaceCaptureDeviceBase::StopAndDeAllocate() {
+diff --git a/content/browser/media/capture/io_surface_capture_device_base_mac.h b/content/browser/media/capture/io_surface_capture_device_base_mac.h
+index 8ac12480f663a74dfbdcf7128a582a81b4474d25..db6802a2603e1d3c3039e49737438124bf2ee1f1 100644
+--- a/content/browser/media/capture/io_surface_capture_device_base_mac.h
++++ b/content/browser/media/capture/io_surface_capture_device_base_mac.h
+@@ -25,7 +25,7 @@ class CONTENT_EXPORT IOSurfaceCaptureDeviceBase
+   ~IOSurfaceCaptureDeviceBase() override;
+ 
+   // OnStart is called by AllocateAndStart.
+-  virtual void OnStart() = 0;
++  virtual void OnStart(std::optional<bool> use_native_picker) = 0;
+ 
+   // OnStop is called by StopAndDeAllocate.
+   virtual void OnStop() = 0;
+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 b6129282c6807702cf88e0a3e2ba233e41a20960..1c2d0c6dd4101fe0bac69e3018bbbedadce224cc 100644
+--- a/content/browser/media/capture/screen_capture_kit_device_mac.mm
++++ b/content/browser/media/capture/screen_capture_kit_device_mac.mm
+@@ -24,24 +24,83 @@
+                                                     std::optional<gfx::Size>,
+                                                     std::optional<gfx::Rect>)>;
+ using ErrorCallback = base::RepeatingClosure;
++using CancelCallback = base::RepeatingClosure;
++
++API_AVAILABLE(macos(15.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;
++  CancelCallback _cancelCallback;
++}
++
++- (void)contentSharingPicker:(SCContentSharingPicker *)picker
++          didCancelForStream:(SCStream *)stream {
++  // TODO: This doesn't appear to be called on Apple's side;
++  // implement this logic
++  _cancelCallback.Run();
++}
++
++- (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
++                             cancelCallback:(CancelCallback)cancelCallback
++                             errorCallback:(ErrorCallback)errorCallback {
++  if (self = [super init]) {
++    _pickerCallback = pickerCallback;
++    _cancelCallback = cancelCallback;
++    _errorCallback = errorCallback;
++  }
++  return self;
++}
++
++@end
+ 
+ API_AVAILABLE(macos(12.3))
+ @interface ScreenCaptureKitDeviceHelper
+     : NSObject <SCStreamDelegate, SCStreamOutput>
+ 
+ - (instancetype)initWithSampleCallback:(SampleCallback)sampleCallback
++                         cancelCallback:(CancelCallback)cancelCallback
+                          errorCallback:(ErrorCallback)errorCallback;
+ @end
+ 
+ @implementation ScreenCaptureKitDeviceHelper {
+   SampleCallback _sampleCallback;
++  CancelCallback _cancelCallback;
+   ErrorCallback _errorCallback;
+ }
+ 
+ - (instancetype)initWithSampleCallback:(SampleCallback)sampleCallback
++                         cancelCallback:(CancelCallback)cancelCallback
+                          errorCallback:(ErrorCallback)errorCallback {
+   if (self = [super init]) {
+     _sampleCallback = sampleCallback;
++    _cancelCallback = cancelCallback;
+     _errorCallback = errorCallback;
+   }
+   return self;
+@@ -141,7 +200,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,
+                                      SCContentFilter* filter)
+@@ -152,18 +212,41 @@ explicit ScreenCaptureKitDeviceMac(const DesktopMediaID& source,
+         device_task_runner_,
+         base::BindRepeating(&ScreenCaptureKitDeviceMac::OnStreamSample,
+                             weak_factory_.GetWeakPtr()));
++    CancelCallback cancel_callback = base::BindPostTask(
++        device_task_runner_,
++        base::BindRepeating(&ScreenCaptureKitDeviceMac::OnStreamError,
++                            weak_factory_.GetWeakPtr()));
+     ErrorCallback error_callback = base::BindPostTask(
+         device_task_runner_,
+         base::BindRepeating(&ScreenCaptureKitDeviceMac::OnStreamError,
+                             weak_factory_.GetWeakPtr()));
+     helper_ = [[ScreenCaptureKitDeviceHelper alloc]
+         initWithSampleCallback:sample_callback
++                 cancelCallback:cancel_callback
+                  errorCallback:error_callback];
++
++    if (@available(macOS 15.0, *)) {
++      auto picker_callback = base::BindPostTask(
++        device_task_runner_,
++        base::BindRepeating(&ScreenCaptureKitDeviceMac::OnContentFilterReady, weak_factory_.GetWeakPtr())
++      );
++      auto* picker_observer = [[ScreenCaptureKitPickerHelper alloc] initWithStreamPickCallback:picker_callback cancelCallback:cancel_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 15.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());
+@@ -232,7 +315,7 @@ void CreateStream(SCContentFilter* filter) {
+       return;
+     }
+ 
+-    if (@available(macOS 14.0, *)) {
++    if (@available(macOS 15.0, *)) {
+       // Update the content size. This step is neccessary when used together
+       // with SCContentSharingPicker. If the Chrome picker is used, it will
+       // change to retina resolution if applicable.
+@@ -241,6 +324,9 @@ void CreateStream(SCContentFilter* filter) {
+                     filter.contentRect.size.height * filter.pointPixelScale);
+     }
+ 
++    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;
+@@ -254,6 +340,7 @@ void CreateStream(SCContentFilter* filter) {
+     stream_ = [[SCStream alloc] initWithFilter:filter
+                                  configuration:config
+                                       delegate:helper_];
++
+     {
+       NSError* error = nil;
+       bool add_stream_output_result =
+@@ -395,7 +482,7 @@ void OnStreamError() {
+       if (fullscreen_module_) {
+         fullscreen_module_->Reset();
+       }
+-      OnStart();
++      OnStart(std::nullopt);
+     } else {
+       client()->OnError(media::VideoCaptureError::kScreenCaptureKitStreamError,
+                         FROM_HERE, "Stream delegate called didStopWithError");
+@@ -418,23 +505,39 @@ void OnUpdateConfigurationError() {
+   }
+ 
+   // IOSurfaceCaptureDeviceBase:
+-  void OnStart() override {
++  void OnStart(std::optional<bool> use_native_picker) override {
+     DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
+-    if (filter_) {
+-      // SCContentSharingPicker is used where filter_ is set on creation.
+-      CreateStream(filter_);
+-    } else {
+-      // Chrome picker is used.
+-      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];
++
++    if (@available(macOS 15.0, *)) {
++      constexpr bool DefaultUseNativePicker = true;
++      if (use_native_picker.value_or(DefaultUseNativePicker) && 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(
++            &ScreenCaptureKitDeviceMac::OnShareableContentCreated,
++            weak_factory_.GetWeakPtr()));
++    auto handler = ^(SCShareableContent* content, NSError* error) {
++      content_callback.Run(content);
++    };
++    [SCShareableContent getShareableContentWithCompletionHandler:handler];
+   }
+   void OnStop() override {
+     DCHECK(device_task_runner_->RunsTasksInCurrentSequence());
+@@ -492,6 +595,8 @@ void ResetStreamTo(SCWindow* window) override {
+   }
+ 
+  private:
++  static int active_streams_;
++
+   const DesktopMediaID source_;
+   SCContentFilter* const filter_;
+   const scoped_refptr<base::SingleThreadTaskRunner> device_task_runner_;
+@@ -521,6 +626,8 @@ void ResetStreamTo(SCWindow* window) override {
+   base::WeakPtrFactory<ScreenCaptureKitDeviceMac> weak_factory_{this};
+ };
+ 
++int ScreenCaptureKitDeviceMac::active_streams_ = 0;
++
+ }  // namespace
+ 
+ // Although ScreenCaptureKit is available in 12.3 there were some bugs that
+diff --git a/content/browser/renderer_host/media/in_process_video_capture_device_launcher.cc b/content/browser/renderer_host/media/in_process_video_capture_device_launcher.cc
+index 7adf8264cfa9980c4a8414bf0f8bfa9ad70ec0b3..d162612dc70a2b57190aaf558aca8f46cbdedcad 100644
+--- a/content/browser/renderer_host/media/in_process_video_capture_device_launcher.cc
++++ b/content/browser/renderer_host/media/in_process_video_capture_device_launcher.cc
+@@ -360,13 +360,15 @@ void InProcessVideoCaptureDeviceLauncher::LaunchDeviceAsync(
+           std::move(after_start_capture_callback));
+       break;
+ #else
++      media::VideoCaptureParams updated_params = params;
++      updated_params.use_native_picker = stream_type != blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE;
+       // All cases other than tab capture or Aura desktop/window capture.
+       TRACE_EVENT_INSTANT0(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
+                            "UsingDesktopCapturer", TRACE_EVENT_SCOPE_THREAD);
+       start_capture_closure = base::BindOnce(
+           &InProcessVideoCaptureDeviceLauncher::
+               DoStartDesktopCaptureOnDeviceThread,
+-          base::Unretained(this), desktop_id, params,
++          base::Unretained(this), desktop_id, updated_params,
+           CreateDeviceClient(media::VideoCaptureBufferType::kSharedMemory,
+                              kMaxNumberOfBuffers, std::move(receiver),
+                              std::move(receiver_on_io_thread)),
+diff --git a/media/capture/video_capture_types.h b/media/capture/video_capture_types.h
+index f2b75f5b2f547ad135c1288bf3639b26dedc8053..ef18724d9f2ea68a47b66fc3981f58a73ac1b51d 100644
+--- a/media/capture/video_capture_types.h
++++ b/media/capture/video_capture_types.h
+@@ -355,6 +355,8 @@ struct CAPTURE_EXPORT VideoCaptureParams {
+   // Flag indicating whether HiDPI mode should be enabled for tab capture
+   // sessions.
+   bool is_high_dpi_enabled = true;
++
++  std::optional<bool> use_native_picker;
+ };
+ 
+ CAPTURE_EXPORT std::ostream& operator<<(

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

@@ -503,6 +503,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)
@@ -524,6 +531,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 final : 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 15.0, *)) {
+    return true;
+  }
+  return false;
+}
+
+}  // namespace electron::api

+ 1 - 1
shell/browser/api/electron_api_session.cc

@@ -1607,7 +1607,7 @@ void Session::FillObjectTemplate(v8::Isolate* isolate,
                  &Session::SetPermissionRequestHandler)
       .SetMethod("setPermissionCheckHandler",
                  &Session::SetPermissionCheckHandler)
-      .SetMethod("setDisplayMediaRequestHandler",
+      .SetMethod("_setDisplayMediaRequestHandler",
                  &Session::SetDisplayMediaRequestHandler)
       .SetMethod("setDevicePermissionHandler",
                  &Session::SetDevicePermissionHandler)

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

+ 4 - 0
typings/internal-electron.d.ts

@@ -127,6 +127,10 @@ declare namespace Electron {
     type?: 'backgroundPage' | 'window' | 'browserView' | 'remote' | 'webview' | 'offscreen';
   }
 
+  interface Session {
+    _setDisplayMediaRequestHandler: Electron.Session['setDisplayMediaRequestHandler'];
+  }
+
   type CreateWindowFunction = (options: BrowserWindowConstructorOptions) => WebContents;
 
   interface Menu {