本文介绍如何在 Electron 框架项目中集成 小鱼易连 Web SDK / Web MeetingKit,实现多人音视频通话与屏幕共享功能。通过文档,开发者将了解Electron 适配要点,以及相关权限、配置和兼容说明。
WebRTC 本身运行在浏览器环境下,而 Electron 虽然基于 Chromium,但其运行机制与安全策略与浏览器有所不同,因此在集成 SDK 时需要额外处理。例如:
提示
✅ Web SDK:提供完整的音视频能力,适用于网页和 Electron 环境。
✅ Web MeetingKit:封装了核心逻辑和 UI,适合快速开发,Electron 下已全面适配。
提示
1、自 v4.0.7+ 版本起,SDK 与 MeetingKit 在 Electron 环境下全面支持呼叫和屏幕共享,Electron最低兼容 v22 版本;
2、Electron v28+ 开始支持系统声音采集;
详细的Electron版本兼容如下:
系统 | Electron版本 | 支持的架构 | 发送/接收 | 屏幕分享 | 屏幕分享采集系统声音 |
Windows | v22-v27 | x64、ia32 | ✅ | ✅ | ❌ |
v28+ | ✅ | ✅ | ✅ | ||
Mac | v22-v27 | x64、arm64 | ✅ | ✅ | ❌ |
v28+ | ✅ | ✅ | ✅ | ||
Linux | v22-v27 | ✅ | ✅ | ❌ | |
v28+ | ✅ | ✅ | ✅ |
小鱼易连建议使用最新版本的 Electron,以获得最佳兼容性和最新浏览器内核能力。
1. 环境准备:创建Electron运行环境。
2. 基础集成: 渲染进程中引入 Web SDK / MeetingKit,实现通话逻辑。
3. 安全配置: 配置 BrowserWindow,确保启用 contextIsolation
并关闭 nodeIntegration
。
4. 桥接 API: 通过preload.js + ipcMain
暴露 Electron 的桌面捕获和权限 API 给渲染进程。
5. 处理权限【仅适用SDK】: 监听特定的错误码,引导用户在系统设置中授予必要权限。
6. 共享内容:在Electron下分享屏幕和窗口。
确保您已创建一个基本的 Electron 项目,包含 main.js, preload.js, index.html 等文件,并引入React框架来快速集成SDK或者MeetingKit库。
如果没有,可以参见小鱼易连Web MeetingKit Electron Demo项目,包含完整的项目结构、路由、主进程架构、全平台打包配置;
在您的 Electron 渲染进程页面,例如React
组件中,集成 SDK 或者 MeetingKit 并实现基础通话逻辑。此部分可以参考通用视频通话集成文档:
完成此步骤后,已可在 Electron 中实现基本的呼叫与会议功能。
接下来是针对Electron环境下的特有配置和处理步骤,请仔细阅读;
在Electron下创建 Browser Window 时,必须开启上下文隔离并禁用 Node.js 集成,以增强安全性。这是在 Electron 中正确运行 SDK 的前提。
在主进程文件(如 main.js
)中:
const { BrowserWindow } = require('electron');
const mainWindow = new BrowserWindow({
webPreferences: {
// 禁用 Node.js 集成
nodeIntegration: false,
// 启用上下文隔离
contextIsolation: true,
// 指定预加载脚本
preload: path.join(__dirname, 'preload.js'),
},
});
由于安全隔离,渲染进程无法直接调用 Electron API,需要通过 Preload + IPC 暴露接口。主要涉及到:获取分享源列表、获取媒体访问状态、请求媒体权限、跳转系统权限设置页面、请求关闭窗口。
创建 preload.js
文件,暴露必要的 Electron API 给渲染进程,SDK 内部需要通过这些 API 来获取屏幕源和权限状态。
代码可直接复制使用,无需额外修改;
// preload.js
import { ipcRenderer, contextBridge } from 'electron';
/**
* 小鱼易连IPC API
*/
const XY_ELECTRON_API = {
/**
* 获取桌面捕获器
*/
desktopCapturer: {
getSources: async (options) => {
try {
const result = await ipcRenderer.invoke('DESKTOP_CAPTURER:GET_SOURCES', options);
return result.data;
} catch (error) {
console.error('Preload: Failed to get desktop sources:', error);
throw error;
}
},
/**
* 获取媒体访问状态
*/
getMediaAccessStatus: async (type = 'screen') => {
return await ipcRenderer.invoke('DESKTOP_CAPTURER:GET_MEDIA_ACCESS_STATUS', type);
},
/**
* 请求媒体权限
*/
requestPermissions: async (type = 'camera') => {
return await ipcRenderer.invoke('DESKTOP_CAPTURER:REQUEST_PERMISSIONS', type);
},
/**
* 跳转系统设置
*/
goToSystemSettings: async (type) => {
return await ipcRenderer.invoke('DESKTOP_CAPTURER:GO_TO_SYSTEM_SETTINGS', type);
},
/**
* 请求关闭窗口
* Web MeetingKit需要定义和实现,WebSDK 场景下不需要
*/
requestCloseWindow: async () => {
return await ipcRenderer.invoke('DESKTOP_CAPTURER:REQUEST_CLOSE_WINDOW');
},
},
};
/**
* 主进程暴露API到渲染进程,方便渲染进程调用
*/
contextBridge.exposeInMainWorld('electronAPI', {
// 小鱼指定ICP 方法
xylink: XY_ELECTRON_API
});
在对应IPC文件或者定义监听ipc方法的文件下实现对应的逻辑;
代码可直接复制使用,无需额外修改;
// ipc.js或者监听ipc文件下实现
import { ipcMain, BrowserWindow, app, desktopCapturer, systemPreferences, shell } from 'electron';
// 获取桌面捕获源
ipcMain.handle('DESKTOP_CAPTURER:GET_SOURCES', async (event, options) => {
try {
const sources = await desktopCapturer.getSources(options);
const serializedSources = sources.map((source) => {
const { thumbnail = null, appIcon = null } = source;
const isHasThumbnail = thumbnail && !thumbnail.isEmpty();
const isHasAppIcon = appIcon && !appIcon.isEmpty();
return {
...source,
thumbnail: isHasThumbnail ? thumbnail.toPNG() : null,
appIcon: !isHasThumbnail && isHasAppIcon ? appIcon.toPNG() : null,
};
});
return { success: true, data: serializedSources };
} catch (error) {
logger.error('IPC handle error: desktop-capturer:getSources', error);
return { success: false, error };
}
});
// 检查媒体权限状态
ipcMain.handle('DESKTOP_CAPTURER:GET_MEDIA_ACCESS_STATUS', async (event, type) => {
try {
if (process.platform === 'darwin') {
const status = await systemPreferences.getMediaAccessStatus(type);
return status;
}
return 'granted';
} catch (error) {
logger.error('Failed to check media permissions', error);
return 'granted';
}
});
// 请求获取麦克风和摄像头权限,并返回最新的权限状态
ipcMain.handle('DESKTOP_CAPTURER:REQUEST_PERMISSIONS', async (event, type) => {
try {
let status = 'granted';
// 如果是macOS系统,则需要请求权限
if (process.platform === 'darwin') {
if (type === 'camera') {
status = await systemPreferences.getMediaAccessStatus('camera');
// 如果权限未授权,尝试请求权限
if (status === 'not-determined') {
await systemPreferences.askForMediaAccess('camera');
status = await systemPreferences.getMediaAccessStatus('camera');
}
} else if (type === 'microphone') {
status = await systemPreferences.getMediaAccessStatus('microphone');
if (status === 'not-determined') {
await systemPreferences.askForMediaAccess('microphone');
status = await systemPreferences.getMediaAccessStatus('microphone');
}
} else {
status = systemPreferences.getMediaAccessStatus(type);
}
}
// 返回权限状态和额外信息
return status;
} catch (error) {
logger.error('IPC handle error: desktop-capturer:getMediaAccessStatus', error);
return 'granted';
}
});
// 跳转系统设置
ipcMain.handle('DESKTOP_CAPTURER:GO_TO_SYSTEM_SETTINGS', async (event, type) => {
if (process.platform === 'darwin') {
const macOSVersion = Number(os.release().split('.')[0]);
if (macOSVersion >= 22) {
// macOS 13 (Ventura) and above
const privacyMap: Record<string, string> = {
camera: 'Privacy_Camera',
microphone: 'Privacy_Microphone',
screen: 'Privacy_ScreenCapture',
};
let baseUrl = 'x-apple.systempreferences:com.apple.preference.security?';
baseUrl += privacyMap[type] || 'Privacy_ScreenCapture';
shell.openExternal(baseUrl);
} else {
// macOS 12 and below
shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy');
}
}
});
// 请求关闭窗口
// Web MeetingKit需要定义和实现,WebSDK 场景下不需要
ipcMain.handle('DESKTOP_CAPTURER:REQUEST_CLOSE_WINDOW', async () => {
// 关闭会议窗口,如果业务不需要,则直接处理退会即可
const currentWindow = BrowserWindow.getFocusedWindow();
if (currentWindow) {
currentWindow.destroy();
}
});
此步骤仅适用于Web SDK集成,Web MeetingKit已经内置逻辑;
当完成上述步骤后,首次加入会议并开启摄像头/麦克风,SDK会调用IPC方法获取设备权限并进行授权调用,系统会自动弹出授权框进行授权处理。开发者需要监听对应的错误码,来处理权限拒绝等异常情况,并给出提示或者再次授权逻辑。
videoAudioTrack
的 track-error
事件并返回错误码,摄像头拒绝错误码:XYSDK:950411
,麦克风拒绝错误码:XYSDK:950412
开发者需监听并提示用户在 系统设置 → 隐私与安全 中手动开启权限。
import { XYMessage, uiMeetingKit } from '@xylink/meetingkit';
import { XYPermissionType } from '@xylink/xy-rtc-sdk';
peopleTrack.on('track-error', (e) => {
const { msg = '', detail, code } = e;
const { CAMERA, MICROPHONE } = XYPermissionType;
// 摄像头和麦克风同时采集时都失败
if (code === 'XYSDK:950414') {
const { videoError, audioError } = detail;
const videoAudioMsg = '需要开启摄像头和麦克风权限!请打开 偏好设置->隐私与安全 并在相应的选项下勾选,勾选后按照系统提示退出并重启App。';
const videoMsg = '需要开启摄像头权限!请打开 偏好设置->隐私与安全 并在相应的选项下勾选,勾选后按照系统提示退出并重启App。';
const audioMsg = '需要开启麦克风权限!请打开 偏好设置->隐私与安全 并在相应的选项下勾选,勾选后按照系统提示退出并重启App。';
if (videoError && videoError.code === 'XYSDK:950411' && audioError && audioError.code === 'XYSDK:950412') {
// 摄像头和麦克风使用权限被拒绝
const message = i18n.t(videoAudioMsg);
showPermissionDialog(message, CAMERA);
return;
} else if (videoError && videoError.code === 'XYSDK:950411') {
// 摄像头使用权限被拒绝
const message = i18n.t(videoMsg);
showPermissionDialog(message, CAMERA);
} else if (audioError && audioError.code === 'XYSDK:950412') {
// 麦克风使用权限被拒绝
const message = i18n.t(audioMsg);
showPermissionDialog(message, MICROPHONE);
}
} else if (code === 'XYSDK:950412') {
// 麦克风使用权限被拒绝
const message = i18n.t(audioMsg);
showPermissionDialog(message, MICROPHONE);
} else if (code === 'XYSDK:950411') {
// 摄像头使用权限被拒绝
const message = i18n.t(videoMsg);
showPermissionDialog(message, CAMERA);
}
msg && XYMessage.info(msg);
});
/**
* 权限弹框
*
* @param { string } message - 提示信息
* @param { XYPermissionType } type - 权限类型
*/
showPermissionDialog(message: string, type: XYPermissionType) {
const isElectron = uiMeetingKit.isElectron;
if (isElectron) {
// 弹框提示
XYDialog.show({
contentTitle: "权限提醒",
drag: true,
maskClosable: false,
mask: false,
okText: "去授权",
children: message,
onOk: () => {
const desktopCapturer = XYRTCClient.getDesktopCapturer();
if (desktopCapturer) {
desktopCapturer.goToSystemSettings(type);
} else {
XYMessage.info(i18n.t("Electron环境所需的方法未定义");
}
}
});
} else {
XYMessage.info(message, 5);
}
}
未授权点击开启摄像头效果如下:
初始系统授权框:
当Electron环境下调用 getElectronContentSources 方法时,SDK内部会开始检测屏幕录制权限。
contentTrack
的 track-error
事件并返回错误码 XYSDK:950530
开发者需监听并引导用户去授权:
import { XYDialog, XYMessage } from '@xylink/meetingkit';
import { XYPermissionType } from '@xylink/xy-rtc-sdk';
contentTrack.on('track-error', (e: IReturnResult) => {
const { msg = '', code = '' } = e;
// 请在系统设置中授予屏幕录制权限!
if (code === 'XYSDK:950530') {
XYDialog.show({
contentTitle: "权限提醒",
drag: true,
maskClosable: false,
mask: true,
okText: "去授权",
children: "需要开启录屏与系统录音权限!请打开 偏好设置->隐私与安全->录屏与系统录音 并在相应的选项下勾选,勾选后按照系统提示退出并重启App。",
onOk: () => {
const desktopCapturer = XYRTCClient.getDesktopCapturer();
if (desktopCapturer) {
desktopCapturer.goToSystemSettings(XYPermissionType.SCREEN);
} else {
XYMessage.info("Electron环境所需的方法未定义");
}
}
});
return;
} else if (code === 'XYSDK:950532' || code === 'XYSDK:950533') {
// 950532:Electron采集屏幕声音失败,业务侧进行提示或者禁用音频采集
// 950533:未定义desktopCapturer方法
XYMessage.info(msg);
return;
} else {
msg && XYMessage.info(msg || "分享屏幕失败");
}
// 停止分享屏幕
contentTrack.close();
});
初始系统授权框:
去授权提醒框:
此步骤仅适用于Web SDK集成,Web MeetingKit已经内置逻辑;
详细见文档:Electron环境下支持共享内容
BrowserWindow 配置
、Preload + IPC 桥接
、权限处理
;