feat: session.setDisplayMediaRequestHandler (#30702)
This commit is contained in:
parent
0c04be502c
commit
221bb51326
14 changed files with 711 additions and 11 deletions
|
@ -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.
|
||||||
|
* `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
|
||||||
|
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)`
|
#### `ses.setDevicePermissionHandler(handler)`
|
||||||
|
|
||||||
* `handler` Function\<boolean> | null
|
* `handler` Function\<boolean> | null
|
||||||
|
|
|
@ -577,6 +577,8 @@ filenames = {
|
||||||
"shell/common/gin_converters/hid_device_info_converter.h",
|
"shell/common/gin_converters/hid_device_info_converter.h",
|
||||||
"shell/common/gin_converters/image_converter.cc",
|
"shell/common/gin_converters/image_converter.cc",
|
||||||
"shell/common/gin_converters/image_converter.h",
|
"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.cc",
|
||||||
"shell/common/gin_converters/message_box_converter.h",
|
"shell/common/gin_converters/message_box_converter.h",
|
||||||
"shell/common/gin_converters/native_window_converter.h",
|
"shell/common/gin_converters/native_window_converter.h",
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@azure/storage-blob": "^12.9.0",
|
"@azure/storage-blob": "^12.9.0",
|
||||||
"@electron/docs-parser": "^0.12.4",
|
"@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/auth-app": "^2.10.0",
|
||||||
"@octokit/rest": "^18.0.3",
|
"@octokit/rest": "^18.0.3",
|
||||||
"@primer/octicons": "^10.0.0",
|
"@primer/octicons": "^10.0.0",
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
#include "shell/browser/api/electron_api_net_log.h"
|
#include "shell/browser/api/electron_api_net_log.h"
|
||||||
#include "shell/browser/api/electron_api_protocol.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_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/api/electron_api_web_request.h"
|
||||||
#include "shell/browser/browser.h"
|
#include "shell/browser/browser.h"
|
||||||
#include "shell/browser/electron_browser_context.h"
|
#include "shell/browser/electron_browser_context.h"
|
||||||
|
@ -65,6 +66,7 @@
|
||||||
#include "shell/common/gin_converters/content_converter.h"
|
#include "shell/common/gin_converters/content_converter.h"
|
||||||
#include "shell/common/gin_converters/file_path_converter.h"
|
#include "shell/common/gin_converters/file_path_converter.h"
|
||||||
#include "shell/common/gin_converters/gurl_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/net_converter.h"
|
||||||
#include "shell/common/gin_converters/value_converter.h"
|
#include "shell/common/gin_converters/value_converter.h"
|
||||||
#include "shell/common/gin_helper/dictionary.h"
|
#include "shell/common/gin_helper/dictionary.h"
|
||||||
|
@ -73,6 +75,7 @@
|
||||||
#include "shell/common/options_switches.h"
|
#include "shell/common/options_switches.h"
|
||||||
#include "shell/common/process_util.h"
|
#include "shell/common/process_util.h"
|
||||||
#include "third_party/blink/public/common/storage_key/storage_key.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"
|
#include "ui/base/l10n/l10n_util.h"
|
||||||
|
|
||||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||||
|
@ -643,6 +646,22 @@ void Session::SetPermissionCheckHandler(v8::Local<v8::Value> val,
|
||||||
permission_manager->SetPermissionCheckHandler(handler);
|
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;
|
||||||
|
}
|
||||||
|
browser_context_->SetDisplayMediaRequestHandler(handler);
|
||||||
|
}
|
||||||
|
|
||||||
void Session::SetDevicePermissionHandler(v8::Local<v8::Value> val,
|
void Session::SetDevicePermissionHandler(v8::Local<v8::Value> val,
|
||||||
gin::Arguments* args) {
|
gin::Arguments* args) {
|
||||||
ElectronPermissionManager::DeviceCheckHandler handler;
|
ElectronPermissionManager::DeviceCheckHandler handler;
|
||||||
|
@ -1198,6 +1217,8 @@ gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
|
||||||
&Session::SetPermissionRequestHandler)
|
&Session::SetPermissionRequestHandler)
|
||||||
.SetMethod("setPermissionCheckHandler",
|
.SetMethod("setPermissionCheckHandler",
|
||||||
&Session::SetPermissionCheckHandler)
|
&Session::SetPermissionCheckHandler)
|
||||||
|
.SetMethod("setDisplayMediaRequestHandler",
|
||||||
|
&Session::SetDisplayMediaRequestHandler)
|
||||||
.SetMethod("setDevicePermissionHandler",
|
.SetMethod("setDevicePermissionHandler",
|
||||||
&Session::SetDevicePermissionHandler)
|
&Session::SetDevicePermissionHandler)
|
||||||
.SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache)
|
.SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache)
|
||||||
|
|
|
@ -179,6 +179,9 @@ class Session : public gin::Wrappable<Session>,
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void SetDisplayMediaRequestHandler(v8::Isolate* isolate,
|
||||||
|
v8::Local<v8::Value> val);
|
||||||
|
|
||||||
// Cached gin_helper::Wrappable objects.
|
// Cached gin_helper::Wrappable objects.
|
||||||
v8::Global<v8::Value> cookies_;
|
v8::Global<v8::Value> cookies_;
|
||||||
v8::Global<v8::Value> protocol_;
|
v8::Global<v8::Value> protocol_;
|
||||||
|
|
|
@ -31,8 +31,11 @@
|
||||||
#include "content/browser/blob_storage/chrome_blob_storage_context.h" // nogncheck
|
#include "content/browser/blob_storage/chrome_blob_storage_context.h" // nogncheck
|
||||||
#include "content/public/browser/browser_thread.h"
|
#include "content/public/browser/browser_thread.h"
|
||||||
#include "content/public/browser/cors_origin_pattern_setter.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/shared_cors_origin_access_list.h"
|
||||||
#include "content/public/browser/storage_partition.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/features.h"
|
||||||
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
|
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
|
||||||
#include "services/network/public/mojom/network_context.mojom.h"
|
#include "services/network/public/mojom/network_context.mojom.h"
|
||||||
|
@ -51,7 +54,10 @@
|
||||||
#include "shell/browser/zoom_level_delegate.h"
|
#include "shell/browser/zoom_level_delegate.h"
|
||||||
#include "shell/common/application_info.h"
|
#include "shell/common/application_info.h"
|
||||||
#include "shell/common/electron_paths.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 "shell/common/options_switches.h"
|
||||||
|
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
|
||||||
|
|
||||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||||
#include "extensions/browser/browser_context_keyed_service_factories.h"
|
#include "extensions/browser/browser_context_keyed_service_factories.h"
|
||||||
|
@ -412,6 +418,131 @@ void ElectronBrowserContext::SetSSLConfigClient(
|
||||||
ssl_config_client_ = std::move(client);
|
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);
|
||||||
|
} 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(
|
void ElectronBrowserContext::GrantDevicePermission(
|
||||||
const url::Origin& origin,
|
const url::Origin& origin,
|
||||||
const base::Value& device,
|
const base::Value& device,
|
||||||
|
|
|
@ -13,8 +13,10 @@
|
||||||
#include "base/memory/weak_ptr.h"
|
#include "base/memory/weak_ptr.h"
|
||||||
#include "chrome/browser/predictors/preconnect_manager.h"
|
#include "chrome/browser/predictors/preconnect_manager.h"
|
||||||
#include "content/public/browser/browser_context.h"
|
#include "content/public/browser/browser_context.h"
|
||||||
|
#include "content/public/browser/media_stream_request.h"
|
||||||
#include "content/public/browser/resource_context.h"
|
#include "content/public/browser/resource_context.h"
|
||||||
#include "electron/buildflags/buildflags.h"
|
#include "electron/buildflags/buildflags.h"
|
||||||
|
#include "gin/arguments.h"
|
||||||
#include "mojo/public/cpp/bindings/remote.h"
|
#include "mojo/public/cpp/bindings/remote.h"
|
||||||
#include "services/network/public/mojom/network_context.mojom.h"
|
#include "services/network/public/mojom/network_context.mojom.h"
|
||||||
#include "services/network/public/mojom/url_loader_factory.mojom.h"
|
#include "services/network/public/mojom/url_loader_factory.mojom.h"
|
||||||
|
@ -38,6 +40,13 @@ class ElectronExtensionSystem;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace v8 {
|
||||||
|
template <typename T>
|
||||||
|
class Local;
|
||||||
|
class Isolate;
|
||||||
|
class Value;
|
||||||
|
} // namespace v8
|
||||||
|
|
||||||
namespace electron {
|
namespace electron {
|
||||||
|
|
||||||
using DevicePermissionMap =
|
using DevicePermissionMap =
|
||||||
|
@ -51,6 +60,12 @@ class ResolveProxyHelper;
|
||||||
class WebViewManager;
|
class WebViewManager;
|
||||||
class ProtocolRegistry;
|
class ProtocolRegistry;
|
||||||
|
|
||||||
|
using DisplayMediaResponseCallbackJs =
|
||||||
|
base::OnceCallback<void(gin::Arguments* args)>;
|
||||||
|
using DisplayMediaRequestHandler =
|
||||||
|
base::RepeatingCallback<void(const content::MediaStreamRequest&,
|
||||||
|
DisplayMediaResponseCallbackJs)>;
|
||||||
|
|
||||||
class ElectronBrowserContext : public content::BrowserContext {
|
class ElectronBrowserContext : public content::BrowserContext {
|
||||||
public:
|
public:
|
||||||
// disable copy
|
// disable copy
|
||||||
|
@ -150,6 +165,10 @@ class ElectronBrowserContext : public content::BrowserContext {
|
||||||
network::mojom::SSLConfigPtr GetSSLConfig();
|
network::mojom::SSLConfigPtr GetSSLConfig();
|
||||||
void SetSSLConfigClient(mojo::Remote<network::mojom::SSLConfigClient> client);
|
void SetSSLConfigClient(mojo::Remote<network::mojom::SSLConfigClient> client);
|
||||||
|
|
||||||
|
bool ChooseDisplayMediaDevice(const content::MediaStreamRequest& request,
|
||||||
|
content::MediaResponseCallback callback);
|
||||||
|
void SetDisplayMediaRequestHandler(DisplayMediaRequestHandler handler);
|
||||||
|
|
||||||
~ElectronBrowserContext() override;
|
~ElectronBrowserContext() override;
|
||||||
|
|
||||||
// Grants |origin| access to |device|.
|
// Grants |origin| access to |device|.
|
||||||
|
@ -176,6 +195,11 @@ class ElectronBrowserContext : public content::BrowserContext {
|
||||||
bool in_memory,
|
bool in_memory,
|
||||||
base::Value::Dict options);
|
base::Value::Dict options);
|
||||||
|
|
||||||
|
static void DisplayMediaDeviceChosen(
|
||||||
|
const content::MediaStreamRequest& request,
|
||||||
|
content::MediaResponseCallback callback,
|
||||||
|
gin::Arguments* args);
|
||||||
|
|
||||||
// Initialize pref registry.
|
// Initialize pref registry.
|
||||||
void InitPrefs();
|
void InitPrefs();
|
||||||
|
|
||||||
|
@ -214,6 +238,8 @@ class ElectronBrowserContext : public content::BrowserContext {
|
||||||
network::mojom::SSLConfigPtr ssl_config_;
|
network::mojom::SSLConfigPtr ssl_config_;
|
||||||
mojo::Remote<network::mojom::SSLConfigClient> ssl_config_client_;
|
mojo::Remote<network::mojom::SSLConfigClient> ssl_config_client_;
|
||||||
|
|
||||||
|
DisplayMediaRequestHandler display_media_request_handler_;
|
||||||
|
|
||||||
// In-memory cache that holds objects that have been granted permissions.
|
// In-memory cache that holds objects that have been granted permissions.
|
||||||
DevicePermissionMap granted_devices_;
|
DevicePermissionMap granted_devices_;
|
||||||
|
|
||||||
|
|
|
@ -111,19 +111,43 @@ void MediaAccessAllowed(const content::MediaStreamRequest& request,
|
||||||
request.video_type ==
|
request.video_type ==
|
||||||
blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE ||
|
blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE ||
|
||||||
request.audio_type ==
|
request.audio_type ==
|
||||||
blink::mojom::MediaStreamType::GUM_TAB_AUDIO_CAPTURE)
|
blink::mojom::MediaStreamType::GUM_TAB_AUDIO_CAPTURE) {
|
||||||
HandleUserMediaRequest(request, std::move(callback));
|
HandleUserMediaRequest(request, std::move(callback));
|
||||||
else if (request.video_type ==
|
} else if (request.video_type ==
|
||||||
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE ||
|
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE ||
|
||||||
request.audio_type ==
|
request.audio_type ==
|
||||||
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)
|
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE) {
|
||||||
webrtc::MediaStreamDevicesController::RequestPermissions(
|
webrtc::MediaStreamDevicesController::RequestPermissions(
|
||||||
request, MediaCaptureDevicesDispatcher::GetInstance(),
|
request, MediaCaptureDevicesDispatcher::GetInstance(),
|
||||||
base::BindOnce(&OnMediaStreamRequestResponse, std::move(callback)));
|
base::BindOnce(&OnMediaStreamRequestResponse, std::move(callback)));
|
||||||
else
|
} else if (request.video_type ==
|
||||||
|
blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE ||
|
||||||
|
request.video_type == blink::mojom::MediaStreamType::
|
||||||
|
DISPLAY_VIDEO_CAPTURE_THIS_TAB ||
|
||||||
|
request.video_type ==
|
||||||
|
blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_SET ||
|
||||||
|
request.audio_type ==
|
||||||
|
blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE) {
|
||||||
|
content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(
|
||||||
|
request.render_process_id, request.render_frame_id);
|
||||||
|
if (!rfh)
|
||||||
|
return;
|
||||||
|
|
||||||
|
content::BrowserContext* browser_context = rfh->GetBrowserContext();
|
||||||
|
ElectronBrowserContext* electron_browser_context =
|
||||||
|
static_cast<ElectronBrowserContext*>(browser_context);
|
||||||
|
auto split_callback = base::SplitOnceCallback(std::move(callback));
|
||||||
|
if (electron_browser_context->ChooseDisplayMediaDevice(
|
||||||
|
request, std::move(split_callback.second)))
|
||||||
|
return;
|
||||||
|
std::move(split_callback.first)
|
||||||
|
.Run(blink::mojom::StreamDevicesSet(),
|
||||||
|
blink::mojom::MediaStreamRequestResult::NOT_SUPPORTED, nullptr);
|
||||||
|
} else {
|
||||||
std::move(callback).Run(
|
std::move(callback).Run(
|
||||||
blink::mojom::StreamDevicesSet(),
|
blink::mojom::StreamDevicesSet(),
|
||||||
blink::mojom::MediaStreamRequestResult::NOT_SUPPORTED, nullptr);
|
blink::mojom::MediaStreamRequestResult::NOT_SUPPORTED, nullptr);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
std::move(callback).Run(
|
std::move(callback).Run(
|
||||||
blink::mojom::StreamDevicesSet(),
|
blink::mojom::StreamDevicesSet(),
|
||||||
|
|
|
@ -26,6 +26,19 @@ v8::Local<v8::Value> Converter<content::RenderFrameHost*>::ToV8(
|
||||||
return electron::api::WebFrameMain::From(isolate, val).ToV8();
|
return electron::api::WebFrameMain::From(isolate, val).ToV8();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
bool Converter<content::RenderFrameHost*>::FromV8(
|
||||||
|
v8::Isolate* isolate,
|
||||||
|
v8::Local<v8::Value> val,
|
||||||
|
content::RenderFrameHost** out) {
|
||||||
|
electron::api::WebFrameMain* web_frame_main = nullptr;
|
||||||
|
if (!ConvertFromV8(isolate, val, &web_frame_main))
|
||||||
|
return false;
|
||||||
|
*out = web_frame_main->render_frame_host();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
v8::Local<v8::Value>
|
v8::Local<v8::Value>
|
||||||
Converter<gin_helper::AccessorValue<content::RenderFrameHost*>>::ToV8(
|
Converter<gin_helper::AccessorValue<content::RenderFrameHost*>>::ToV8(
|
||||||
|
|
|
@ -18,6 +18,9 @@ template <>
|
||||||
struct Converter<content::RenderFrameHost*> {
|
struct Converter<content::RenderFrameHost*> {
|
||||||
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
|
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
|
||||||
content::RenderFrameHost* val);
|
content::RenderFrameHost* val);
|
||||||
|
static bool FromV8(v8::Isolate* isolate,
|
||||||
|
v8::Local<v8::Value> val,
|
||||||
|
content::RenderFrameHost** out);
|
||||||
};
|
};
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
|
|
36
shell/common/gin_converters/media_converter.cc
Normal file
36
shell/common/gin_converters/media_converter.cc
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright (c) 2021 Slack Technologies, LLC.
|
||||||
|
// Use of this source code is governed by the MIT license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
#include "shell/common/gin_converters/media_converter.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "content/public/browser/media_stream_request.h"
|
||||||
|
#include "content/public/browser/render_frame_host.h"
|
||||||
|
#include "gin/data_object_builder.h"
|
||||||
|
#include "shell/common/gin_converters/frame_converter.h"
|
||||||
|
#include "shell/common/gin_converters/gurl_converter.h"
|
||||||
|
#include "shell/common/gin_helper/dictionary.h"
|
||||||
|
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
|
||||||
|
|
||||||
|
namespace gin {
|
||||||
|
|
||||||
|
v8::Local<v8::Value> Converter<content::MediaStreamRequest>::ToV8(
|
||||||
|
v8::Isolate* isolate,
|
||||||
|
const content::MediaStreamRequest& request) {
|
||||||
|
content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(
|
||||||
|
request.render_process_id, request.render_frame_id);
|
||||||
|
return gin::DataObjectBuilder(isolate)
|
||||||
|
.Set("frame", rfh)
|
||||||
|
.Set("securityOrigin", request.security_origin)
|
||||||
|
.Set("userGesture", request.user_gesture)
|
||||||
|
.Set("videoRequested",
|
||||||
|
request.video_type != blink::mojom::MediaStreamType::NO_SERVICE)
|
||||||
|
.Set("audioRequested",
|
||||||
|
request.audio_type != blink::mojom::MediaStreamType::NO_SERVICE)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace gin
|
26
shell/common/gin_converters/media_converter.h
Normal file
26
shell/common/gin_converters/media_converter.h
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright (c) 2021 Slack Technologies, LLC.
|
||||||
|
// Use of this source code is governed by the MIT license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
#ifndef ELECTRON_SHELL_COMMON_GIN_CONVERTERS_MEDIA_CONVERTER_H_
|
||||||
|
#define ELECTRON_SHELL_COMMON_GIN_CONVERTERS_MEDIA_CONVERTER_H_
|
||||||
|
|
||||||
|
#include "gin/converter.h"
|
||||||
|
#include "third_party/blink/public/common/mediastream/media_stream_request.h"
|
||||||
|
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-forward.h"
|
||||||
|
|
||||||
|
namespace content {
|
||||||
|
struct MediaStreamRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace gin {
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct Converter<content::MediaStreamRequest> {
|
||||||
|
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
|
||||||
|
const content::MediaStreamRequest& request);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace gin
|
||||||
|
|
||||||
|
#endif // ELECTRON_SHELL_COMMON_GIN_CONVERTERS_MEDIA_CONVERTER_H_
|
361
spec-main/api-media-handler-spec.ts
Normal file
361
spec-main/api-media-handler-spec.ts
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { BrowserWindow, session, desktopCapturer } from 'electron/main';
|
||||||
|
import { closeAllWindows } from './window-helpers';
|
||||||
|
import * as http from 'http';
|
||||||
|
import { ifdescribe, ifit } from './spec-helpers';
|
||||||
|
|
||||||
|
const features = process._linkedBinding('electron_common_features');
|
||||||
|
|
||||||
|
ifdescribe(features.isDesktopCapturerEnabled())('setDisplayMediaRequestHandler', () => {
|
||||||
|
afterEach(closeAllWindows);
|
||||||
|
// These tests are done on an http server because navigator.userAgentData
|
||||||
|
// requires a secure context.
|
||||||
|
let server: http.Server;
|
||||||
|
let serverUrl: string;
|
||||||
|
before(async () => {
|
||||||
|
server = http.createServer((req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
res.end('');
|
||||||
|
});
|
||||||
|
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
serverUrl = `http://localhost:${(server.address() as any).port}`;
|
||||||
|
});
|
||||||
|
after(() => {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE(nornagon): this test fails on our macOS CircleCI runners with the
|
||||||
|
// error message:
|
||||||
|
// [ERROR:video_capture_device_client.cc(659)] error@ OnStart@content/browser/media/capture/desktop_capture_device_mac.cc:98, CGDisplayStreamCreate failed, OS message: Value too large to be stored in data type (84)
|
||||||
|
// This is possibly related to the OS/VM setup that CircleCI uses for macOS.
|
||||||
|
// Our arm64 runners are in @jkleinsc's office, and are real machines, so the
|
||||||
|
// test works there.
|
||||||
|
ifit(!(process.platform === 'darwin' && process.arch === 'x64'))('works when calling getDisplayMedia', async function () {
|
||||||
|
if ((await desktopCapturer.getSources({ types: ['screen'] })).length === 0) { return this.skip(); }
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
let requestHandlerCalled = false;
|
||||||
|
let mediaRequest: any = null;
|
||||||
|
ses.setDisplayMediaRequestHandler((request, callback) => {
|
||||||
|
requestHandlerCalled = true;
|
||||||
|
mediaRequest = request;
|
||||||
|
desktopCapturer.getSources({ types: ['screen'] }).then((sources) => {
|
||||||
|
// Grant access to the first screen found.
|
||||||
|
const { id, name } = sources[0];
|
||||||
|
callback({
|
||||||
|
video: { id, name }
|
||||||
|
// TODO: 'loopback' and 'loopbackWithMute' are currently only supported on Windows.
|
||||||
|
// audio: { id: 'loopback', name: 'System Audio' }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: true,
|
||||||
|
audio: false,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(requestHandlerCalled).to.be.true();
|
||||||
|
expect(mediaRequest.videoRequested).to.be.true();
|
||||||
|
expect(mediaRequest.audioRequested).to.be.false();
|
||||||
|
expect(ok).to.be.true(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not crash when using a bogus ID', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
let requestHandlerCalled = false;
|
||||||
|
ses.setDisplayMediaRequestHandler((request, callback) => {
|
||||||
|
requestHandlerCalled = true;
|
||||||
|
callback({
|
||||||
|
video: { id: 'bogus', name: 'whatever' }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(requestHandlerCalled).to.be.true();
|
||||||
|
expect(ok).to.be.false();
|
||||||
|
expect(message).to.equal('Could not start video source');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not crash when providing only audio for a video request', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
let requestHandlerCalled = false;
|
||||||
|
let callbackError: any;
|
||||||
|
ses.setDisplayMediaRequestHandler((request, callback) => {
|
||||||
|
requestHandlerCalled = true;
|
||||||
|
try {
|
||||||
|
callback({
|
||||||
|
audio: 'loopback'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
callbackError = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(requestHandlerCalled).to.be.true();
|
||||||
|
expect(ok).to.be.false();
|
||||||
|
expect(callbackError?.message).to.equal('Video was requested, but no video stream was provided');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not crash when providing only an audio stream for an audio+video request', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
let requestHandlerCalled = false;
|
||||||
|
let callbackError: any;
|
||||||
|
ses.setDisplayMediaRequestHandler((request, callback) => {
|
||||||
|
requestHandlerCalled = true;
|
||||||
|
try {
|
||||||
|
callback({
|
||||||
|
audio: 'loopback'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
callbackError = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(requestHandlerCalled).to.be.true();
|
||||||
|
expect(ok).to.be.false();
|
||||||
|
expect(callbackError?.message).to.equal('Video was requested, but no video stream was provided');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not crash when providing a non-loopback audio stream', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
let requestHandlerCalled = false;
|
||||||
|
ses.setDisplayMediaRequestHandler((request, callback) => {
|
||||||
|
requestHandlerCalled = true;
|
||||||
|
callback({
|
||||||
|
video: w.webContents.mainFrame,
|
||||||
|
audio: 'default' as any
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(requestHandlerCalled).to.be.true();
|
||||||
|
expect(ok).to.be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not crash when providing no streams', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
let requestHandlerCalled = false;
|
||||||
|
let callbackError: any;
|
||||||
|
ses.setDisplayMediaRequestHandler((request, callback) => {
|
||||||
|
requestHandlerCalled = true;
|
||||||
|
try {
|
||||||
|
callback({});
|
||||||
|
} catch (e) {
|
||||||
|
callbackError = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(requestHandlerCalled).to.be.true();
|
||||||
|
expect(ok).to.be.false();
|
||||||
|
expect(callbackError.message).to.equal('Video was requested, but no video stream was provided');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not crash when using a bogus web-contents-media-stream:// ID', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
let requestHandlerCalled = false;
|
||||||
|
ses.setDisplayMediaRequestHandler((request, callback) => {
|
||||||
|
requestHandlerCalled = true;
|
||||||
|
callback({
|
||||||
|
video: { id: 'web-contents-media-stream://9999:9999', name: 'whatever' }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(requestHandlerCalled).to.be.true();
|
||||||
|
// This is a little surprising... apparently chrome will generate a stream
|
||||||
|
// for this non-existent web contents?
|
||||||
|
expect(ok).to.be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is not called when calling getUserMedia', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
ses.setDisplayMediaRequestHandler(() => {
|
||||||
|
throw new Error('bad');
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getUserMedia({
|
||||||
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(ok).to.be.true(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works when calling getDisplayMedia with preferCurrentTab', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
let requestHandlerCalled = false;
|
||||||
|
ses.setDisplayMediaRequestHandler((request, callback) => {
|
||||||
|
requestHandlerCalled = true;
|
||||||
|
callback({ video: w.webContents.mainFrame });
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
|
preferCurrentTab: true,
|
||||||
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(requestHandlerCalled).to.be.true();
|
||||||
|
expect(ok).to.be.true(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
ifit(!(process.platform === 'darwin' && process.arch === 'x64'))('can supply a screen response to preferCurrentTab', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
let requestHandlerCalled = false;
|
||||||
|
ses.setDisplayMediaRequestHandler(async (request, callback) => {
|
||||||
|
requestHandlerCalled = true;
|
||||||
|
const sources = await desktopCapturer.getSources({ types: ['screen'] });
|
||||||
|
callback({ video: sources[0] });
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
|
preferCurrentTab: true,
|
||||||
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(requestHandlerCalled).to.be.true();
|
||||||
|
expect(ok).to.be.true(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can supply a frame response', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
let requestHandlerCalled = false;
|
||||||
|
ses.setDisplayMediaRequestHandler(async (request, callback) => {
|
||||||
|
requestHandlerCalled = true;
|
||||||
|
callback({ video: w.webContents.mainFrame });
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(requestHandlerCalled).to.be.true();
|
||||||
|
expect(ok).to.be.true(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is not called when calling legacy getUserMedia', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
ses.setDisplayMediaRequestHandler(() => {
|
||||||
|
throw new Error('bad');
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||||
|
new Promise((resolve, reject) => navigator.getUserMedia({
|
||||||
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
}, x => resolve({ok: x instanceof MediaStream}), e => reject({ok: false, message: e.message})))
|
||||||
|
`);
|
||||||
|
expect(ok).to.be.true(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is not called when calling legacy getUserMedia with desktop capture constraint', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
ses.setDisplayMediaRequestHandler(() => {
|
||||||
|
throw new Error('bad');
|
||||||
|
});
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||||
|
new Promise((resolve, reject) => navigator.getUserMedia({
|
||||||
|
video: {
|
||||||
|
mandatory: {
|
||||||
|
chromeMediaSource: 'desktop'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, x => resolve({ok: x instanceof MediaStream}), e => reject({ok: false, message: e.message})))
|
||||||
|
`);
|
||||||
|
expect(ok).to.be.true(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works when calling getUserMedia without a media request handler', async () => {
|
||||||
|
const w = new BrowserWindow({ show: false });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getUserMedia({
|
||||||
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(ok).to.be.true(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works when calling legacy getUserMedia without a media request handler', async () => {
|
||||||
|
const w = new BrowserWindow({ show: false });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||||
|
new Promise((resolve, reject) => navigator.getUserMedia({
|
||||||
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
}, x => resolve({ok: x instanceof MediaStream}), e => reject({ok: false, message: e.message})))
|
||||||
|
`);
|
||||||
|
expect(ok).to.be.true(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can remove a displayMediaRequestHandler', async () => {
|
||||||
|
const ses = session.fromPartition('' + Math.random());
|
||||||
|
|
||||||
|
ses.setDisplayMediaRequestHandler(() => {
|
||||||
|
throw new Error('bad');
|
||||||
|
});
|
||||||
|
ses.setDisplayMediaRequestHandler(null);
|
||||||
|
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: true,
|
||||||
|
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
|
||||||
|
`);
|
||||||
|
expect(ok).to.be.false();
|
||||||
|
expect(message).to.equal('Not supported');
|
||||||
|
});
|
||||||
|
});
|
|
@ -126,10 +126,10 @@
|
||||||
ora "^4.0.3"
|
ora "^4.0.3"
|
||||||
pretty-ms "^5.1.0"
|
pretty-ms "^5.1.0"
|
||||||
|
|
||||||
"@electron/typescript-definitions@^8.9.5":
|
"@electron/typescript-definitions@^8.9.6":
|
||||||
version "8.9.5"
|
version "8.9.6"
|
||||||
resolved "https://registry.yarnpkg.com/@electron/typescript-definitions/-/typescript-definitions-8.9.5.tgz#e6cb08e0e7c9656e178b892eab50866a8a80bf7a"
|
resolved "https://registry.yarnpkg.com/@electron/typescript-definitions/-/typescript-definitions-8.9.6.tgz#99575209b12ae00784190282e5b636a44f1beabc"
|
||||||
integrity sha512-xDLFl6joGpA8c9cGSPWC3DFHyIGf9+OWZmDrPbGJW1URt6C1ukdQWKSmjb1Rttb94QQxBrGuUlSyz27IQgLFsw==
|
integrity sha512-Hlvzo0A5iuRFICOB/xIADKKc1axCA4G13vsCC5ZcG6VVvJPsmPrjr2/npb9Aebfzm4OUbdoPHS952lqPXFLFXQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "^11.13.7"
|
"@types/node" "^11.13.7"
|
||||||
chalk "^2.4.2"
|
chalk "^2.4.2"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue