23、屏幕共享Electron应用:Electron桌面捕获、主进程与渲染进程通信、打包部署
说实话,做WebRTC屏幕共享这么久,我一直觉得Electron是个很妙的载体。你想想看,浏览器里做屏幕共享,权限限制多,功能也受限。但换成Electron,咱们就能直接调用操作系统的桌面捕获API,想抓哪个窗口就抓哪个窗口,想录全屏就录全屏。嗯,这章我就带你把这个流程完整走一遍。
Electron桌面捕获的核心机制
Electron的桌面捕获,说白了就是通过desktopCapturer这个模块来获取屏幕源。我在项目中遇到过一个问题:用户想共享某个特定窗口,但列表里死活找不到。后来发现是没在BrowserWindow的webPreferences里开启enableRemoteModule。这个坑我踩过,所以先给你提个醒。
核心API:desktopCapturer.getSources(options)
这个方法会返回一个数组,每个元素代表一个可捕获的屏幕源。每个源包含name、id、thumbnail等信息。
// 主进程代码
const { desktopCapturer } = require('electron');
async function getScreenSources() {
const sources = await desktopCapturer.getSources({
types: ['screen', 'window'], // 捕获屏幕和窗口
thumbnailSize: { width: 320, height: 240 } // 缩略图大小
});
return sources.map(source => ({
id: source.id,
name: source.name,
thumbnail: source.thumbnail.toDataURL()
}));
}
这里有个细节:types参数可以传['screen']只捕获整个屏幕,也可以传['window']只捕获窗口。我个人习惯两个都传,让用户自己选。
主进程与渲染进程的通信设计
Electron应用里,主进程负责系统级操作(比如桌面捕获),渲染进程负责UI展示。这两者怎么通信?我用的是ipcMain和ipcRenderer。说白了就是一套事件机制。
为什么会这样设计?因为安全。渲染进程不能直接调用系统API,必须通过主进程中转。我曾经见过有人图省事,在渲染进程里直接require('electron'),结果打包后各种报错。嗯,这个坑你别踩。
主进程监听请求
// main.js - 主进程
const { ipcMain, desktopCapturer } = require('electron');
ipcMain.handle('GET_SOURCES', async (event) => {
const sources = await desktopCapturer.getSources({
types: ['screen', 'window'],
thumbnailSize: { width: 320, height: 240 }
});
return sources.map(source => ({
id: source.id,
name: source.name,
thumbnail: source.thumbnail.toDataURL()
}));
});
渲染进程发起请求
// renderer.js - 渲染进程
const { ipcRenderer } = require('electron');
async function loadSources() {
const sources = await ipcRenderer.invoke('GET_SOURCES');
// 渲染到UI
sources.forEach(source => {
const div = document.createElement('div');
div.innerHTML = `
<img src="${source.thumbnail}" />
<p>${source.name}</p>
`;
div.onclick = () => selectSource(source.id);
document.getElementById('source-list').appendChild(div);
});
}
小技巧:如果你需要频繁通信,可以用webContents.send()从主进程主动推送数据给渲染进程。比如实时更新屏幕捕获的帧率信息。
将屏幕流传递给WebRTC
拿到桌面源之后,怎么把它塞进WebRTC的RTCPeerConnection?流程是这样的:
- 渲染进程拿到源ID后,通过
navigator.mediaDevices.getUserMedia获取流 - 把流添加到
RTCPeerConnection中 - 创建Offer/Answer,建立连接
// 渲染进程 - 获取屏幕流
async function startScreenShare(sourceId) {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sourceId,
maxWidth: 1920,
maxHeight: 1080,
maxFrameRate: 30
}
}
});
// 添加到PeerConnection
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
});
// 本地预览
document.getElementById('local-video').srcObject = stream;
} catch (err) {
console.error('获取屏幕流失败:', err);
}
}
注意:在Electron中,chromeMediaSource必须设置为'desktop',否则无法捕获桌面。另外,audio: false是因为屏幕共享一般不包含系统音频,如果你需要,可以单独处理。
打包部署:从开发到生产
开发完了怎么打包?我用的是electron-builder。这个工具能帮你生成Windows的exe、macOS的dmg、Linux的AppImage。配置起来也不复杂。
安装依赖
npm install --save-dev electron-builder
配置package.json
{
"name": "screen-share-app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder"
},
"build": {
"appId": "com.example.screenshare",
"productName": "屏幕共享工具",
"directories": {
"output": "dist"
},
"win": {
"target": "nsis",
"icon": "assets/icon.ico"
},
"mac": {
"target": "dmg",
"icon": "assets/icon.icns"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
}
}
运行npm run build,等几分钟,dist目录下就会生成安装包。我第一次打包时忘了配置appId,结果报错说缺少标识符。嗯,这个字段是必填的,别漏了。
避坑指南
- 权限问题:macOS上需要开启屏幕录制权限,否则
desktopCapturer返回空数组。我当初调试了半天才发现是权限没开。 - 打包后路径问题:开发时用
__dirname没问题,打包后路径会变。建议用app.getAppPath()获取应用根目录。 - 窗口捕获性能:如果用户有多个显示器,捕获整个屏幕时帧率可能下降。我建议限制最大帧率为15fps,用户体验会好很多。
知识体系总览
下面这张图把整个流程串起来了,你可以对照着看:
这张图里,主进程和渲染进程通过IPC通信,各司其职。桌面捕获在主进程完成,WebRTC连接在渲染进程建立。打包时用electron-builder把整个应用封装成可安装的程序。
好了,这一章的内容就这些。你跟着代码走一遍,应该能跑通一个完整的屏幕共享Electron应用。如果遇到问题,回头看看避坑指南,大部分坑我都帮你踩过了。