0029. 实现屏幕监听功能
- 这是参照官方示例实现一个屏幕实时监听的 demo。
1. 🔗 links
- https://www.electronjs.org/docs/latest/api/desktop-capturer
- Electron,查看主进程的 desktopCapturer API 的相关描述。
- https://www.electronjs.org/zh/docs/latest/api/screen
- Electron,查看主进程模块
screen
的相关说明。
- Electron,查看主进程模块
- https://www.electronjs.org/zh/docs/latest/api/structures/desktop-capturer-source
- Electron,查看 DesktopCapturerSource 对象结构详情,
desktopCapturer
的返回值类型是Promise<DesktopCapturerSource[]>
。
- Electron,查看 DesktopCapturerSource 对象结构详情,
- https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia
- MDN,查看 WebRTC API -
MediaDevices.getUserMedia()
的相关说明。仅作为一个参考即可,还是以 Electron 官方的写法为准。(这玩意儿要求的参数结构和官方示例中的结构不一致。)
- MDN,查看 WebRTC API -
- https://github.com/electron/electron/issues/27139
- Electron Github Issues,mandatory property missing from MediaTrackConstraints #27139。
- 主要讨论了在使用 Electron 的 desktopCapturer API 与 TypeScript 时,由于 mandatory 属性不在 MediaTrackConstraints 类型中而引起的类型错误问题。
2. 📒navigator.mediaDevices.getUserMedia()
的 video 配置结构问题
- https://github.com/electron/electron/issues/27139
- Electron Github Issues,mandatory property missing from MediaTrackConstraints #27139。
- 主要讨论了在使用 Electron 的 desktopCapturer API 与 TypeScript 时,由于 mandatory 属性不在 MediaTrackConstraints 类型中而引起的类型错误问题。
- 这个问题将在本节实现的 demo 中遇到,主要是数据结构不一致的一个问题。
- 虽然 Electron 基于 Chromium,但在 desktopCapturer 和
navigator.mediaDevices.getUserMedia
的整合中存在一些差异。Electron 可能在内部处理这些约束的方式与纯 Chromium 环境不同。
ts
// lib.dom.d.ts
interface MediaStreamConstraints {
audio?: boolean | MediaTrackConstraints;
peerIdentity?: string;
preferCurrentTab?: boolean;
video?: boolean | MediaTrackConstraints;
}
interface MediaTrackConstraints extends MediaTrackConstraintSet {
advanced?: MediaTrackConstraintSet[];
}
interface MediaTrackConstraintSet {
aspectRatio?: ConstrainDouble;
autoGainControl?: ConstrainBoolean;
channelCount?: ConstrainULong;
deviceId?: ConstrainDOMString;
displaySurface?: ConstrainDOMString;
echoCancellation?: ConstrainBoolean;
facingMode?: ConstrainDOMString;
frameRate?: ConstrainDouble;
groupId?: ConstrainDOMString;
height?: ConstrainULong;
noiseSuppression?: ConstrainBoolean;
sampleRate?: ConstrainULong;
sampleSize?: ConstrainULong;
width?: ConstrainULong;
}
// Electron 官方示例中 video 字段中的 mandatory 字段,在新版的类型描述信息中压根就不存在。
// 可以理解为 mandatory 这种写法是 Electron 中特有的写法。
// 如果是用 TS 写的项目,在打包时出现了类型错误,可以暴力处理 - 手动去改类型,或者直接断言类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
3. 💻 demo
js
// index.js
const {
BrowserWindow,
desktopCapturer,
screen,
app,
ipcMain,
} = require('electron')
const { join } = require('path')
let win
function createWindow() {
win = new BrowserWindow({
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: join(__dirname, './preload.js'),
},
})
win.loadFile('./index.html')
win.webContents.openDevTools({ mode: 'detach' })
}
ipcMain.handle('desktop-capturer-get-screen-sources', async (_) => {
const displays = screen.getAllDisplays()
const sources = await desktopCapturer.getSources({ types: ['screen'] })
for (const source of sources) {
const display = displays.filter((d) => +source.display_id === d.id)[0]
return { chromeMediaSourceId: source.id, display }
}
})
app.whenReady().then(createWindow)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
screen.getAllDisplays()
,获取屏幕列表,返回值是Display[]
。desktopCapturer.getSources({ types: ['screen'] })
,通过 desktopCapturer 模块获取屏幕相关信息,其返回值是Promise<DesktopCapturerSource[]>
,每个DesktopCapturerSource
代表一个屏幕或一个可以被捕获的独立窗口。displays.filter((d) => +source.display_id === d.id)[0]
,从sources
也就是DesktopCapturerSource[]
中找到第一个屏幕,然后直接 return。 该 demo 只需要实现对某个屏幕画面的监听功能即可,并没有加不同屏幕的区分逻辑。return { chromeMediaSourceId: source.id, display }
,将desktopCapturer
返回结果中的sourceId
(一个window
或者screen
的唯一标识)传递给渲染进程,在调用navigator.mediaDevices.getUserMedia
时需要用到,可以作为chromeMediaSourceId
的约束条件。
js
// preload.js
const { ipcRenderer, contextBridge } = require('electron')
const TdahuyouAPI = {
async getScreenStream() {
return await ipcRenderer.invoke('desktop-capturer-get-screen-sources')
}
}
if (process.contextIsolated) {
contextBridge.exposeInMainWorld('TdahuyouAPI', TdahuyouAPI)
} else {
window.TdahuyouAPI = TdahuyouAPI
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
- 通过预加载脚本
preload.js
往渲染进程中注入需要的接口。
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>desktop capturer</title>
</head>
<body>
<h1>实现屏幕监听功能</h1>
<video
style="width: 80vw; height: 80vh; border: 1px solid #ddd"
autoplay
></video>
<script src="./renderer.js"></script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js
// renderer.js
;(async () => {
try {
const { chromeMediaSourceId, display } = await TdahuyouAPI.getScreenStream()
// console.log('chromeMediaSourceId', chromeMediaSourceId)
// console.log('display', display)
// 获取屏幕视频流
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId,
minWidth: display.bounds.width,
maxWidth: display.bounds.width * display.scaleFactor,
minHeight: display.bounds.height,
maxHeight: display.bounds.height * display.scaleFactor,
},
// mandatory 用于设置获取屏幕视频流的具体要求
// 这个配置项会影响最终捞到的视频流的清晰度
// 实际画面的清晰度还和很多其他因素有关,
// 在测试时发现 minWidth、minHeight 如果也乘上
// display.scaleFactor 的话,清晰度会高很多。
},
})
const video = document.querySelector('video')
video.srcObject = stream
video.onloadedmetadata = () => video.play()
} catch (e) {
console.log(e)
}
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
TdahuyouAPI.getScreenStream()
,调用 preload 中暴露的接口,安全地访问浏览器环境之外的 API,获取必要的数据。navigator.mediaDevices.getUserMedia(...)
,实时获取桌面屏幕视频流数据。video.srcObject = stream
、video.onloadedmetadata = () => video.play()
,将视频流数据丢给video
标签,并播放视频流。
最终效果
- 最终效果如下。
- 下面是屏幕 A 上的内容,这个显示屏中所展示的画面数据,将被屏幕 B 监听,可以在 B 上看到 A 的画面。
- 下面是在屏幕 B 上的渲染进程窗口,在这个窗口上可以实时监听屏幕 A 上的内容。
- 如果 B 是另一位用户的电脑屏幕,那么这就基本实现了远程控制工具的一小部分功能。当然,现在的画面监控,仅仅是在本地实现的,并且也没有加入任何交互(远程控制)逻辑。