目录
- 什么是WebRTC?
- WebRTC的工作原理
- 核心技术概念
- [为什么使用Agora SDK](#为什么使用Agora SDK)
- 项目中的三种应用场景
- 技术实现细节
- 常见问题与优化
- 总结
什么是WebRTC?
初识WebRTC
想象一下,你和朋友在不同的城市,想要通过互联网进行视频通话。传统的方式可能是:你的视频先发送到服务器,服务器再转发给朋友。这种方式存在一个问题:延迟较高,而且服务器需要处理大量的视频数据。
WebRTC(Web Real-Time Communication,网页实时通信)就是为了解决这个问题而诞生的技术。它允许你的浏览器直接与对方的浏览器建立连接 ,无需经过中间服务器转发视频流,从而实现低延迟、高质量的实时音视频通信。
WebRTC的特点
- 实时性强:延迟通常在几百毫秒以内,接近面对面交流的体验
- P2P连接:端到端直接通信,减少服务器负担
- 开源标准:由Google主导,W3C标准化,各大浏览器都支持
- 音视频处理能力:内置音频降噪、回声消除、视频编码等功能
WebRTC的工作原理
简单的类比:邮寄包裹的过程
让我们用邮寄包裹来理解WebRTC的工作流程:
- 收集信息:你要知道朋友的地址(网络地址)
- 协商路线:通过邮局(信令服务器)交换地址信息
- 建立连接:确定最优路线(建立P2P连接)
- 发送包裹:开始发送包裹(传输音视频数据)
- 保持沟通:持续监控连接状态,确保包裹顺利送达
技术流程详解
第一步:获取媒体流(Media Stream)
javascript
// 获取用户的摄像头和麦克风
const stream = await navigator.mediaDevices.getUserMedia({
video: true, // 启用视频
audio: true // 启用音频
});
这一步相当于"打开你的摄像头和麦克风"。
第二步:建立连接(RTCPeerConnection)
WebRTC使用RTCPeerConnection对象来建立连接。但问题是:你的浏览器不知道对方的网络地址,这就需要**信令服务器(Signaling Server)**的帮助。
信令服务器的作用:
- 交换双方的网络信息(IP地址、端口等)
- 协商音视频编码格式
- 协调连接建立过程
注意:信令服务器只负责"介绍",不负责传输音视频数据。真正的音视频数据是直接在两个浏览器之间传输的。
第三步:ICE候选(ICE Candidates)
由于网络环境的复杂性(NAT、防火墙等),浏览器需要通过**ICE(Interactive Connectivity Establishment)**来找到可用的网络路径。
浏览器会尝试多种方式建立连接:
- 本地网络:如果在同一局域网
- STUN服务器:帮助发现公网IP
- TURN服务器:如果直连失败,作为中继
第四步:数据交换(Data Exchange)
一旦建立连接,音视频数据就可以开始传输了。
核心技术概念
1. 音视频轨道(Tracks)
Audio Track(音频轨道) :来自麦克风的音频数据
Video Track(视频轨道):来自摄像头的视频数据
一个媒体流可以包含多个轨道,例如:
- 一个音频轨道(麦克风)
- 一个视频轨道(摄像头)
2. 信令(Signaling)
信令是用来交换网络信息和控制消息的机制。常见方式:
- WebSocket:实时双向通信
- Socket.IO:基于WebSocket的封装
- HTTP轮询:简单的请求-响应模式
3. STUN和TURN服务器
- STUN(Session Traversal Utilities for NAT):帮助发现公网IP地址
- TURN(Traversal Using Relays around NAT):当直连失败时,作为中继服务器转发数据
4. SDP(Session Description Protocol)
SDP是描述媒体会话信息的协议,包含:
- 支持的音视频编码格式
- 网络地址信息
- 其他会话参数
为什么使用Agora SDK
虽然WebRTC是浏览器原生支持的,但直接使用原生API会遇到很多问题:
原生WebRTC的挑战
- 浏览器兼容性:不同浏览器API差异大
- 代码复杂度:需要处理STUN/TURN、ICE、信令等复杂逻辑
- 音视频处理:降噪、回声消除等需要自己实现
- 跨平台支持:Web、iOS、Android需要分别处理
- 服务器搭建:需要自己搭建和维护TURN服务器
Agora SDK的优势
Agora(声网)是一个专业的实时音视频云服务提供商,他们的SDK解决了上述所有问题:
- 统一API:一套代码支持多个平台
- 高质量传输:全球CDN节点,自动选择最优路径
- 音视频增强:内置降噪、回声消除、美颜等功能
- 简单易用:几行代码就能实现音视频通话
- 稳定可靠:企业级服务保障
在项目中的使用
我们项目使用agora-rtc-sdk-ng(Agora RTC SDK for Web),这是Agora官方提供的Web版本SDK。
项目中的三种应用场景
我们的项目基于Agora SDK实现了三种不同的实时通信场景。每种场景都有不同的特点和实现方式。
1v1视频通话
场景描述
这是最常见的视频通话场景:两个人互相视频聊天,就像微信视频通话一样。
特点:
- 双方都可以看到对方
- 双方都可以听到对方
- 双方都要发布(publish)自己的音视频
- 双方都要订阅(subscribe)对方的音视频
技术实现
1v1视频通话实现如下。
关键配置:
typescript
// 创建RTC客户端,使用rtc模式
client: createClient({ mode: 'rtc', codec: 'vp8' })
为什么使用'rtc'模式?
rtc(Real-Time Communication)模式是专门为实时通信设计的:
- 所有参与者都可以发布和订阅媒体流
- 低延迟优化
- 适合双向通信场景
实现流程
第一步:加入频道
typescript
// 加入RTC频道
await rtc.client.join(
appId, // Agora应用ID
channel, // 频道名称(房间号)
token, // 安全令牌
uid // 用户ID
);
第二步:创建并发布本地音视频
typescript
// 创建本地音视频轨道
const [localAudioTrack, localVideoTrack] = await createMicrophoneAndCameraTracks(
{}, // 音频配置
{ facingMode: 'user' } // 视频配置(前置摄像头)
);
// 保存轨道
rtc.localAudioTrack = localAudioTrack;
rtc.localVideoTrack = localVideoTrack;
// 发布到频道
await rtc.client.publish([localAudioTrack, localVideoTrack]);
第三步:监听远程用户
typescript
// 监听用户发布事件
rtc.client.on('user-published', async (user, mediaType) => {
// 订阅远程用户的媒体流
await rtc.client.subscribe(user, mediaType);
if (mediaType === 'video') {
// 播放远程视频
user.videoTrack?.play('remote-player-container');
}
if (mediaType === 'audio') {
// 播放远程音频
user.audioTrack?.play();
}
});
代码示例:加入视频通话
在face-time.vue中:
typescript
function joinChannel() {
const userId = userStore.userInfo.userId;
join({
channel: faceTimeStore.currentChannel!.channelName, // 频道名
token: faceTimeStore.currentChannel!.rtcToken, // Token
uid: userId, // 用户ID
success(rtc) {
// 加入成功后的回调
// 渲染本地摄像头
rtc.localVideoTrack!.play('local-player-container');
}
});
}
关键特性
-
双发布双订阅:
- 用户A发布自己的音视频 → 用户B订阅用户A的音视频
- 用户B发布自己的音视频 → 用户A订阅用户B的音视频
-
本地预览:
- 在发布的同时,可以在页面上预览自己的视频
-
动态管理:
- 监听
user-left事件,当对方离开时自动处理
- 监听
1v1直播
场景描述
这是典型的直播场景:一个人(主播)在直播,很多人(观众)在观看。
特点:
- 只有主播发布音视频
- 观众只能订阅(观看)主播的流
- 观众不能发布自己的音视频
- 一对多或一对一的关系(可以只有一个观众)
技术实现
1v1直播使用src/utils/agora-service-live.ts文件实现。
关键配置:
typescript
// 创建RTC客户端,使用live模式
client: createClient({ mode: 'live', codec: 'vp8' })
// 默认设置为观众角色
client.setClientRole('audience', { level: 1 })
为什么使用'live'模式?
live模式是专门为直播场景设计的:
- 支持角色区分:主播(host)和观众(audience)
- 主播可以发布流,观众只能订阅
- 适合一对多的广播场景
实现流程
第一步:加入频道(作为观众)
typescript
// 加入频道,默认是观众角色
await rtc.client.join(appId, channel, token, uid);
第二步:监听主播的流
typescript
// 监听主播发布流的事件
rtc.client.on('user-published', async (user, mediaType) => {
// 订阅主播的媒体流
await rtc.client.subscribe(user, mediaType);
if (mediaType === 'video' && user.videoTrack) {
// 播放主播的视频
const remoteVideoTrack = user.videoTrack;
remoteVideoTrack.play('live-video');
// 监听视频状态变化
remoteVideoTrack.on('video-state-changed', (event) => {
if (event === 0) {
// 检测到黑屏,尝试重新播放
remoteVideoTrack.stop();
await rtc.client.subscribe(user, 'video');
remoteVideoTrack.play('live-video');
}
if (event === 2) {
// 视频正常播放
onPlay(event);
}
});
}
if (mediaType === 'audio' && user.audioTrack) {
// 播放主播的音频
const remoteAudioTrack = user.audioTrack;
remoteAudioTrack.setVolume(150); // 设置音量
remoteAudioTrack.play();
}
});
代码示例:加入直播间
在src/pages/index/components/living/living-room/home.vue中:
typescript
const joinAgora = async (showRoomNo, token) => {
const userId = userStore.userInfo?.userId;
// 加入声网频道
await retryJoinAgora(showRoomNo, token, userId, 1);
// 处理视频流回调
const onPlayCallback = (eventState) => {
if (eventState === 2) {
// 视频流正在解码,正常播放
loading.value = false;
inCall.value = false;
}
};
// 获取音视频流
getLiveStream(null, onPlayCallback, onConnectCallback);
};
关键特性
-
角色区分:
- 观众角色:只能订阅,不能发布
- 主播角色:可以发布,也可以订阅其他主播(连麦场景)
-
流状态监听:
- 监听视频状态变化,处理黑屏、卡顿等问题
- 自动重连机制
-
连接状态管理:
- 监听连接状态变化,处理断线重连
-
预加载机制:
- 使用
preloadAgora函数提前加载频道,减少延迟
- 使用
typescript
async function preloadAgora(options: { channel: string; token: string; uid: string }) {
await preload(
appId,
options.channel,
options.token,
options.uid
);
}
多语音房(MG房间)
场景描述
这是多人在线互动的场景:一个房间里有多个人,可以开启摄像头和麦克风进行互动。类似于视频会议或语音聊天室。
特点:
- 多人可以同时发布音视频
- 支持动态加入和离开
- 有角色区分:管理员、主播、普通用户
- 需要管理多个视频窗口的位置
技术实现
多语音房使用src/utils/agora-service-mg.ts文件实现。
关键配置:
typescript
// 创建RTC客户端,使用live模式
mgRtc.client = createClient({
mode: 'live', // 直播模式
codec: 'vp8' // 视频编码格式
});
// 设置小流参数(用于节省带宽)
client.setLowStreamParameter({
width: 160,
height: 120,
framerate: 15,
bitrate: 120
});
// 开启双流模式(大流+小流)
client.enableDualStream();
// 默认设置为观众角色
client.setClientRole('audience');
为什么使用'live'模式?
- 支持动态的角色切换(观众↔主播)
- 可以同时有多人发布流
- 适合复杂的多对多场景
实现流程
第一步:加入房间
typescript
const joinMGRoom = async (options: {
channel: string; // 房间号
token: string; // 房间令牌
uid: string; // 用户ID
}) => {
const client = initMGClient(); // 初始化客户端
// 清理之前的连接
if (connStates.includes(client.connectionState)) {
await client.leave();
}
// 清理所有视频容器
clearAllVideoContainers();
// 加入房间
await client.join(appId, options.channel, options.token, options.uid);
};
第二步:监听多用户流
typescript
const getMGLiveStream = async (onPlayCallback, onConnectCallback) => {
const client = initMGClient();
// 监听用户发布流
client.on('user-published', async (user, mediaType) => {
await client.subscribe(user, mediaType);
const userId = user.uid.toString();
// 根据userId获取渲染位置
const containerId = getRenderPosition(userId);
// 播放媒体流到指定位置
playMedia(user, containerId, mediaType);
// 执行播放回调
onPlayCallback && onPlayCallback(2, user, user.uid);
});
// 监听用户取消发布
client.on('user-unpublished', async (user, mediaType) => {
const userId = user.uid.toString();
if (mediaType === 'audio') {
// 更新麦克风UI状态
updateMicUI(userId, false);
}
if (mediaType === 'video') {
// 清理视频容器
clearVideoContainer(getRenderPosition(userId));
}
await client.unsubscribe(user, mediaType);
});
};
第三步:管理用户位置
typescript
// 成员位置映射:userId -> position
let memberPositions: Record<string, number> = {};
// 根据userId获取渲染位置
const getRenderPosition = (userId: string): string => {
const position = memberPositions[userId];
if (position == null) return '';
// 根据位置返回对应的容器ID
if (position === 6) return 'admin-video'; // 管理员位置
if (position === 7) return 'user-video'; // 用户位置
return `host-video-${position + 1}`; // 主播位置
};
第四步:发布本地流(当需要上麦时)
typescript
const publishLocalTracks = async (onConfirmPublishedCallback) => {
// 创建本地音视频轨道
const [localAudioTrack, localVideoTrack] = await createMicrophoneAndCameraTracks();
mgRtc.localAudioTrack = localAudioTrack;
mgRtc.localVideoTrack = localVideoTrack;
// 切换为主播角色
await mgRtc.client.setClientRole('host');
// 发布视频流
if (mgRoomStore.cameraStatus) {
await mgRtc.client.publish([mgRtc.localVideoTrack]);
}
// 发布音频流
if (mgRoomStore.micStatus) {
await mgRtc.client.publish([mgRtc.localAudioTrack]);
}
// 播放本地视频
await mgRtc.localVideoTrack!.play('user-video');
};
关键特性
-
多用户管理:
- 维护用户ID到位置的映射关系
- 动态分配视频容器位置
- 处理用户加入/离开事件
-
角色动态切换:
- 观众可以随时上麦(切换到host角色)
- 上麦后可以发布音视频
- 下麦后切换回观众角色
-
媒体流管理:
- 单独控制音频和视频的发布
- 可以只开音频不开视频(语音聊天)
- 可以只开视频不开音频(静音模式)
-
视频容器管理:
- 管理员位置:
admin-video - 用户自己位置:
user-video - 其他主播位置:
host-video-1~host-video-6
- 管理员位置:
-
UI状态同步:
- 根据音频流状态显示麦克风图标
- 自动更新UI反馈
-
双流模式:
- 支持大小流切换,节省带宽
- 根据网络情况自动选择
技术实现细节
三种场景对比
| 特性 | 1v1视频通话 | 1v1直播 | 多语音房 |
|---|---|---|---|
| 模式 | rtc |
live |
live |
| 角色 | 无需角色 | audience(观众) |
audience/host(观众/主播) |
| 发布流 | 双方都发布 | 只有主播发布 | 多人可发布 |
| 订阅流 | 双方都订阅 | 观众订阅主播 | 所有人订阅所有发布者 |
| 适用场景 | 视频通话 | 直播观看 | 多人互动 |
代码结构
/utils/
├── agora-service.ts # 1v1视频通话服务
├── agora-service-live.ts # 1v1直播服务
└── agora-service-mg.ts # 多语音房服务
每个服务都提供以下核心方法:
join()- 加入频道/房间leave()- 离开频道/房间getLiveStream()- 获取并处理媒体流- 其他辅助方法(切换摄像头、控制音视频开关等)
核心事件监听
所有场景都需要监听以下关键事件:
- user-joined:用户加入频道
- user-left:用户离开频道
- user-published:用户发布媒体流
- user-unpublished:用户取消发布媒体流
- connection-state-change:连接状态变化
常见问题与优化
1. 黑屏问题
问题:视频播放时出现黑屏
解决方案:
typescript
// 监听视频状态变化
remoteVideoTrack.on('video-state-changed', async (event) => {
if (event === 0) {
// 检测到黑屏,尝试重新播放
remoteVideoTrack.stop();
await rtc.client.subscribe(user, 'video');
remoteVideoTrack.play('live-video');
}
});
2. 连接断开重连
问题:网络不稳定导致连接断开
解决方案:
typescript
// 监听连接状态变化
rtc.client.on('connection-state-change', async (curState, revState) => {
if (curState === 'DISCONNECTED' && revState === 'CONNECTED') {
// 重新订阅所有用户的流
for (const user of rtc.client.remoteUsers) {
await rtc.client.subscribe(user, 'video');
user.videoTrack?.play('live-video');
}
}
});
3. 权限问题
问题:用户拒绝授予摄像头/麦克风权限
解决方案:
typescript
try {
const [localAudioTrack, localVideoTrack] = await createMicrophoneAndCameraTracks();
// ... 发布流
} catch (error: any) {
if (error.message?.includes('PERMISSION_DENIED')) {
// 显示权限提示对话框
confirmDialog.show({
content: '请允许访问麦克风和摄像头权限'
});
}
}
4. 多频道切换
问题:从一个频道切换到另一个频道时,可能还存在旧连接
解决方案:
typescript
// 在加入新频道前,检查并退出旧频道
const connStates = ['CONNECTING', 'CONNECTED', 'RECONNECTING'];
if (connStates.includes(rtc.client.connectionState)) {
await rtc.client.leave();
}
5. 内存泄漏
问题:页面关闭时没有正确清理资源
解决方案:
typescript
onUnload(async () => {
// 关闭本地轨道
rtc.localAudioTrack?.close();
rtc.localVideoTrack?.close();
// 离开频道
await rtc.client.leave();
// 移除所有事件监听
rtc.client.removeAllListeners();
});
6. 预加载优化
问题:进入直播间时等待时间过长
解决方案:
typescript
// 提前预加载频道,减少延迟
async function preloadAgora(options: { channel: string; token: string; uid: string }) {
await preload(appId, options.channel, options.token, options.uid);
}
总结
WebRTC技术总结
WebRTC是一项强大的实时通信技术,它:
- 实现了浏览器间的直接通信
- 提供了低延迟、高质量的音视频传输
- 是构建实时通信应用的基础
项目应用总结
在我们的项目中:
- 1v1视频通话 :使用
rtc模式,实现双向视频通话,适合点对点交流 - 1v1直播 :使用
live模式,观众角色,实现直播观看,适合内容展示 - 多语音房 :使用
live模式,支持多角色切换,实现多人互动,适合社交场景
技术选型
选择Agora SDK而不是原生WebRTC的原因:
- 简化开发流程
- 提供稳定的服务保障
- 内置丰富的音视频处理功能
- 跨平台统一API
最佳实践
- 错误处理:完善的错误捕获和用户提示
- 状态管理:合理使用Store管理通话状态
- 资源清理:页面卸载时正确释放资源
- 用户体验:提供加载状态、错误提示等反馈
- 性能优化:使用预加载、双流模式等技术