实现音视频互动
本文介绍如何集成声网实时互动 SDK,通过少量代码从 0 开始实现一个简单的实时互动 App,适用于互动直播和视频通话场景。
首先,你需要了解以下有关音视频实时互动的基础概念:
- 声网实时互动 SDK:由声网开发的、帮助开发者在 App 中实现实时音视频互动的 SDK。
- 频道:用于传输数据的通道,在同一个频道内的用户可以进行实时互动。
- 主播:可以在频道内发布音视频,同时也可以订阅其他主播发布的音视频。
- 观众:可以在频道内订阅音视频,不具备发布音视频权限。
更多概念详见关键概念。
下图展示在 App 中实现音视频互动的基本工作流程:
- 所有用户调用
joinChannel
方法加入频道,并根据需要设置用户角色:- 互动直播:如果用户需要在频道中发流,则设为主播;如果用户只需要收流,则设为观众。
- 视频通话:将所有的用户角色都为主播。
- 加入频道后,不同角色的用户具备不同的行为:
- 所有用户默认都可以接收频道中的音视频流。
- 主播可以在频道内发布音视频流。
- 观众如果需要发流,可在频道内调用
setClientRole
方法修改用户角色,使其具备发流权限。
前提条件
- Node.js 6.9.1 或更高版本。
- 可以访问互联网的 Windows 或 macOS 电脑。如果你的网络环境部署了防火墙,参考应对防火墙限制以正常使用声网服务。
- 一个有效的声网账号以及声网项目。请参考开通服务从声网控制台获得以下信息:
- App ID:声网随机生成的字符串,用于识别你的项目。
- 临时 Token:Token 也称为动态密钥,在客户端加入频道时对用户鉴权。临时 Token 的有效期为 24 小时。
创建项目
在本地文件夹中为 Electron 项目新建一个目录,并在项目根目录下创建以下文件:
package.json
: 用于安装和管理项目依赖项。index.html
:用于设计 App 的用户界面。main.js
:主进程文件。renderer.js
:渲染进程文件,用于实现与声网 Electron SDK 的交互。
集成 SDK
根据实际情况,在以下集成方式中任选一种,在你的项目中集成 SDK。
- npm 集成
- 手动集成
- 在
package.json
文件中添加以下代码:
-
macOS:
JSON{
"name": "electron-demo-app",
"version": "0.1.0",
"author": "your name",
"description": "My Electron app",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"agora_electron": {
"platform": "darwin",
"prebuilt": true
},
"dependencies": { "agora-electron-sdk": "latest" },
"devDependencies": {"electron": "latest" }
} -
Windows:
JSON{
"name": "electron-demo-app",
"version": "0.1.0",
"author": "your name",
"description": "My Electron app",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"agora_electron": {
"platform": "win32",
"prebuilt": true,
"arch": "ia32"
},
"dependencies": { "agora-electron-sdk": "latest" },
"devDependencies": {"electron": "latest" }
}
字段的具体含义如下:
agora-electron-sdk
:声网 Electron SDK 的版本号。设为latest
表示获取最新版本的 SDK,其它版本可在 agora-electron-sdk 中查看。electron
:Electron 的版本号,支持 5.0.0 及以上版本(包括最新版本)。如果使用搭载 M1 芯片的 macOS 设备,Electron 的版本号需要设为 11.0.0 或以上。- (可选)
platform
:默认根据系统选择,macOS 为darwin
,Windows 为win32
。 - (可选)
prebuilt
:默认设为true
,防止出现 Electron 或 Node.js 版本与 SDK 不兼容的问题。 - (可选)
arch
:默认根据系统选择。
由于不同版本的 Electron 对本地环境的要求有差异,运行项目时可能因环境不匹配报错。声网推荐你参考 Electron 官方文档选择合适的 Electron 和 Node.js 版本。
-
在终端中进入项目根目录,运行如下命令安装依赖项:
Shell# macOS
npm install
# Windows
npm install -D --arch=ia32 electron
npm install
- Windows 平台需要先运行
npm install -D --arch=ia32 electron
安装 32 位的 Electron,然后运行npm install
。否则会收到报错:Not a valid win32 application
。 - 如果项目根目录下已有
node_modules
文件夹,建议先删除该文件夹,再运行npm install
,以免出现报错。
除了使用 npm 集成以外,你也可以进入 Electron-SDK 手动编译最新的 SDK,集成到应用中。
创建用户界面
在 index.html
文件中添加以下代码,创建一个只包含本地视频窗口和远端视频窗口的用户界面:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Electron Quickstart</title>
</head>
<body>
<h1>Electron Quickstart</h1>
<!--在界面中添加本地视频窗口 -->
<div
id="join-channel-local-video"
style="width: 300px; height: 300px; float: left"
></div>
<!--在界面中添加远端视频窗口 -->
<div
id="join-channel-remote-video"
style="width: 300px; height: 300px; float: left"
></div>
</body>
<script src="./renderer.js"></script>
</html>
实现步骤
本节介绍如何实现一个实时音视频互动 App。
下图展示了使用声网 RTC SDK 实现音视频互动的基本流程。
下面列出了实现实时音视频互动基本流程的完整代码以供参考,你可以先将 main.js
和 renderer.js
文件的全部内容分别替换为以下代码,参考测试项目中的步骤快速体验实时音视频互动的基础功能,再按照实现流程了解其中的核心 API 调用。
const { app, BrowserWindow } = require("electron");
const path = require("path");
// 如果使用 Electron 9.x 及以上版本,需要将 allowRendererProcessReuse 设为 false
app.allowRendererProcessReuse = false;
function createWindow() {
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// preload: path.join(__dirname, "renderer.js"),
// 将 nodeIntegration 设置 true 以及 contextIsolation 设为 false
nodeIntegration: true,
contextIsolation: false,
},
});
// 加载 index.html 文件的内容
mainWindow.loadFile("./index.html");
// 开启开发者工具
mainWindow.webContents.openDevTools();
}
// 管理 Electron 应用的浏览器窗口
app.whenReady().then(() => {
createWindow();
// 如果当前没有窗口打开,则新建一个窗口(适用于 macOS)
app.on("activate", function () {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// 如果所有窗口都已关闭,则退出 Electron 应用(适用于 Windows)
app.on("window-all-closed", function () {
if (process.platform !== "darwin") app.quit();
});
const {
createAgoraRtcEngine,
ChannelProfileType,
ClientRoleType,
VideoSourceType,
VideoViewSetupMode,
} = require("agora-electron-sdk");
let rtcEngine;
let localVideoContainer;
let remoteVideoContainer;
// 填入你的 App ID
const APPID = "<-- Insert App Id -->";
// 填入你的临时 Token
let token = "<-- Insert Token -->";
// 填入生成 Token 时使用的频道名
const channel = "<-- Insert Channel Name -->";
// 用户 ID,并确保其在频道内的唯一性
let uid = 123;
const EventHandles = {
// 监听本地用户加入频道事件
onJoinChannelSuccess: ({ channelId, localUid }, elapsed) => {
console.log('成功加入频道:' + channelId);
// 本地用户加入频道后,设置本地视频窗口
rtcEngine.setupLocalVideo({
sourceType: VideoSourceType.VideoSourceCameraPrimary,
uid: uid,
view: localVideoContainer,
setupMode: VideoViewSetupMode.VideoViewSetupAdd,
});
},
// 监听远端用户加入频道事件
onUserJoined: ({ channelId, localUid }, remoteUid, elapsed) => {
console.log('远端用户 ' + remoteUid + ' 已加入');
// 远端用户加入频道后,设置远端视频窗口
rtcEngine.setupRemoteVideo(
{
sourceType: VideoSourceType.VideoSourceRemote,
uid: remoteUid,
view: remoteVideoContainer,
setupMode: VideoViewSetupMode.VideoViewSetupAdd,
},
{ channelId },
);
},
// 监听用户离开频道事件
onUserOffline: ( { channelId, localUid }, remoteUid, reason ) => {
console.log('远端用户 ' + remoteUid + ' 已离开频道');
// 远端用户离开频道后,关闭远端视频窗口
rtcEngine.setupRemoteVideo(
{
sourceType: VideoSourceType.VideoSourceRemote,
uid: remoteUid,
view: remoteVideoContainer,
setupMode: VideoViewSetupMode.VideoViewSetupRemove,
},
);
},
};
window.onload = () => {
const os = require("os");
const path = require("path");
localVideoContainer = document.getElementById("join-channel-local-video");
remoteVideoContainer = document.getElementById("join-channel-remote-video");
const sdkLogPath = path.resolve(os.homedir(), "./test.log");
// 创建 RtcEngine 实例
rtcEngine = createAgoraRtcEngine();
// 初始化 RtcEngine 实例
rtcEngine.initialize({
appId: APPID,
logConfig: { filePath: sdkLogPath }
});
// 注册事件回调
rtcEngine.registerEventHandler(EventHandles);
// 视启用视频模块
rtcEngine.enableVideo();
// 开启本地视频预览
rtcEngine.startPreview();
// 使用临时 Token 加入频道
rtcEngine.joinChannel(token, channel, uid, {
// 设置频道场景为直播场景
channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
// 设置用户角色为主播;如果要将用户角色设置为观众,保持默认值即可
clientRoleType: ClientRoleType.ClientRoleBroadcaster,
// 发布麦克风采集的音频
publishMicrophoneTrack: true,
// 发布摄像头采集的视频
publishCameraTrack: true,
// 自动订阅所有音频流
autoSubscribeAudio: true,
// 自动订阅所有视频流
autoSubscribeVideo: true,
});
};
- 将
renderer.js
文件中的<-- Insert App Id -->
、<-- Insert Token -->
和<-- Insert Channel Name -->
分别替换为你在控制台获取到的 App ID、临时 Token,以及生成临时 Token 时填入的频道名。 - 如果你的目标平台为 macOS v10.14 及以上,还需参考获取设备权限在
main.js
中补充设备权限相关代码。
设计主进程
使用 Electron 框架创建一个应用程序,实现基本的 Electron 项目主进程。主进程中的行为包括:
- 导入所需模块
- 创建浏览器窗口
- 加载
index.html
文件 - 处理应用程序生命周期事件
将以下代码复制到 main.js
文件中:
const { app, BrowserWindow } = require("electron");
const path = require("path");
// 如果使用 Electron 9.x 及以上版本,需要将 allowRendererProcessReuse 设为 false
app.allowRendererProcessReuse = false;
function createWindow() {
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// preload: path.join(__dirname, "renderer.js"),
// 将 nodeIntegration 设置 true 以及 contextIsolation 设为 false
nodeIntegration: true,
contextIsolation: false,
},
});
// 加载 index.html 文件的内容
mainWindow.loadFile("./index.html");
// 开启开发者工具
mainWindow.webContents.openDevTools();
}
// 管理 Electron 应用的浏览器窗口
app.whenReady().then(() => {
createWindow();
// 如果当前没有窗口打开,则新建一个窗口(适用于 macOS)
app.on("activate", function () {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// 如果所有窗口都已关闭,则退出 Electron 应用(适用于 Windows)
app.on("window-all-closed", function () {
if (process.platform !== "darwin") app.quit();
});
获取设备权限 (macOS)
(可选)该步骤仅在目标平台为 macOS 时需要执行。macOS 自 v10.14 起,使用摄像头和麦克风前需要检查并获取设备的使用权限,你需要在主进程中调用 Electron 原生的 askForMediaAccess
方法,来向用户请求摄像头和麦克风权限。
在 main.js
文件中添加如下代码:
// 检查并获取设备权限
async function checkAndApplyDeviceAccessPrivilege() {
// 检查并获取摄像头权限
const cameraPrivilege = systemPreferences.getMediaAccessStatus('camera');
console.log(`Camera privilege before applying: ${cameraPrivilege}`);
if (cameraPrivilege !== 'granted') {
await systemPreferences.askForMediaAccess('camera');
console.log('Requested camera access from user');
}
// 检查并获取麦克风权限
const micPrivilege = systemPreferences.getMediaAccessStatus('microphone');
console.log(`Microphone privilege before applying: ${micPrivilege}`);
if (micPrivilege !== 'granted') {
await systemPreferences.askForMediaAccess('microphone');
console.log('Requested microphone access from user');
}
}
checkAndApplyDeviceAccessPrivilege();
导入依赖项
在 renderer.js
中导入声网 SDK 中构建 App 所需的模块和函数。
const {
createAgoraRtcEngine,
ChannelProfileType,
ClientRoleType,
VideoSourceType,
VideoViewSetupMode,
} = require("agora-electron-sdk");
定义基本信息
传入从声网控制台获取的 App ID、临时 Token,以及生成临时 Token 时填入的频道名,用于后续初始化引擎和加入频道。
// 填入你的 App ID
const APPID = "<-- Insert App Id -->";
// 填入你的临时 Token
let token = "<-- Insert Token -->";
// 填入生成 Token 时使用的频道名
const channel = "<-- Insert Channel Name -->";
// 用户 ID,并确保其在频道内的唯一性
let uid = 123;
初始化引擎
在 renderer.js
中调用 createAgoraRtcEngine
方法创建一个 RtcEngine
实例,然后调用 initialize
初始化引擎。
在初始化 SDK 前,需确保终端用户已经充分了解并同意相关的隐私政策。
let rtcEngine;
const os = require("os");
const path = require("path");
const sdkLogPath = path.resolve(os.homedir(), "./test.log");
// 创建 RtcEngine 实例
rtcEngine = createAgoraRtcEngine();
// 初始化 RtcEngine 实例
rtcEngine.initialize({
appId: APPID,
logConfig: { filePath: sdkLogPath }
});
启用视频模块
按照以下步骤启用视频模块:
- 调用
enableVideo
方法,启用视频模块。 - 调用
startPreview
方法,开启本地视频预览。
// 启用视频模块
rtcEngine.enableVideo();
// 开启本地视频预览
rtcEngine.startPreview();
加入频道并发布音视频流
调用 joinChannel
方法,填入你在控制台获取的临时 Token,以及获取 Token 时填入的频道名加入频道,并在 options
参数中设置频道场景、用户角色等信息。
// 使用临时 Token 加入频道
rtcEngine.joinChannel(token, channel, uid, {
// 设置频道场景为直播场景
channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
// 设置用户角色为主播;如果要将用户角色设置为观众,保持默认值即可
clientRoleType: ClientRoleType.ClientRoleBroadcaster,
// 发布麦克风采集的音频
publishMicrophoneTrack: true,
// 发布摄像头采集的视频
publishCameraTrack: true,
// 自动订阅所有音频流
autoSubscribeAudio: true,
// 自动订阅所有视频流
autoSubscribeVideo: true,
});
实现常用回调
调用 registerEventHandler
方法注册以下回调事件,触发事件时使用 console.log
方法提示结果,并执行特定操作:
onJoinChannelSuccess
:成功加入频道回调。本地用户加入频道后,调用setupLocalVideo
设置本地视频窗口。onUserJoined
:远端用户加入当前频道回调。远端用户加入频道后,调用setupRemoteVideo
设置远端视频窗口。onUserOffline
:远端用户离开当前频道回调。远端用户离开频道后,调用setupRemoteVideo
关闭远端视频窗口。
const EventHandles = {
// 监听本地用户加入频道事件
onJoinChannelSuccess: ({ channelId, localUid }, elapsed) => {
console.log('成功加入频道:' + channelId);
// 本地用户加入频道后,设置本地视频窗口
rtcEngine.setupLocalVideo({
sourceType: VideoSourceType.VideoSourceCameraPrimary,
uid: uid,
view: localVideoContainer,
setupMode: VideoViewSetupMode.VideoViewSetupAdd,
});
},
// 监听远端用户加入频道事件
onUserJoined: ({ channelId, localUid }, remoteUid, elapsed) => {
console.log('远端用户 ' + remoteUid + ' 已加入');
// 远端用户加入频道后,设置远端视频窗口
rtcEngine.setupRemoteVideo(
{
sourceType: VideoSourceType.VideoSourceRemote,
uid: remoteUid,
view: remoteVideoContainer,
setupMode: VideoViewSetupMode.VideoViewSetupAdd,
},
{ channelId },
);
},
// 监听用户离开频道事件
onUserOffline: ( { channelId, localUid }, remoteUid, reason ) => {
console.log('远端用户 ' + remoteUid + ' 已离开频道');
// 远端用户离开频道后,关闭远端视频窗口
rtcEngine.setupRemoteVideo(
{
sourceType: VideoSourceType.VideoSourceRemote,
uid: remoteUid,
view: remoteVideoContainer,
setupMode: VideoViewSetupMode.VideoViewSetupRemove,
},
);
},
};
// 注册事件回调
rtcEngine.registerEventHandler(EventHandles);
离开频道
根据场景需要,如关闭应用或应用切换至后台时,调用 leaveChannel
离开当前频道。
// 停止预览
rtcEngine.stopPreview();
// 离开频道
rtcEngine.leaveChannel();
如果你不再需要互动,调用 release
方法释放引擎资源。
// 取消注册回调
rtcEngine.unregisterEventHandler(EventHandles);
// 销毁引擎
rtcEngine.release();
调用 release
后,你将无法再使用 SDK 的所有方法和回调。如需再次使用实时音视频互动功能,你必须重新创建一个新的引擎。详见初始化引擎。
测试项目
参考以下步骤测试你的 Electron 项目:
-
确保已经在代码中传入了有效的
appId
,channelName
和token
。详见开通服务。 -
在终端中进入项目根目录,运行以下命令:
Shellnpm start
项目成功运行后,你会看到一个自动弹出的窗口,在窗口中看到本地的视频画面。
-
邀请一位朋友使用第二台设备,重复以上步骤,在该设备上安装 App,并打开 App 加入频道。
-
你的朋友成功加入后,你们可以听见、看见对方。
后续步骤
- 本文的示例使用了临时 Token 加入频道。在测试或生产环境中,为确保通信安全,声网推荐使用 Token 服务器来生成 Token,详见使用 Token 鉴权。
- 如果你想要实现极速直播场景,可以在互动直播的基础上,通过修改观众端的延时级别为低延时 (
audienceLatencyLevelLowLatency
)实现。详见实现极速直播。
参考信息
示例项目
声网提供了开源的实时音视频互动示例项目供你参考,你可以前往下载或查看其中的源代码。
常见问题
- Electron 平台常见开发问题
- 直播场景下,如何监听远端观众角色用户加入/离开频道的事件?
- 如何处理视频黑屏问题?
- 为什么我无法打开摄像头?
- 如何处理频道相关常见问题?
- 如何设置日志文件?
- 编译 Xcode 项目时遇到无法打开 framework 的弹窗警告怎么办?
- 运行 Electron App 时,控制台提示 “WARNING:Too many active WebGL contexts” 怎么办?
- 为什么 SDK 中使用的是动态库而不是静态库?