feat: add desktopCapturer.getMediaSourceIdForWebContents() to get stream source id from web contents (#22701)
* feat: add desktopCapturer.getMediaSourceIdForWebContents() to get stream source id from web contents * Cleanup from #22701 PR comments
This commit is contained in:
parent
dc72f74020
commit
204f001c5d
9 changed files with 178 additions and 2 deletions
|
@ -72,6 +72,50 @@ const constraints = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This example shows how to capture a video from a [WebContents](web-contents.md)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In the renderer process.
|
||||||
|
const { desktopCapturer, remote } = require('electron')
|
||||||
|
|
||||||
|
desktopCapturer.getMediaSourceIdForWebContents(remote.getCurrentWebContents().id).then(async mediaSourceId => {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: {
|
||||||
|
mandatory: {
|
||||||
|
chromeMediaSource: 'tab',
|
||||||
|
chromeMediaSourceId: mediaSourceId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
mandatory: {
|
||||||
|
chromeMediaSource: 'tab',
|
||||||
|
chromeMediaSourceId: mediaSourceId,
|
||||||
|
minWidth: 1280,
|
||||||
|
maxWidth: 1280,
|
||||||
|
minHeight: 720,
|
||||||
|
maxHeight: 720
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
handleStream(stream)
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleStream (stream) {
|
||||||
|
const video = document.querySelector('video')
|
||||||
|
video.srcObject = stream
|
||||||
|
video.onloadedmetadata = (e) => video.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
The `desktopCapturer` module has the following methods:
|
The `desktopCapturer` module has the following methods:
|
||||||
|
@ -94,6 +138,15 @@ Returns `Promise<DesktopCapturerSource[]>` - Resolves with an array of [`Desktop
|
||||||
**Note** Capturing the screen contents requires user consent on macOS 10.15 Catalina or higher,
|
**Note** Capturing the screen contents requires user consent on macOS 10.15 Catalina or higher,
|
||||||
which can detected by [`systemPreferences.getMediaAccessStatus`].
|
which can detected by [`systemPreferences.getMediaAccessStatus`].
|
||||||
|
|
||||||
|
### `desktopCapturer.getMediaSourceIdForWebContents(webContentsId)`
|
||||||
|
|
||||||
|
* `webContentsId` number - Id of the WebContents to get stream of
|
||||||
|
|
||||||
|
Returns `Promise<string>` - Resolves with the identifier of a WebContents stream, this identifier can be
|
||||||
|
used with [`navigator.mediaDevices.getUserMedia`].
|
||||||
|
The identifier is **only valid for 10 seconds**.
|
||||||
|
The identifier may be empty if not requested from a renderer process.
|
||||||
|
|
||||||
[`navigator.mediaDevices.getUserMedia`]: https://developer.mozilla.org/en/docs/Web/API/MediaDevices/getUserMedia
|
[`navigator.mediaDevices.getUserMedia`]: https://developer.mozilla.org/en/docs/Web/API/MediaDevices/getUserMedia
|
||||||
[`systemPreferences.getMediaAccessStatus`]: system-preferences.md#systempreferencesgetmediaaccessstatusmediatype-macos
|
[`systemPreferences.getMediaAccessStatus`]: system-preferences.md#systempreferencesgetmediaaccessstatusmediatype-macos
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const { createDesktopCapturer } = process.electronBinding('desktop_capturer');
|
const { createDesktopCapturer, getMediaSourceIdForWebContents: getMediaSourceIdForWebContentsBinding } = process.electronBinding('desktop_capturer');
|
||||||
|
|
||||||
const deepEqual = (a: ElectronInternal.GetSourcesOptions, b: ElectronInternal.GetSourcesOptions) => JSON.stringify(a) === JSON.stringify(b);
|
const deepEqual = (a: ElectronInternal.GetSourcesOptions, b: ElectronInternal.GetSourcesOptions) => JSON.stringify(a) === JSON.stringify(b);
|
||||||
|
|
||||||
|
@ -77,3 +77,7 @@ export const getSourcesImpl = (event: Electron.IpcMainEvent | null, args: Electr
|
||||||
|
|
||||||
return getSources;
|
return getSources;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMediaSourceIdForWebContents = (event: Electron.IpcMainEvent, webContentsId: number) => {
|
||||||
|
return getMediaSourceIdForWebContentsBinding(event.sender.id, webContentsId);
|
||||||
|
};
|
||||||
|
|
|
@ -73,6 +73,10 @@ if (BUILDFLAG(ENABLE_DESKTOP_CAPTURER)) {
|
||||||
|
|
||||||
return typeUtils.serialize(await desktopCapturer.getSourcesImpl(event, options));
|
return typeUtils.serialize(await desktopCapturer.getSourcesImpl(event, options));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMainInternal.handle('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_MEDIA_SOURCE_ID_FOR_WEB_CONTENTS', function (event, webContentsId, stack) {
|
||||||
|
return desktopCapturer.getMediaSourceIdForWebContents(event, webContentsId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRemoteModuleEnabled = BUILDFLAG(ENABLE_REMOTE_MODULE)
|
const isRemoteModuleEnabled = BUILDFLAG(ENABLE_REMOTE_MODULE)
|
||||||
|
|
|
@ -16,3 +16,7 @@ function getCurrentStack () {
|
||||||
export async function getSources (options: Electron.SourcesOptions) {
|
export async function getSources (options: Electron.SourcesOptions) {
|
||||||
return deserialize(await ipcRendererInternal.invoke('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', options, getCurrentStack()));
|
return deserialize(await ipcRendererInternal.invoke('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', options, getCurrentStack()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMediaSourceIdForWebContents (webContentsId: number) {
|
||||||
|
return ipcRendererInternal.invoke<string>('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_MEDIA_SOURCE_ID_FOR_WEB_CONTENTS', webContentsId, getCurrentStack());
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,11 @@
|
||||||
#include "chrome/browser/media/webrtc/desktop_media_list.h"
|
#include "chrome/browser/media/webrtc/desktop_media_list.h"
|
||||||
#include "chrome/browser/media/webrtc/window_icon_util.h"
|
#include "chrome/browser/media/webrtc/window_icon_util.h"
|
||||||
#include "content/public/browser/desktop_capture.h"
|
#include "content/public/browser/desktop_capture.h"
|
||||||
|
#include "content/public/browser/desktop_streams_registry.h"
|
||||||
|
#include "content/public/browser/render_frame_host.h"
|
||||||
|
#include "content/public/browser/render_process_host.h"
|
||||||
#include "gin/object_template_builder.h"
|
#include "gin/object_template_builder.h"
|
||||||
|
#include "shell/browser/api/electron_api_web_contents.h"
|
||||||
#include "shell/common/api/electron_api_native_image.h"
|
#include "shell/common/api/electron_api_native_image.h"
|
||||||
#include "shell/common/gin_converters/gfx_converter.h"
|
#include "shell/common/gin_converters/gfx_converter.h"
|
||||||
#include "shell/common/gin_helper/dictionary.h"
|
#include "shell/common/gin_helper/dictionary.h"
|
||||||
|
@ -22,6 +26,7 @@
|
||||||
#include "shell/common/node_includes.h"
|
#include "shell/common/node_includes.h"
|
||||||
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_options.h"
|
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_options.h"
|
||||||
#include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h"
|
#include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h"
|
||||||
|
#include "url/origin.h"
|
||||||
|
|
||||||
#if defined(OS_WIN)
|
#if defined(OS_WIN)
|
||||||
#include "third_party/webrtc/modules/desktop_capture/win/dxgi_duplicator_controller.h"
|
#include "third_party/webrtc/modules/desktop_capture/win/dxgi_duplicator_controller.h"
|
||||||
|
@ -202,6 +207,55 @@ gin::Handle<DesktopCapturer> DesktopCapturer::Create(v8::Isolate* isolate) {
|
||||||
return gin::CreateHandle(isolate, new DesktopCapturer(isolate));
|
return gin::CreateHandle(isolate, new DesktopCapturer(isolate));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
std::string DesktopCapturer::GetMediaSourceIdForWebContents(
|
||||||
|
v8::Isolate* isolate,
|
||||||
|
gin_helper::ErrorThrower thrower,
|
||||||
|
int32_t request_web_contents_id,
|
||||||
|
int32_t web_contents_id) {
|
||||||
|
std::string id;
|
||||||
|
auto* web_contents = gin_helper::TrackableObject<WebContents>::FromWeakMapID(
|
||||||
|
isolate, web_contents_id);
|
||||||
|
|
||||||
|
if (!web_contents) {
|
||||||
|
thrower.ThrowError("Failed to find WebContents with id " +
|
||||||
|
std::to_string(web_contents_id));
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* main_frame = web_contents->web_contents()->GetMainFrame();
|
||||||
|
DCHECK(main_frame);
|
||||||
|
content::DesktopMediaID media_id(
|
||||||
|
content::DesktopMediaID::TYPE_WEB_CONTENTS,
|
||||||
|
content::DesktopMediaID::kNullId,
|
||||||
|
content::WebContentsMediaCaptureId(main_frame->GetProcess()->GetID(),
|
||||||
|
main_frame->GetRoutingID()));
|
||||||
|
|
||||||
|
auto* request_web_contents =
|
||||||
|
gin_helper::TrackableObject<WebContents>::FromWeakMapID(
|
||||||
|
isolate, request_web_contents_id);
|
||||||
|
if (request_web_contents) {
|
||||||
|
// comment copied from
|
||||||
|
// chrome/browser/extensions/api/desktop_capture/desktop_capture_base.cc
|
||||||
|
// TODO(miu): Once render_frame_host() is being set, we should register the
|
||||||
|
// exact RenderFrame requesting the stream, not the main RenderFrame. With
|
||||||
|
// that change, also update
|
||||||
|
// MediaCaptureDevicesDispatcher::ProcessDesktopCaptureAccessRequest().
|
||||||
|
// http://crbug.com/304341
|
||||||
|
auto* const request_main_frame =
|
||||||
|
request_web_contents->web_contents()->GetMainFrame();
|
||||||
|
DCHECK(request_main_frame);
|
||||||
|
id = content::DesktopStreamsRegistry::GetInstance()->RegisterStream(
|
||||||
|
request_main_frame->GetProcess()->GetID(),
|
||||||
|
request_main_frame->GetRoutingID(),
|
||||||
|
url::Origin::Create(
|
||||||
|
request_main_frame->GetLastCommittedURL().GetOrigin()),
|
||||||
|
media_id, "", content::kRegistryStreamTypeTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
gin::ObjectTemplateBuilder DesktopCapturer::GetObjectTemplateBuilder(
|
gin::ObjectTemplateBuilder DesktopCapturer::GetObjectTemplateBuilder(
|
||||||
v8::Isolate* isolate) {
|
v8::Isolate* isolate) {
|
||||||
return gin::Wrappable<DesktopCapturer>::GetObjectTemplateBuilder(isolate)
|
return gin::Wrappable<DesktopCapturer>::GetObjectTemplateBuilder(isolate)
|
||||||
|
@ -225,6 +279,9 @@ void Initialize(v8::Local<v8::Object> exports,
|
||||||
gin_helper::Dictionary dict(context->GetIsolate(), exports);
|
gin_helper::Dictionary dict(context->GetIsolate(), exports);
|
||||||
dict.SetMethod("createDesktopCapturer",
|
dict.SetMethod("createDesktopCapturer",
|
||||||
&electron::api::DesktopCapturer::Create);
|
&electron::api::DesktopCapturer::Create);
|
||||||
|
dict.SetMethod(
|
||||||
|
"getMediaSourceIdForWebContents",
|
||||||
|
&electron::api::DesktopCapturer::GetMediaSourceIdForWebContents);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
#include "chrome/browser/media/webrtc/native_desktop_media_list.h"
|
#include "chrome/browser/media/webrtc/native_desktop_media_list.h"
|
||||||
#include "gin/handle.h"
|
#include "gin/handle.h"
|
||||||
#include "gin/wrappable.h"
|
#include "gin/wrappable.h"
|
||||||
|
#include "shell/common/gin_helper/dictionary.h"
|
||||||
|
|
||||||
namespace electron {
|
namespace electron {
|
||||||
|
|
||||||
|
@ -32,6 +33,12 @@ class DesktopCapturer : public gin::Wrappable<DesktopCapturer>,
|
||||||
|
|
||||||
static gin::Handle<DesktopCapturer> Create(v8::Isolate* isolate);
|
static gin::Handle<DesktopCapturer> Create(v8::Isolate* isolate);
|
||||||
|
|
||||||
|
static std::string GetMediaSourceIdForWebContents(
|
||||||
|
v8::Isolate* isolate,
|
||||||
|
gin_helper::ErrorThrower thrower,
|
||||||
|
int32_t request_web_contents_id,
|
||||||
|
int32_t web_contents_id);
|
||||||
|
|
||||||
void StartHandling(bool capture_window,
|
void StartHandling(bool capture_window,
|
||||||
bool capture_screen,
|
bool capture_screen,
|
||||||
const gfx::Size& thumbnail_size,
|
const gfx::Size& thumbnail_size,
|
||||||
|
|
|
@ -138,6 +138,29 @@ ifdescribe(features.isDesktopCapturerEnabled() && !process.arch.includes('arm')
|
||||||
expect(mediaSourceId).to.equal(foundSource!.id);
|
expect(mediaSourceId).to.equal(foundSource!.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getMediaSourceIdForWebContents', () => {
|
||||||
|
const getMediaSourceIdForWebContents: typeof desktopCapturer.getMediaSourceIdForWebContents = (webContentsId: number) => {
|
||||||
|
return w.webContents.executeJavaScript(`
|
||||||
|
require('electron').desktopCapturer.getMediaSourceIdForWebContents(${JSON.stringify(webContentsId)}).then(r => JSON.parse(JSON.stringify(r)))
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should return a stream id for web contents', async () => {
|
||||||
|
const result = await getMediaSourceIdForWebContents(w.webContents.id);
|
||||||
|
expect(result).to.be.a('string').that.is.not.empty();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error for invalid options', async () => {
|
||||||
|
const promise = getMediaSourceIdForWebContents('not-an-id' as unknown as number);
|
||||||
|
await expect(promise).to.be.eventually.rejectedWith(Error, 'TypeError: Error processing argument');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error for invalid web contents id', async () => {
|
||||||
|
const promise = getMediaSourceIdForWebContents(-200);
|
||||||
|
await expect(promise).to.be.eventually.rejectedWith(Error, 'Failed to find WebContents');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// TODO(deepak1556): currently fails on all ci, enable it after upgrade.
|
// TODO(deepak1556): currently fails on all ci, enable it after upgrade.
|
||||||
it.skip('moveAbove should move the window at the requested place', async () => {
|
it.skip('moveAbove should move the window at the requested place', async () => {
|
||||||
// DesktopCapturer.getSources() is guaranteed to return in the correct
|
// DesktopCapturer.getSources() is guaranteed to return in the correct
|
||||||
|
|
|
@ -130,6 +130,27 @@ desktopCapturer.getSources({ types: ['window', 'screen'] }).then(sources => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
desktopCapturer.getMediaSourceIdForWebContents(remote.getCurrentWebContents().id).then(mediaSourceId => {
|
||||||
|
(navigator as any).webkitGetUserMedia({
|
||||||
|
audio: {
|
||||||
|
mandatory: {
|
||||||
|
chromeMediaSource: 'tab',
|
||||||
|
chromeMediaSourceId: mediaSourceId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
mandatory: {
|
||||||
|
chromeMediaSource: 'tab',
|
||||||
|
chromeMediaSourceId: mediaSourceId,
|
||||||
|
minWidth: 1280,
|
||||||
|
maxWidth: 1280,
|
||||||
|
minHeight: 720,
|
||||||
|
maxHeight: 720
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, gotStream, getUserMediaError)
|
||||||
|
})
|
||||||
|
|
||||||
function gotStream (stream: any) {
|
function gotStream (stream: any) {
|
||||||
(document.querySelector('video') as HTMLVideoElement).src = URL.createObjectURL(stream)
|
(document.querySelector('video') as HTMLVideoElement).src = URL.createObjectURL(stream)
|
||||||
}
|
}
|
||||||
|
|
5
typings/internal-ambient.d.ts
vendored
5
typings/internal-ambient.d.ts
vendored
|
@ -100,7 +100,10 @@ declare namespace NodeJS {
|
||||||
electronBinding(name: 'v8_util'): V8UtilBinding;
|
electronBinding(name: 'v8_util'): V8UtilBinding;
|
||||||
electronBinding(name: 'app'): { app: Electron.App, App: Function };
|
electronBinding(name: 'app'): { app: Electron.App, App: Function };
|
||||||
electronBinding(name: 'command_line'): Electron.CommandLine;
|
electronBinding(name: 'command_line'): Electron.CommandLine;
|
||||||
electronBinding(name: 'desktop_capturer'): { createDesktopCapturer(): ElectronInternal.DesktopCapturer };
|
electronBinding(name: 'desktop_capturer'): {
|
||||||
|
createDesktopCapturer(): ElectronInternal.DesktopCapturer;
|
||||||
|
getMediaSourceIdForWebContents(requestWebContentsId: number, webContentsId: number): string;
|
||||||
|
};
|
||||||
electronBinding(name: 'net'): {
|
electronBinding(name: 'net'): {
|
||||||
isValidHeaderName: (headerName: string) => boolean;
|
isValidHeaderName: (headerName: string) => boolean;
|
||||||
isValidHeaderValue: (headerValue: string) => boolean;
|
isValidHeaderValue: (headerValue: string) => boolean;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue