当前位置: 首页 > news >正文

H5使用环信实现视频或语音通话

前言:切记在针对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/

附代码:

// 声网通话管理器
import AgoraRTC from 'agora-rtc-sdk-ng'class AgoraCallManager {constructor(appId = 'bcc4e0d8e5cc473c9789337311577e66') {this.client = nullthis.localTracks = {audioTrack: null,videoTrack: null}this.remoteUsers = new Map()this.isJoined = falsethis.isPublished = false// 在 join 完成前缓存需要订阅的远端发布事件,避免“user is not in the channel”this.pendingSubscriptions = []this.currentChannelName = ''// 声网配置// 使用提供的App IDthis.APP_ID = appIdconsole.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_IDconst appId = this.APP_IDconsole.log('准备加入频道,参数:', {appId: appId,channelName: channelName,token: token,uid: uid})// 确保传递所有必要参数:appId, channelName, token(可选), uidif (!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必须使用Tokenif (!token) {console.warn('警告: 已启用正式证书的AppID必须使用Token')// 不抛出错误,但记录警告}// 与服务端 buildTokenWithUid 一致:将纯数字字符串转为数字;空则为0let finalUid = uidif (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 = truethis.currentChannelName = channelNameconsole.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 = trueconsole.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 = trueconsole.log('视频轨道发布成功')return true} catch (error) {console.error('发布视频轨道失败:', error)return false}}// 发布音视频轨道async publishTracks() {try {// 创建麦克风和摄像头轨道const [microphoneTrack, cameraTrack] = await AgoraRTC.createMicrophoneAndCameraTracks()this.localTracks.audioTrack = microphoneTrackthis.localTracks.videoTrack = cameraTrack// 发布轨道到频道await this.client.publish([this.localTracks.audioTrack, this.localTracks.videoTrack])this.isPublished = trueconsole.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 = falseconsole.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) returnif (!this.pendingSubscriptions.length) returnconsole.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 = nullthis.localTracks.videoTrack = nullthis.isJoined = falsethis.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

http://www.dtcms.com/a/478565.html

相关文章:

  • SMTPman高效稳定的smtp服务器使用指南解析
  • 《Qt应用开发》笔记p3
  • Java-148 深入浅出 MongoDB 聚合操作:$match、$group、$project、$sort 全面解析 Pipeline 实例详解与性能优化
  • Oops 概念
  • 用老域名做新网站 权重怎么传递哈尔滨网站建设公司哪家好
  • Servlet内存马
  • 为什么要使用反射举例
  • python开发生态及学习路线和应用领域都有哪些
  • bk7258 交叉编译libzip-1.11.4
  • 汽车级mosfet的应用场景
  • 手机做ppt的免费模板下载网站深圳自适应网站的公司
  • svn 库 co 下来有白叉
  • Windows安全狗安装教程
  • 深度解析:OpenCASCADE 中平面闭合轮廓的离散点提取
  • 河源盛世网站建设丽水市建设监理协会网站在哪里
  • 衡阳做网站建设的公司在哪里查关键词排名
  • linux学习笔记(30)网络编程——TCP协议详解
  • ICT 数字测试原理 21 - -VCL中的板级预处理
  • 学校要求做网站做网站要源代码
  • 项目缺乏成功衡量标准会导致什么问题
  • 2025年的12大技术栈
  • 越南国家建设部网站企业站手机网站
  • Qt6.7.2下,qml中Window组件全屏加载WebEngineView实现圆角
  • Struts2_S2-045漏洞复现:原理详解+环境搭建+渗透实践(CVE-2017-5638)
  • 【慕伏白】Android Studio 无线调试配置
  • 厦门方易网站制作有限公司做网站对象存储
  • 【Docker】零基础上手:原理+Ubuntu/Windows GUI 安装 + 镜像源 / 目录优化
  • 网站的引导页怎么做的手机虚拟空间
  • 大连网站开发公司力推选仟亿科技有源码如何搭建网站
  • 【Java虚拟机(JVM)全面解析】从原理到面试实战、JVM故障处理、类加载、内存区域、垃圾回收