Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: session.setDisplayMediaRequestHandler #30702

Merged
merged 40 commits into from Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c599fc6
feat: session.setMediaRequestHandler
nornagon Aug 25, 2021
72fd6aa
address comments
nornagon Aug 26, 2021
5373a64
iwyu
nornagon Aug 28, 2021
fda34cf
Merge remote-tracking branch 'origin/main' into media-request-handler
nornagon Sep 2, 2021
31c10b6
review, bump typescript-definitions
nornagon Sep 2, 2021
d4edceb
fix some stuff
nornagon Sep 3, 2021
d474408
Merge remote-tracking branch 'origin/main' into media-request-handler
nornagon Jun 15, 2022
68f0bef
update for changes to media stream api
nornagon Jun 16, 2022
fc36ba4
include patches
nornagon Jun 20, 2022
3b87eb4
chore: update patches
patchup[bot] Jun 20, 2022
8cf7385
Merge branch 'main' into media-request-handler
nornagon Jun 20, 2022
4479e80
Merge remote-tracking branch 'origin/main' into media-request-handler
nornagon Jun 29, 2022
0ae0dc7
lint
nornagon Jun 29, 2022
36d405c
remove patches applied upstream
nornagon Jun 29, 2022
997a8dc
fix when no media handler installed
nornagon Jun 30, 2022
ecdc1ea
add getVideoCaptureDevices so gUM requests can be served
nornagon Jun 30, 2022
ba8c9d9
remove PEPPER_ONLY
nornagon Jun 30, 2022
057f4ee
camelCase
nornagon Jun 30, 2022
6dcde55
comments, tests
nornagon Jun 30, 2022
3801884
Merge remote-tracking branch 'origin/main' into media-request-handler
nornagon Jul 20, 2022
40a6674
new api
nornagon Jul 21, 2022
6ddbad4
lint
nornagon Jul 21, 2022
e6173c8
remove log
nornagon Jul 21, 2022
a6e8011
simplify request object
nornagon Jul 21, 2022
76834b0
setMediaRequestHandler => setDisplayMediaRequestHandler
nornagon Jul 21, 2022
ae328ba
remove unneeded code
nornagon Jul 21, 2022
856f3ae
allow passing WebFrameMain
nornagon Jul 21, 2022
93d2940
Update electron_api_desktop_capturer.cc
nornagon Jul 21, 2022
11e085c
Update electron_api_desktop_capturer.cc
nornagon Jul 21, 2022
a501c95
Update shell/browser/electron_browser_context.cc
nornagon Jul 25, 2022
5d2d7e0
review feedback
nornagon Jul 25, 2022
a745f3b
throw an error when setting display media request handler to an inval…
nornagon Jul 25, 2022
c147347
disable local echo for WebFrameMain audio streams
nornagon Jul 25, 2022
1ba104e
id/name on video streams come from DesktopCapturerSource
nornagon Jul 25, 2022
70ba06d
fix various crash cases and add better errors, improve docs
nornagon Jul 25, 2022
aead701
Merge remote-tracking branch 'origin/main' into media-request-handler
nornagon Aug 5, 2022
5269fba
Merge remote-tracking branch 'origin/main' into media-request-handler
nornagon Aug 15, 2022
716f79a
try skipping when screen source length is 0..?
nornagon Aug 15, 2022
f005525
skip screen tests on macOS / x64
nornagon Aug 16, 2022
8c8a464
Update docs/api/session.md
nornagon Aug 17, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 54 additions & 0 deletions docs/api/session.md
Expand Up @@ -698,6 +698,60 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents,
})
```

#### `ses.setDisplayMediaRequestHandler(handler)`

* `handler` Function | null
* `request` Object
* `frame` [WebFrameMain](web-frame-main.md) - frame that is requesting access to media.
* `securityOrigin` String - origin of the page making the request.
* `videoRequested` Boolean - true if the web content requested a video stream.
* `audioRequested` Boolean - true if the web content requested an audio stream.
* `userGesture` Boolean - whether a user gesture was active when this request was triggered.
* `callback` Function
* `streams` Object
* `video` Object | [WebFrameMain](web-frame-main.md) (optional)
* `id` String - the id of the stream being granted. This will usually
come from a [DesktopCapturerSource](structures/desktop-capturer-source.md)
object.
* `name` String - the name of the stream being granted. This will
usually come from a [DesktopCapturerSource](structures/desktop-capturer-source.md)
object.
nornagon marked this conversation as resolved.
Show resolved Hide resolved
* `audio` String | [WebFrameMain](web-frame-main.md) (optional) - If
a string is specified, can be `loopback` or `loopbackWithMute`.
Specifying a loopback device will capture system audio, and is
currently only supported on Windows. If a WebFrameMain is specified,
will capture audio from that frame.

This handler will be called when web content requests access to display media
nornagon marked this conversation as resolved.
Show resolved Hide resolved
via the `navigator.mediaDevices.getDisplayMedia` API. Use the
[desktopCapturer](desktop-capturer.md) API to choose which stream(s) to grant
access to.

```javascript
const { session, desktopCapturer } = require('electron')

session.defaultSession.setDisplayMediaRequestHandler((request, callback) => {
desktopCapturer.getSources({ types: ['screen'] }).then((sources) => {
// Grant access to the first screen found.
callback({ video: sources[0] })
})
})
```

Passing a [WebFrameMain](web-frame-main.md) object as a video or audio stream
will capture the video or audio stream from that frame.

```javascript
const { session } = require('electron')

session.defaultSession.setDisplayMediaRequestHandler((request, callback) => {
// Allow the tab to capture itself.
callback({ video: request.frame })
})
```

Passing `null` instead of a function resets the handler to its default state.

#### `ses.setDevicePermissionHandler(handler)`

* `handler` Function\<boolean> | null
Expand Down
2 changes: 2 additions & 0 deletions filenames.gni
Expand Up @@ -577,6 +577,8 @@ filenames = {
"shell/common/gin_converters/hid_device_info_converter.h",
"shell/common/gin_converters/image_converter.cc",
"shell/common/gin_converters/image_converter.h",
"shell/common/gin_converters/media_converter.cc",
"shell/common/gin_converters/media_converter.h",
"shell/common/gin_converters/message_box_converter.cc",
"shell/common/gin_converters/message_box_converter.h",
"shell/common/gin_converters/native_window_converter.h",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -6,7 +6,7 @@
"devDependencies": {
"@azure/storage-blob": "^12.9.0",
"@electron/docs-parser": "^0.12.4",
"@electron/typescript-definitions": "^8.9.5",
"@electron/typescript-definitions": "^8.9.6",
"@octokit/auth-app": "^2.10.0",
"@octokit/rest": "^18.0.3",
"@primer/octicons": "^10.0.0",
Expand Down
21 changes: 21 additions & 0 deletions shell/browser/api/electron_api_session.cc
Expand Up @@ -52,6 +52,7 @@
#include "shell/browser/api/electron_api_net_log.h"
#include "shell/browser/api/electron_api_protocol.h"
#include "shell/browser/api/electron_api_service_worker_context.h"
#include "shell/browser/api/electron_api_web_frame_main.h"
#include "shell/browser/api/electron_api_web_request.h"
#include "shell/browser/browser.h"
#include "shell/browser/electron_browser_context.h"
Expand All @@ -65,6 +66,7 @@
#include "shell/common/gin_converters/content_converter.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/media_converter.h"
#include "shell/common/gin_converters/net_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
Expand All @@ -73,6 +75,7 @@
#include "shell/common/options_switches.h"
#include "shell/common/process_util.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
#include "ui/base/l10n/l10n_util.h"

#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
Expand Down Expand Up @@ -643,6 +646,22 @@ void Session::SetPermissionCheckHandler(v8::Local<v8::Value> val,
permission_manager->SetPermissionCheckHandler(handler);
}

void Session::SetDisplayMediaRequestHandler(v8::Isolate* isolate,
v8::Local<v8::Value> val) {
if (val->IsNull()) {
browser_context_->SetDisplayMediaRequestHandler(
DisplayMediaRequestHandler());
return;
}
DisplayMediaRequestHandler handler;
if (!gin::ConvertFromV8(isolate, val, &handler)) {
gin_helper::ErrorThrower(isolate).ThrowTypeError(
"Display media request handler must be null or a function");
return;
nornagon marked this conversation as resolved.
Show resolved Hide resolved
}
browser_context_->SetDisplayMediaRequestHandler(handler);
}

void Session::SetDevicePermissionHandler(v8::Local<v8::Value> val,
gin::Arguments* args) {
ElectronPermissionManager::DeviceCheckHandler handler;
Expand Down Expand Up @@ -1198,6 +1217,8 @@ gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
&Session::SetPermissionRequestHandler)
.SetMethod("setPermissionCheckHandler",
&Session::SetPermissionCheckHandler)
.SetMethod("setDisplayMediaRequestHandler",
&Session::SetDisplayMediaRequestHandler)
.SetMethod("setDevicePermissionHandler",
&Session::SetDevicePermissionHandler)
.SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache)
Expand Down
3 changes: 3 additions & 0 deletions shell/browser/api/electron_api_session.h
Expand Up @@ -179,6 +179,9 @@ class Session : public gin::Wrappable<Session>,
#endif

private:
void SetDisplayMediaRequestHandler(v8::Isolate* isolate,
v8::Local<v8::Value> val);

// Cached gin_helper::Wrappable objects.
v8::Global<v8::Value> cookies_;
v8::Global<v8::Value> protocol_;
Expand Down
131 changes: 131 additions & 0 deletions shell/browser/electron_browser_context.cc
Expand Up @@ -31,8 +31,11 @@
#include "content/browser/blob_storage/chrome_blob_storage_context.h" // nogncheck
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/cors_origin_pattern_setter.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/shared_cors_origin_access_list.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents_media_capture_id.h"
#include "media/audio/audio_device_description.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/network_context.mojom.h"
Expand All @@ -51,7 +54,10 @@
#include "shell/browser/zoom_level_delegate.h"
#include "shell/common/application_info.h"
#include "shell/common/electron_paths.h"
#include "shell/common/gin_converters/frame_converter.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/options_switches.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"

#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
#include "extensions/browser/browser_context_keyed_service_factories.h"
Expand Down Expand Up @@ -412,6 +418,131 @@ void ElectronBrowserContext::SetSSLConfigClient(
ssl_config_client_ = std::move(client);
}

void ElectronBrowserContext::SetDisplayMediaRequestHandler(
DisplayMediaRequestHandler handler) {
display_media_request_handler_ = handler;
}

void ElectronBrowserContext::DisplayMediaDeviceChosen(
const content::MediaStreamRequest& request,
content::MediaResponseCallback callback,
gin::Arguments* args) {
blink::mojom::StreamDevicesSetPtr stream_devices_set =
blink::mojom::StreamDevicesSet::New();
v8::Local<v8::Value> result;
if (!args->GetNext(&result) || result->IsNullOrUndefined()) {
std::move(callback).Run(
blink::mojom::StreamDevicesSet(),
blink::mojom::MediaStreamRequestResult::CAPTURE_FAILURE, nullptr);
return;
}
gin_helper::Dictionary result_dict;
if (!gin::ConvertFromV8(args->isolate(), result, &result_dict)) {
gin_helper::ErrorThrower(args->isolate())
.ThrowTypeError(
"Display Media Request streams callback must be called with null "
"or a valid object");
std::move(callback).Run(
blink::mojom::StreamDevicesSet(),
blink::mojom::MediaStreamRequestResult::CAPTURE_FAILURE, nullptr);
return;
}
stream_devices_set->stream_devices.emplace_back(
blink::mojom::StreamDevices::New());
blink::mojom::StreamDevices& devices = *stream_devices_set->stream_devices[0];
bool video_requested =
request.video_type != blink::mojom::MediaStreamType::NO_SERVICE;
bool audio_requested =
request.audio_type != blink::mojom::MediaStreamType::NO_SERVICE;
bool has_video = false;
if (video_requested && result_dict.Has("video")) {
gin_helper::Dictionary video_dict;
std::string id;
std::string name;
content::RenderFrameHost* rfh;
if (result_dict.Get("video", &video_dict) && video_dict.Get("id", &id) &&
video_dict.Get("name", &name)) {
devices.video_device =
blink::MediaStreamDevice(request.video_type, id, name);
nornagon marked this conversation as resolved.
Show resolved Hide resolved
} else if (result_dict.Get("video", &rfh)) {
devices.video_device = blink::MediaStreamDevice(
request.video_type,
content::WebContentsMediaCaptureId(rfh->GetProcess()->GetID(),
rfh->GetRoutingID())
.ToString(),
base::UTF16ToUTF8(
content::WebContents::FromRenderFrameHost(rfh)->GetTitle()));
} else {
gin_helper::ErrorThrower(args->isolate())
.ThrowTypeError(
"video must be a WebFrameMain or DesktopCapturerSource");
std::move(callback).Run(
blink::mojom::StreamDevicesSet(),
blink::mojom::MediaStreamRequestResult::CAPTURE_FAILURE, nullptr);
return;
}
has_video = true;
}
if (audio_requested && result_dict.Has("audio")) {
gin_helper::Dictionary audio_dict;
std::string id;
std::string name;
content::RenderFrameHost* rfh;
// NB. this is not permitted by the documentation, but is left here as an
// "escape hatch" for providing an arbitrary name/id if needed in the
// future.
if (result_dict.Get("audio", &audio_dict) && audio_dict.Get("id", &id) &&
audio_dict.Get("name", &name)) {
devices.audio_device =
blink::MediaStreamDevice(request.audio_type, id, name);
} else if (result_dict.Get("audio", &rfh)) {
devices.audio_device = blink::MediaStreamDevice(
request.audio_type,
content::WebContentsMediaCaptureId(rfh->GetProcess()->GetID(),
rfh->GetRoutingID(),
/* disable_local_echo= */ true)
.ToString(),
"Tab audio");
} else if (result_dict.Get("audio", &id)) {
devices.audio_device =
blink::MediaStreamDevice(request.audio_type, id, "System audio");
} else {
gin_helper::ErrorThrower(args->isolate())
.ThrowTypeError(
"audio must be a WebFrameMain, \"loopback\" or "
"\"loopbackWithMute\"");
std::move(callback).Run(
blink::mojom::StreamDevicesSet(),
blink::mojom::MediaStreamRequestResult::CAPTURE_FAILURE, nullptr);
return;
}
}

if ((video_requested && !has_video)) {
gin_helper::ErrorThrower(args->isolate())
.ThrowTypeError(
"Video was requested, but no video stream was provided");
std::move(callback).Run(
blink::mojom::StreamDevicesSet(),
blink::mojom::MediaStreamRequestResult::CAPTURE_FAILURE, nullptr);
return;
}

std::move(callback).Run(*stream_devices_set,
blink::mojom::MediaStreamRequestResult::OK, nullptr);
}

bool ElectronBrowserContext::ChooseDisplayMediaDevice(
const content::MediaStreamRequest& request,
content::MediaResponseCallback callback) {
if (!display_media_request_handler_)
return false;
DisplayMediaResponseCallbackJs callbackJs =
base::BindOnce(&DisplayMediaDeviceChosen, request, std::move(callback));
display_media_request_handler_.Run(request, std::move(callbackJs));
return true;
}

void ElectronBrowserContext::GrantDevicePermission(
const url::Origin& origin,
const base::Value& device,
Expand Down
26 changes: 26 additions & 0 deletions shell/browser/electron_browser_context.h
Expand Up @@ -13,8 +13,10 @@
#include "base/memory/weak_ptr.h"
#include "chrome/browser/predictors/preconnect_manager.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/media_stream_request.h"
#include "content/public/browser/resource_context.h"
#include "electron/buildflags/buildflags.h"
#include "gin/arguments.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
Expand All @@ -38,6 +40,13 @@ class ElectronExtensionSystem;
}
#endif

namespace v8 {
template <typename T>
class Local;
class Isolate;
class Value;
} // namespace v8

namespace electron {

using DevicePermissionMap =
Expand All @@ -51,6 +60,12 @@ class ResolveProxyHelper;
class WebViewManager;
class ProtocolRegistry;

using DisplayMediaResponseCallbackJs =
base::OnceCallback<void(gin::Arguments* args)>;
using DisplayMediaRequestHandler =
base::RepeatingCallback<void(const content::MediaStreamRequest&,
DisplayMediaResponseCallbackJs)>;

class ElectronBrowserContext : public content::BrowserContext {
public:
// disable copy
Expand Down Expand Up @@ -150,6 +165,10 @@ class ElectronBrowserContext : public content::BrowserContext {
network::mojom::SSLConfigPtr GetSSLConfig();
void SetSSLConfigClient(mojo::Remote<network::mojom::SSLConfigClient> client);

bool ChooseDisplayMediaDevice(const content::MediaStreamRequest& request,
content::MediaResponseCallback callback);
void SetDisplayMediaRequestHandler(DisplayMediaRequestHandler handler);

~ElectronBrowserContext() override;

// Grants |origin| access to |device|.
Expand All @@ -176,6 +195,11 @@ class ElectronBrowserContext : public content::BrowserContext {
bool in_memory,
base::Value::Dict options);

static void DisplayMediaDeviceChosen(
const content::MediaStreamRequest& request,
content::MediaResponseCallback callback,
gin::Arguments* args);

// Initialize pref registry.
void InitPrefs();

Expand Down Expand Up @@ -214,6 +238,8 @@ class ElectronBrowserContext : public content::BrowserContext {
network::mojom::SSLConfigPtr ssl_config_;
mojo::Remote<network::mojom::SSLConfigClient> ssl_config_client_;

DisplayMediaRequestHandler display_media_request_handler_;

// In-memory cache that holds objects that have been granted permissions.
DevicePermissionMap granted_devices_;

Expand Down