**前言:**切记在针对web端时环信本身自带的组件通话是不支持vue的,如果想在web端使用环信封装好的视频或语音通话,开发框架要选择为react
【vue项目使用视频和语音通话(PS:需要搭配声网RTC创建本地频道进行简历连接)】
先说逻辑后附代码
1、加入和离开频道:
调用 AgoraRTC.createClient 即可创建 AgoraRTCClient
对象。在创建 AgoraRTCClient
时,需要指定使用的编码格式
2、创建本地轨道对象(使用麦克风和摄像头)
最常用的方法是直接通过麦克风或者摄像头采集的音视频来创建本地轨道对象,SDK 提供了三种方法:
- createCameraVideoTrack:使用摄像头采集的视频来创建本地视频轨道,返回一个 CameraVideoTrack 对象。
- createMicrophoneAudioTrack:使用麦克风采集的音频来创建本地音频轨道,返回一个 MicrophoneAudioTrack 对象。
- createMicrophoneAndCameraTracks:同时使用麦克风和摄像头采集的音视频创建本地轨道,返回一个包含 CameraVideoTrack 和 MicrophoneAudioTrack 的列表。
3、发布和订阅(这一步需要注意的是要在双方都加入频道后再订阅,不然就会出现失败的情况)
完成本地轨道的创建并且成功加入频道后,就可以调用 AgoraRTCClient.publish 将本地的音视频数据发布到当前频道,以供频道中的其他用户订阅。
4、使用 Token 鉴权
这一步前端直接调用后端的接口,获取对应的频道token去建立,传入对方的用户id即可
步骤顺序不要错,根据声网提供的文档也可以更加仔细的去看:https://doc.shengwang.cn/
附代码:
javascript
// 声网通话管理器
import AgoraRTC from 'agora-rtc-sdk-ng'
class AgoraCallManager {
constructor(appId = 'bcc4e0d8e5cc473c9789337311577e66') {
this.client = null
this.localTracks = {
audioTrack: null,
videoTrack: null
}
this.remoteUsers = new Map()
this.isJoined = false
this.isPublished = false
// 在 join 完成前缓存需要订阅的远端发布事件,避免"user is not in the channel"
this.pendingSubscriptions = []
this.currentChannelName = ''
// 声网配置
// 使用提供的App ID
this.APP_ID = appId
console.log('AgoraCallManager 初始化,AppID:', this.APP_ID)
}
// 初始化声网客户端
async initClient() {
try {
// 检查AppID是否有效
if (!this.APP_ID || typeof this.APP_ID !== 'string' || this.APP_ID.trim() === '') {
throw new Error('无效的AppID: ' + this.APP_ID)
}
console.log('开始初始化声网客户端,App ID:', this.APP_ID)
// 创建RTC客户端
console.log('创建RTC客户端,使用AppID:', this.APP_ID)
try {
this.client = AgoraRTC.createClient({
mode: "rtc",
codec: "vp8"
})
console.log('RTC客户端创建成功')
} catch (err) {
console.error('RTC客户端创建失败:', err)
throw err
}
// 设置事件监听
this.setupEventListeners()
console.log('声网客户端初始化成功')
return true
} catch (error) {
console.error('声网客户端初始化失败:', error)
return false
}
}
// 设置事件监听
setupEventListeners() {
// 用户加入频道
this.client.on("user-published", async (user, mediaType) => {
console.log('用户发布媒体流:', user.uid, mediaType)
// 标准流程:在 user-published 回调内订阅
if (!this.isJoined) {
// 未加入则暂存,等待加入后重试
this.pendingSubscriptions.push({ user, mediaType, retry: 0 })
console.log('尚未加入频道,缓存订阅,当前缓存数:', this.pendingSubscriptions.length)
return
}
this._trySubscribe(user, mediaType)
})
// 用户离开频道
this.client.on("user-unpublished", (user, mediaType) => {
console.log('用户取消发布媒体流:', user.uid, mediaType)
if (mediaType === "video") {
this.remoteUsers.delete(user.uid)
this.onRemoteVideoTrackRemoved(user)
}
})
// 用户离开频道
this.client.on("user-left", (user) => {
console.log('用户离开频道:', user.uid)
this.remoteUsers.delete(user.uid)
this.onRemoteVideoTrackRemoved(user)
})
// 网络质量变化
this.client.on("network-quality", (stats) => {
console.log('网络质量:', stats)
})
// 连接状态变化
this.client.on("connection-state-change", (curState, revState) => {
console.log('连接状态变化:', curState, revState)
})
}
// 加入频道
async joinChannel(channelName, token, uid) {
try {
if (!this.client) {
await this.initClient()
}
// 使用当前实例的APP_ID
const appId = this.APP_ID
console.log('准备加入频道,参数:', {
appId: appId,
channelName: channelName,
token: token,
uid: uid
})
// 确保传递所有必要参数:appId, channelName, token(可选), uid
if (!channelName) {
throw new Error('频道名不能为空')
}
// 验证频道名格式
// 声网频道名要求:长度不超过64字节,仅支持特定字符
const validChars = /^[a-zA-Z0-9 !#$%&()+\-:;<=>.\?@\[\]\^_{}|~,]*$/;
if (channelName.length > 64) {
console.error('频道名长度超过64字节限制');
throw new Error('频道名长度超过限制,请使用更短的频道名');
}
if (!validChars.test(channelName)) {
console.error('频道名包含不支持的字符');
console.error('支持的字符: a-z,A-Z,0-9,空格,!,#,$,%,&,(,),+,-,:,;,<,=,.,>,?,@,[,],^,_,{,},|,~,,');
throw new Error('频道名包含不支持的字符,请检查频道名格式');
}
// 已启用正式证书的AppID必须使用Token
if (!token) {
console.warn('警告: 已启用正式证书的AppID必须使用Token')
// 不抛出错误,但记录警告
}
// 与服务端 buildTokenWithUid 一致:将纯数字字符串转为数字;空则为0
let finalUid = uid
if (typeof finalUid === 'string' && /^\d+$/.test(finalUid)) {
finalUid = Number(finalUid)
}
if (finalUid === undefined || finalUid === null || finalUid === '') {
finalUid = 0
}
// 这里使用appId, channelName, token, finalUid四个参数
// 与官方文档保持一致: client.join(appid, channel, token, uid)
console.log('准备加入频道,最终参数:', {
appId,
channelName,
token: token || 'null',
finalUid,
finalUidType: typeof finalUid
})
try {
// 确保Token与AppID匹配
if (appId === 'bcc4e0d8e5cc473c9789337311577e66' && !token) {
console.error('错误: 已启用正式证书的AppID必须使用Token')
throw new Error('已启用正式证书的AppID必须使用Token')
}
// 打印详细的token信息用于调试
if (token) {
console.log('Token详细信息:')
console.log('- Token长度:', token.length)
console.log('- Token前20字符:', token.substring(0, 20))
console.log('- Token是否包含"=":', token.includes('='))
}
try {
await this.client.join(appId, channelName, token, finalUid)
console.log('成功调用join方法')
} catch (joinError) {
console.error('调用join方法失败详细信息:', joinError)
if (joinError.code === 'CAN_NOT_GET_GATEWAY_SERVER' && joinError.message.includes('invalid token')) {
console.error('Token验证失败,可能原因:')
console.error('1. Token已过期')
console.error('2. Token与AppID或ChannelName不匹配')
console.error('3. Token格式不正确')
console.error('建议:')
console.error('- 重新从服务器获取最新Token')
console.error('- 确保Token是针对当前AppID和频道名生成的')
} else if (joinError.code === 'INVALID_PARAMS') {
console.error('参数无效,可能原因:')
if (joinError.message.includes('length must be within 64 bytes')) {
console.error('频道名长度超过64字节限制')
}
if (joinError.message.includes('supported characters')) {
console.error('频道名包含不支持的字符')
console.error('支持的字符: a-z,A-Z,0-9,空格,!,#,$,%,&,(,),+,-,:,;,<,=,.,>,?,@,[,],^,_,{,},|,~,,')
}
console.error('建议:')
console.error('- 使用更短的频道名')
console.error('- 仅使用支持的字符')
console.error('- 当前频道名:', channelName)
}
throw joinError
}
} catch (err) {
console.error('调用join方法失败:', err)
console.error('错误代码:', err.code)
console.error('错误消息:', err.message)
// 针对特定错误提供更详细的信息
if (err.code === 'CAN_NOT_GET_GATEWAY_SERVER') {
console.error('可能原因: AppID无效或Token与AppID不匹配')
console.error('解决方案: 1. 确认AppID是否正确')
console.error(' 2. 确认Token是否与AppID匹配')
console.error(' 3. 确认Token是否已过期')
}
throw err
}
this.isJoined = true
this.currentChannelName = channelName
console.log('成功加入频道:', channelName)
// 在加入频道后发一个全局事件,包含频道名/uid,供双方比对
uni.$emit('rtcJoined', { channelName, uid: finalUid })
// 加入成功后可由页面触发 flush 或等待 user-published 再次触发订阅
return true
} catch (error) {
console.error('加入频道失败:', error)
console.error('错误详情:', {
code: error.code,
message: error.message,
appId: this.APP_ID,
channelName: channelName,
token: token,
uid: uid
})
return false
}
}
// 离开频道
async leaveChannel() {
try {
if (this.isPublished) {
await this.unpublishTracks()
}
if (this.isJoined) {
await this.client.leave()
this.isJoined = false
}
this.cleanup()
console.log('成功离开频道')
return true
} catch (error) {
console.error('离开频道失败:', error)
return false
}
}
// 发布音频轨道
async publishAudioTrack() {
try {
// 创建麦克风音频轨道
this.localTracks.audioTrack = await AgoraRTC.createMicrophoneAudioTrack()
// 发布音频轨道到频道
await this.client.publish([this.localTracks.audioTrack])
this.isPublished = true
console.log('音频轨道发布成功')
return true
} catch (error) {
console.error('发布音频轨道失败:', error)
return false
}
}
// 发布视频轨道
async publishVideoTrack() {
try {
// 创建摄像头视频轨道
this.localTracks.videoTrack = await AgoraRTC.createCameraVideoTrack()
// 发布视频轨道到频道
await this.client.publish([this.localTracks.videoTrack])
this.isPublished = true
console.log('视频轨道发布成功')
return true
} catch (error) {
console.error('发布视频轨道失败:', error)
return false
}
}
// 发布音视频轨道
async publishTracks() {
try {
// 创建麦克风和摄像头轨道
const [microphoneTrack, cameraTrack] = await AgoraRTC.createMicrophoneAndCameraTracks()
this.localTracks.audioTrack = microphoneTrack
this.localTracks.videoTrack = cameraTrack
// 发布轨道到频道
await this.client.publish([this.localTracks.audioTrack, this.localTracks.videoTrack])
this.isPublished = true
console.log('音视频轨道发布成功')
return true
} catch (error) {
console.error('发布音视频轨道失败:', error)
return false
}
}
// 取消发布轨道
async unpublishTracks() {
try {
if (this.localTracks.audioTrack) {
this.localTracks.audioTrack.close()
this.localTracks.audioTrack = null
}
if (this.localTracks.videoTrack) {
this.localTracks.videoTrack.close()
this.localTracks.videoTrack = null
}
this.isPublished = false
console.log('轨道取消发布成功')
return true
} catch (error) {
console.error('取消发布轨道失败:', error)
return false
}
}
// 获取本地视频轨道
getLocalVideoTrack() {
return this.localTracks.videoTrack
}
// 获取本地音频轨道
getLocalAudioTrack() {
return this.localTracks.audioTrack
}
// 获取远程用户
getRemoteUsers() {
return Array.from(this.remoteUsers.values())
}
// 尝试订阅,若报 INVALID_REMOTE_USER 则退避重试
async _trySubscribe(user, mediaType, retry = 0) {
if (!this.isJoined) {
this.pendingSubscriptions.push({ user, mediaType, retry })
return
}
try {
// 订阅前再等待一个短暂延迟,确保对方加入完成
if (retry === 0) {
await new Promise(r => setTimeout(r, 150))
}
await this.client.subscribe(user, mediaType)
if (mediaType === 'video') {
this.remoteUsers.set(user.uid, user)
this.onRemoteVideoTrack(user)
}
if (mediaType === 'audio') {
user.audioTrack.play()
}
} catch (error) {
const msg = (error && (error.message || error.toString())) || ''
console.error('订阅用户媒体流失败:', error)
// user is not in the channel → 说明对方尚未完全加入或频道尚未对齐,退避重试
if (msg.includes('INVALID_REMOTE_USER') || msg.includes('user is not in the channel')) {
if (retry < 5) {
const delay = 300 * Math.pow(2, retry) // 指数退避
console.log(`订阅重试(${retry + 1}),将在 ${delay}ms 后再次尝试,uid=${user.uid}, type=${mediaType}`)
setTimeout(() => this._trySubscribe(user, mediaType, retry + 1), delay)
return
}
}
}
}
// 在确认双方都在同一频道后,由页面调用该方法统一执行订阅
async flushPendingSubscriptions() {
if (!this.isJoined) return
if (!this.pendingSubscriptions.length) return
console.log('flushPendingSubscriptions 开始,数量:', this.pendingSubscriptions.length)
const cached = this.pendingSubscriptions.splice(0)
for (const item of cached) {
await this._trySubscribe(item.user, item.mediaType, item.retry || 0)
}
console.log('flushPendingSubscriptions 完成')
}
// 处理远程视频轨道
onRemoteVideoTrack(user) {
// 这个方法需要在组件中重写
console.log('远程视频轨道可用:', user.uid)
}
// 处理远程视频轨道移除
onRemoteVideoTrackRemoved(user) {
// 这个方法需要在组件中重写
console.log('远程视频轨道移除:', user.uid)
}
// 清理资源
cleanup() {
this.remoteUsers.clear()
this.localTracks.audioTrack = null
this.localTracks.videoTrack = null
this.isJoined = false
this.isPublished = false
}
// 静音/取消静音
async muteAudio(mute) {
if (this.localTracks.audioTrack) {
await this.localTracks.audioTrack.setMuted(mute)
}
}
// 关闭/开启摄像头
async muteVideo(mute) {
if (this.localTracks.videoTrack) {
await this.localTracks.videoTrack.setMuted(mute)
}
}
// 切换摄像头
async switchCamera() {
if (this.localTracks.videoTrack) {
await this.localTracks.videoTrack.setDevice("camera")
}
}
// 获取设备列表
async getDevices() {
try {
const devices = await AgoraRTC.getDevices()
return devices
} catch (error) {
console.error('获取设备列表失败:', error)
return []
}
}
}
export default AgoraCallManager