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)`
|
||||
|
||||
* `handler` Function\<boolean> | null
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
browser_context_->SetDisplayMediaRequestHandler(handler);
|
||||
}
|
||||
|
||||
void Session::SetDevicePermissionHandler(v8::Local<v8::Value> val,
|
||||
gin::Arguments* args) {
|
||||
ElectronPermissionManager::DeviceCheckHandler handler;
|
||||
|
@ -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)
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
} 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,
|
||||
|
|
|
@ -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"
|
||||
|
@ -38,6 +40,13 @@ class ElectronExtensionSystem;
|
|||
}
|
||||
#endif
|
||||
|
||||
namespace v8 {
|
||||
template <typename T>
|
||||
class Local;
|
||||
class Isolate;
|
||||
class Value;
|
||||
} // namespace v8
|
||||
|
||||
namespace electron {
|
||||
|
||||
using DevicePermissionMap =
|
||||
|
@ -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
|
||||
|
@ -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|.
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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_;
|
||||
|
||||
|
|
|
@ -111,19 +111,43 @@ void MediaAccessAllowed(const content::MediaStreamRequest& request,
|
|||
request.video_type ==
|
||||
blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE ||
|
||||
request.audio_type ==
|
||||
blink::mojom::MediaStreamType::GUM_TAB_AUDIO_CAPTURE)
|
||||
blink::mojom::MediaStreamType::GUM_TAB_AUDIO_CAPTURE) {
|
||||
HandleUserMediaRequest(request, std::move(callback));
|
||||
else if (request.video_type ==
|
||||
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE ||
|
||||
request.audio_type ==
|
||||
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)
|
||||
} else if (request.video_type ==
|
||||
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE ||
|
||||
request.audio_type ==
|
||||
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE) {
|
||||
webrtc::MediaStreamDevicesController::RequestPermissions(
|
||||
request, MediaCaptureDevicesDispatcher::GetInstance(),
|
||||
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(
|
||||
blink::mojom::StreamDevicesSet(),
|
||||
blink::mojom::MediaStreamRequestResult::NOT_SUPPORTED, nullptr);
|
||||
}
|
||||
} else {
|
||||
std::move(callback).Run(
|
||||
blink::mojom::StreamDevicesSet(),
|
||||
|
|
|
@ -26,6 +26,19 @@ v8::Local<v8::Value> Converter<content::RenderFrameHost*>::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
|
||||
v8::Local<v8::Value>
|
||||
Converter<gin_helper::AccessorValue<content::RenderFrameHost*>>::ToV8(
|
||||
|
|
|
@ -18,6 +18,9 @@ template <>
|
|||
struct Converter<content::RenderFrameHost*> {
|
||||
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
|
||||
content::RenderFrameHost* val);
|
||||
static bool FromV8(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> val,
|
||||
content::RenderFrameHost** out);
|
||||
};
|
||||
|
||||
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"
|
||||
pretty-ms "^5.1.0"
|
||||
|
||||
"@electron/typescript-definitions@^8.9.5":
|
||||
version "8.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@electron/typescript-definitions/-/typescript-definitions-8.9.5.tgz#e6cb08e0e7c9656e178b892eab50866a8a80bf7a"
|
||||
integrity sha512-xDLFl6joGpA8c9cGSPWC3DFHyIGf9+OWZmDrPbGJW1URt6C1ukdQWKSmjb1Rttb94QQxBrGuUlSyz27IQgLFsw==
|
||||
"@electron/typescript-definitions@^8.9.6":
|
||||
version "8.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@electron/typescript-definitions/-/typescript-definitions-8.9.6.tgz#99575209b12ae00784190282e5b636a44f1beabc"
|
||||
integrity sha512-Hlvzo0A5iuRFICOB/xIADKKc1axCA4G13vsCC5ZcG6VVvJPsmPrjr2/npb9Aebfzm4OUbdoPHS952lqPXFLFXQ==
|
||||
dependencies:
|
||||
"@types/node" "^11.13.7"
|
||||
chalk "^2.4.2"
|
||||
|
|
Loading…
Add table
Reference in a new issue