技术分享| 小程序实现音视频通话
2022/8/5 1:24:01
本文主要是介绍技术分享| 小程序实现音视频通话,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
上一期我们把前期准备工作做完了,这一期就带大家实现音视频通话!
sdk 二次封装
为了更好的区分功能,我分成了六个 js 文件
-
config.js 音视频与呼叫邀请配置
-
store.js 实现音视频通话的变量
-
rtc.js 音视频逻辑封装
-
live-code.js 微信推拉流状态码
-
rtm.js 呼叫邀请相关逻辑封装
-
util.js 其他方法
config.js
配置 sdk 所需的 AppId
,如需私有云可在此配置
-
RTC 音视频相关
-
RTM 实时消息(呼叫邀请)
module.exports = { AppId: "", // RTC 私有云配置 RTC_setParameters: { setParameters: { // //配置私有云网关 // ConfPriCloudAddr: { // ServerAdd: "", // Port: , // Wss: true, // }, }, }, // RTM 私有云配置 RTM_setParameters: { setParameters: { // //配置内网网关 // confPriCloudAddr: { // ServerAdd: "", // Port: , // Wss: true, // }, }, }, }
store.js
整个通话系统使用的变量设置
module.exports = { // 网络状态 networkType: "", // rtm连接状态 rtmNetWorkType: "", // rtc连接状态 rtcNetWorkType: "", // 视频通话0 语音通话1 Mode: 0, // 当前场景 0:首页 1:呼叫页面 2:通信页面 State: 0, // 本地用户uid userId: "", // 远端用户uid peerUserId: "", // 频道房间 channelId: "", // RTM 客户端 rtmClient: null, // RTC 客户端 rtcClient: null, // 本地录制地址(小程序特有推流) livePusherUrl: "", // 远端播放(小程序特有拉流) livePlayerUrl: "", // 主叫邀请实例 localInvitation: null, // 被叫收到的邀请实例 remoteInvitation: null, // 是否正在通话 Calling: false, // 是否是单人通话 Conference: false, // 通话计时 callTime: 0, callTimer: null, // 30s 后无网络取消通话 networkEndCall: null, networkEndCallTime: 30*1000, // 断网发送查询后检测是否返回消息 networkSendInfoDetection: null, networkSendInfoDetectionTime: 10*1000, }
rtc.js
音视频 sdk 二测封装,方便调用
// 引入 RTC const ArRTC = require("ar-rtc-miniapp"); // 引入 until const Until = require("./util"); // 引入 store let Store = require("./store"); // 引入 SDK 配置 const Config = require("./config"); // 初始化 RTC const InItRTC = async () => { // 创建RTC客户端 Store.rtcClient = new ArRTC.client(); // 初始化 await Store.rtcClient.init(Config.AppId); Config.RTC_setParameters.setParameters && await Store.rtcClient.setParameters(Config.RTC_setParameters.setParameters) // 已添加远端音视频流 Store.rtcClient.on('stream-added', rtcEvent.userPublished); // 已删除远端音视频流 Store.rtcClient.on('stream-removed', rtcEvent.userUnpublished); // 通知应用程序发生错误 Store.rtcClient.on('error', rtcEvent.error); // 更新 Url 地址 Store.rtcClient.on('update-url', rtcEvent.updateUrl); // 远端视频已旋转 Store.rtcClient.on('video-rotation', rtcEvent.videoRotation); // 远端用户已停止发送音频流 Store.rtcClient.on('mute-audio', rtcEvent.muteAudio); // 远端用户已停止发送视频流 Store.rtcClient.on('mute-video', rtcEvent.muteVideo); // 远端用户已恢复发送音频流 Store.rtcClient.on('unmute-audio', rtcEvent.unmuteAudio); // 远端用户已恢复发送视频流 Store.rtcClient.on('unmute-video', rtcEvent.unmuteAudio); } // RTC 监听事件处理 const rtcEvent = { // RTC SDK 监听用户发布 userPublished: ({ uid }) => { console.log("RTC SDK 监听用户发布", uid); Store.networkSendInfoDetection && clearTimeout(Store.networkSendInfoDetection); if (Store.Mode == 0) { wx.showLoading({ title: '远端加载中', mask: true, }) } // 订阅远端用户发布音视频 Store.rtcClient.subscribe(uid, (url) => { console.log("远端用户发布音视频", url); // 向视频页面发送远端拉流地址 Until.emit("livePusherUrlEvent", { livePlayerUrl: url }); }, (err) => { console.log("订阅远端用户发布音视频失败", err); }) }, // RTC SDK 监听用户取消发布 userUnpublished: ({ uid }) => { console.log("RTC SDK 监听用户取消发布", uid); Store.networkSendInfoDetection && clearTimeout(Store.networkSendInfoDetection); Store.networkSendInfoDetection = setTimeout(() => { wx.showToast({ title: '对方网络异常', icon: "error" }); setTimeout(() => { rtcInternal.leaveChannel(false); }, 2000) }, Store.networkSendInfoDetectionTime); }, // 更新 Url 地址 updateUrl: ({ uid, url }) => { console.log("包含远端用户的 ID 和更新后的拉流地址", uid, url); // 向视频页面发送远端拉流地址 Until.emit("livePusherUrlEvent", { livePlayerUrl: url }); }, // 视频的旋转信息以及远端用户的 ID videoRotation: ({ uid, rotation }) => { console.log("视频的旋转信息以及远端用户的 ID", uid, rotation); }, // 远端用户已停止发送音频流 muteAudio: ({ uid }) => { console.log("远端用户已停止发送音频流", uid); }, // 远端用户已停止发送视频流 muteVideo: ({ uid }) => { console.log("远端用户已停止发送视频流", uid); }, // 远端用户已恢复发送音频流 unmuteAudio: ({ uid }) => { console.log("远端用户已恢复发送音频流", uid); }, // 远端用户已恢复发送视频流 unmuteAudio: ({ uid }) => { console.log("远端用户已恢复发送视频流", uid); }, // 通知应用程序发生错误。 该回调中会包含详细的错误码和错误信息 error: ({ code, reason }) => { console.log("错误码:" + code, "错误信息:" + reason); }, } // RTC 内部逻辑 const rtcInternal = { // 加入频道 joinChannel: () => { Store.rtcClient.join(undefined, Store.channelId, Store.userId, () => { console.log("加入频道成功", Store.rtcClient); // 发布视频 rtcInternal.publishTrack(); // 加入房间一定时间内无人加入 Store.networkSendInfoDetection && clearTimeout(Store.networkSendInfoDetection); Store.networkSendInfoDetection = setTimeout(() => { wx.showToast({ title: '对方网络异常', icon: "error" }); setTimeout(() => { rtcInternal.leaveChannel(false); }, 2000) }, Store.networkSendInfoDetectionTime); }, (err) => { console.log("加入频道失败"); }); }, // 离开频道 leaveChannel: (sendfase = true) => { console.log("离开频道", sendfase); console.log("RTC 离开频道", Store); Store.networkSendInfoDetection && clearTimeout(Store.networkSendInfoDetection); if (Store.rtcClient) { // 引入 RTM const RTM = require("./rtm"); Store.rtcClient.destroy(() => { console.log("离开频道", RTM); if (sendfase) { // 发送离开信息 RTM.rtmInternal.sendMessage(Store.peerUserId, { Cmd: "EndCall", }) } Until.clearStore(); // 返回首页 wx.reLaunch({ url: '../index/index', success:function () { wx.showToast({ title: '通话结束', icon:'none' }) } }); }, (err) => { console.log("离开频道失败", err); }) } else { Until.clearStore(); } }, // 发布本地音视频 publishTrack: () => { Store.rtcClient.publish((url) => { console.log("发布本地音视频", url); // 本地录制地址(小程序特有推流) Store.livePusherUrl = url; // 向视频页面发送本地推流地址 Until.emit("livePusherUrlEvent", { livePusherUrl: url }); }, ({ code, reason }) => { console.log("发布本地音视频失败", code, reason); }) }, // 切换静音 switchAudio: (enableAudio = false) => { /** * muteLocal 停止发送本地用户的音视频流 * unmuteLocal 恢复发送本地用户的音视频流 */ Store.rtcClient[enableAudio ? 'muteLocal' : 'unmuteLocal']('audio', () => { wx.showToast({ title: enableAudio ? '关闭声音' : '开启声音', icon: 'none', duration: 2000 }) }, ({ code, reason }) => { console.log("发布本地音视频失败", code, reason); }) }, } module.exports = { InItRTC, rtcInternal, }
live-code.js
微信推拉流状态码
module.exports = { 1001: "已经连接推流服务器", 1002: "已经与服务器握手完毕,开始推流", 1003: "打开摄像头成功", 1004: "录屏启动成功", 1005: "推流动态调整分辨率", 1006: "推流动态调整码率", 1007: "首帧画面采集完成", 1008: "编码器启动", "-1301": "打开摄像头失败", "-1302": "打开麦克风失败", "-1303": "视频编码失败", "-1304": "音频编码失败", "-1305": "不支持的视频分辨率", "-1306": "不支持的音频采样率", "-1307": "网络断连,且经多次重连抢救无效,更多重试请自行重启推流", "-1308": "开始录屏失败,可能是被用户拒绝", "-1309": "录屏失败,不支持的Android系统版本,需要5.0以上的系统", "-1310": "录屏被其他应用打断了", "-1311": "Android Mic打开成功,但是录不到音频数据", "-1312": "录屏动态切横竖屏失败", 1101: "网络状况不佳:上行带宽太小,上传数据受阻", 1102: "网络断连, 已启动自动重连", 1103: "硬编码启动失败,采用软编码", 1104: "视频编码失败", 1105: "新美颜软编码启动失败,采用老的软编码", 1106: "新美颜软编码启动失败,采用老的软编码", 3001: "RTMP -DNS解析失败", 3002: "RTMP服务器连接失败", 3003: "RTMP服务器握手失败", 3004: "RTMP服务器主动断开,请检查推流地址的合法性或防盗链有效期", 3005: "RTMP 读/写失败", 2001: "已经连接服务器", 2002: "已经连接 RTMP 服务器,开始拉流", 2003: "网络接收到首个视频数据包(IDR)", 2004: "视频播放开始", 2005: "视频播放进度", 2006: "视频播放结束", 2007: "视频播放Loading", 2008: "解码器启动", 2009: "视频分辨率改变", "-2301": "网络断连,且经多次重连抢救无效,更多重试请自行重启播放", "-2302": "获取加速拉流地址失败", 2101: "当前视频帧解码失败", 2102: "当前音频帧解码失败", 2103: "网络断连, 已启动自动重连", 2104: "网络来包不稳:可能是下行带宽不足,或由于主播端出流不均匀", 2105: "当前视频播放出现卡顿", 2106: "硬解启动失败,采用软解", 2107: "当前视频帧不连续,可能丢帧", 2108: "当前流硬解第一个I帧失败,SDK自动切软解", };
rtm.js
实时消息(呼叫邀请)二次封装。使用 p2p 消息发送接受(信令收发),呼叫邀请
// 引入 anyRTM const ArRTM = require("ar-rtm-sdk"); // 引入 until const Until = require("./util"); // 引入 store let Store = require("./store"); // 引入 SDK 配置 const Config = require("../utils/config"); // 引入 RTC const RTC = require("./rtc"); // 本地 uid 随机生成 Store.userId = Until.generateNumber(4) + ''; // 监听网络状态变化事件 wx.onNetworkStatusChange(function (res) { // 网络状态 Store.networkType = res.networkType // 无网络 if (res.networkType == 'none') { wx.showLoading({ title: '网络掉线了', mask: true }); Store.rtmNetWorkType = ""; // 30s 无网络连接结束当前呼叫 Store.networkEndCall && clearTimeout(Store.networkEndCall); Store.networkEndCall = setTimeout(() => { rtmInternal.networkEndCall(); }, Store.networkEndCallTime); } else { Store.networkEndCall && clearTimeout(Store.networkEndCall); wx.hideLoading(); if (!Store.rtmClient) { // 初始化 InItRtm(); } else { if (!Store.rtcClient) { // 呼叫阶段 let oRtmSetInterval = setInterval(() => { // rtm 链接状态 if (Store.rtmNetWorkType == "CONNECTED") { clearInterval(oRtmSetInterval); Store.networkSendInfoDetection && clearTimeout(Store.networkSendInfoDetection); // 发送信息,查看对方状态 rtmInternal.sendMessage(Store.peerUserId, { Cmd: "CallState", }); // 发送无响应 Store.networkSendInfoDetection = setTimeout(() => { rtmInternal.networkEndCall(); }, Store.networkEndCallTime); } }, 500) } } } }); // 初始化 const InItRtm = async () => { // 创建 RTM 客户端 Store.rtmClient = await ArRTM.createInstance(Config.AppId); Config.RTM_setParameters.setParameters && await Store.rtmClient.setParameters(Config.RTM_setParameters.setParameters) // RTM 版本 console.log("RTM 版本", ArRTM.VERSION); wx.showLoading({ title: '登录中', mask: true }) // 登录 RTM await Store.rtmClient.login({ token: "", uid: Store.userId }).then(() => { wx.hideLoading(); wx.showToast({ title: '登录成功', icon: 'success', duration: 2000 }) console.log("登录成功"); }).catch((err) => { Store.userId = ""; wx.hideLoading(); wx.showToast({ icon: 'error', title: 'RTM 登录失败', mask: true, duration: 2000 }); console.log("RTM 登录失败", err); }); // 监听收到来自主叫的呼叫邀请 Store.rtmClient.on( "RemoteInvitationReceived", rtmEvent.RemoteInvitationReceived ); // 监听收到来自对端的点对点消息 Store.rtmClient.on("MessageFromPeer", rtmEvent.MessageFromPeer); // 通知 SDK 与 RTM 系统的连接状态发生了改变 Store.rtmClient.on( "ConnectionStateChanged", rtmEvent.ConnectionStateChanged ); } // RTM 监听事件 const rtmEvent = { // 主叫:被叫已收到呼叫邀请 localInvitationReceivedByPeer: () => { console.log("主叫:被叫已收到呼叫邀请"); // 跳转至呼叫页面 wx.reLaunch({ url: '../pageinvite/pageinvite?call=0' }); wx.showToast({ title: '被叫已收到呼叫邀请', icon: 'none', duration: 2000, mask: true, }); }, // 主叫:被叫已接受呼叫邀请 localInvitationAccepted: async (response) => { console.log("主叫:被叫已接受呼叫邀请", response); try { const oInfo = JSON.parse(response); // 更改通话方式 Store.Mode = oInfo.Mode; wx.showToast({ title: '呼叫邀请成功', icon: 'success', duration: 2000 }); // anyRTC 初始化 await RTC.InItRTC(); // 加入 RTC 频道 await RTC.rtcInternal.joinChannel(); // 进入通话页面 wx.reLaunch({ url: '../pagecall/pagecall', }); } catch (error) { console.log("主叫:被叫已接受呼叫邀请 数据解析失败", response); } }, // 主叫:被叫拒绝了你的呼叫邀请 localInvitationRefused: (response) => { try { const oInfo = JSON.parse(response); // 不同意邀请后返回首页 rtmInternal.crosslightgoBack(oInfo.Cmd == "Calling" ? "用户正在通话中" : "用户拒绝邀请"); } catch (error) { rtmInternal.crosslightgoBack("用户拒绝邀请") } }, // 主叫:呼叫邀请进程失败 localInvitationFailure: (response) => { console.log("主叫:呼叫邀请进程失败", response); // rtmInternal.crosslightgoBack("呼叫邀请进程失败"); }, // 主叫:呼叫邀请已被成功取消 (主动挂断) localInvitationCanceled: () => { console.log("主叫:呼叫邀请已被成功取消 (主动挂断)"); // 不同意邀请后返回首页 rtmInternal.crosslightgoBack("已取消呼叫"); }, // 被叫:监听收到来自主叫的呼叫邀请 RemoteInvitationReceived: async (remoteInvitation) => { if (Store.Calling) { // 正在通话中处理 rtmInternal.callIng(remoteInvitation); } else { wx.showLoading({ title: '收到呼叫邀请', mask: true, }) // 解析主叫呼叫信息 const invitationContent = await JSON.parse(remoteInvitation.content); if (invitationContent.Conference) { setTimeout(() => { wx.hideLoading(); wx.showToast({ title: '暂不支持多人通话(如需添加,请自行添加相关逻辑)', icon: 'none', duration: 3000, mask: true, }) // 暂不支持多人通话(如需添加,请自行添加相关逻辑) remoteInvitation.refuse(); }, 1500); } else { wx.hideLoading(); Store = await Object.assign(Store, { // 通话方式 Mode: invitationContent.Mode, // 频道房间 channelId: invitationContent.ChanId, // 存放被叫实例 remoteInvitation, // 远端用户 peerUserId: remoteInvitation.callerId, // 标识为正在通话中 Calling: true, // 是否是单人通话 Conference: invitationContent.Conference, }) // 跳转至呼叫页面 wx.reLaunch({ url: '../pageinvite/pageinvite?call=1' }); // 收到呼叫邀请处理 rtmInternal.inviteProcessing(remoteInvitation); } } }, // 被叫:监听接受呼叫邀请 RemoteInvitationAccepted: async () => { console.log("被叫 接受呼叫邀请", Store); wx.showLoading({ title: '接受邀请', mask: true, }) // anyRTC 初始化 await RTC.InItRTC(); // 加入 RTC 频道 await RTC.rtcInternal.joinChannel(); wx.hideLoading() // 进入通话页面 wx.reLaunch({ url: '../pagecall/pagecall', }); }, // 被叫:监听拒绝呼叫邀请 RemoteInvitationRefused: () => { console.log("被叫 拒绝呼叫邀请"); // 不同意邀请后返回首页 rtmInternal.crosslightgoBack("成功拒绝邀请"); }, // 被叫:监听主叫取消呼叫邀请 RemoteInvitationCanceled: () => { console.log("被叫 取消呼叫邀请"); // 不同意邀请后返回首页 rtmInternal.crosslightgoBack("主叫取消呼叫邀请"); }, // 被叫:监听呼叫邀请进程失败 RemoteInvitationFailure: () => { console.log("被叫 呼叫邀请进程失败"); // 不同意邀请后返回首页 rtmInternal.crosslightgoBack("呼叫邀请进程失败"); }, // 收到来自对端的点对点消息 MessageFromPeer: (message, peerId) => { console.log("收到来自对端的点对点消息", message, peerId); message.text = JSON.parse(message.text); switch (message.text.Cmd) { case "SwitchAudio": // 视频通话页面转语音 Until.emit("callModeChange", { mode: 1 }); break; case "EndCall": // 挂断 RTC.rtcInternal.leaveChannel(false); break; case "CallState": // 对方查询本地状态,返回给对方信息 rtmInternal.sendMessage(peerId, { Cmd: "CallStateResult", state: Store.peerUserId !== peerId ? 0 : Store.State, Mode: Store.Mode, }) break; case "CallStateResult": // 远端用户返回信息处理 console.log("本地断网重连后对方状态", message, peerId); Store.networkSendInfoDetection && clearTimeout(Store.networkSendInfoDetection); if (message.text.state == 0 && Store.State != 0) { // 远端停止通话,本地还在通话 rtmInternal.networkEndCall(); } else if (message.text.state == 2) { Store.Mode = message.text.Mode; // 远端 rtc 通话 if (Store.State == 1) { // 本地 rtm 呼叫中进入RTC console.log("本地 rtm 呼叫中进入RTC",Store); } else if (Store.State == 2) { // 本地 rtc 通话 if (message.text.Mode == 1) { // 转语音通话 Until.emit("callModeChange", { mode: 1 }); } } } break; default: console.log("收到来自对端的点对点消息", message, peerId); break; } }, // 通知 SDK 与 RTM 系统的连接状态发生了改变 ConnectionStateChanged: (newState, reason) => { console.log("系统的连接状态发生了改变", newState); Store.rtmNetWorkType = newState; switch (newState) { case "CONNECTED": wx.hideLoading(); // SDK 已登录 RTM 系统 wx.showToast({ title: 'RTM 连接成功', icon: 'success', mask: true, }) break; case "ABORTED": wx.showToast({ title: 'RTM 停止登录', icon: 'error', mask: true, }); console.log("RTM 停止登录,重新登录"); break; default: wx.showLoading({ title: 'RTM 连接中', mask: true, }) break; } } } // RTM 内部逻辑 const rtmInternal = { // 查询呼叫用户是否在线 peerUserQuery: async (uid) => { const oUserStatus = await Store.rtmClient.queryPeersOnlineStatus([uid]); if (!oUserStatus[uid]) { wx.showToast({ title: '用户不在线', icon: 'error', duration: 2000, mask: true, }); return false; } return true; }, // 主叫发起呼叫 inviteSend: async (callMode) => { Store = await Object.assign(Store, { // 随机生成频道 channelId: '' + Until.generateNumber(9), // 正在通话中 Calling: true, // 通话方式 Mode: callMode, // 创建呼叫邀请 localInvitation: Store.rtmClient.createLocalInvitation( Store.peerUserId ) }) // 设置邀请内容 Store.localInvitation.content = JSON.stringify({ Mode: callMode, // 呼叫类型 视频通话 0 语音通话 1 Conference: false, // 是否是多人会议 ChanId: Store.channelId, // 频道房间 UserData: "", SipData: "", VidCodec: ["H264"], AudCodec: ["Opus"], }); // 事件监听 // 监听被叫已收到呼叫邀请 Store.localInvitation.on( "LocalInvitationReceivedByPeer", rtmEvent.localInvitationReceivedByPeer ); // 监听被叫已接受呼叫邀请 Store.localInvitation.on( "LocalInvitationAccepted", rtmEvent.localInvitationAccepted ); // 监听被叫拒绝了你的呼叫邀请 Store.localInvitation.on( "LocalInvitationRefused", rtmEvent.localInvitationRefused ); // 监听呼叫邀请进程失败 Store.localInvitation.on( "LocalInvitationFailure", rtmEvent.localInvitationFailure ); // 监听呼叫邀请已被成功取消 Store.localInvitation.on( "LocalInvitationCanceled", rtmEvent.localInvitationCanceled ); // 发送邀请 Store.localInvitation.send(); }, // 被叫收到呼叫邀请处理(给收到的邀请实例绑定事件) inviteProcessing: async (remoteInvitation) => { // 监听接受呼叫邀请 remoteInvitation.on( "RemoteInvitationAccepted", rtmEvent.RemoteInvitationAccepted ); // 监听拒绝呼叫邀请 remoteInvitation.on( "RemoteInvitationRefused", rtmEvent.RemoteInvitationRefused ); // 监听主叫取消呼叫邀请 remoteInvitation.on( "RemoteInvitationCanceled", rtmEvent.RemoteInvitationCanceled ); // 监听呼叫邀请进程失败 remoteInvitation.on( "RemoteInvitationFailure", rtmEvent.RemoteInvitationFailure ); }, // 正在通话中处理 callIng: async (remoteInvitation) => { remoteInvitation.response = await JSON.stringify({ // Reason: "Calling", refuseId: Store.ownUserId, Reason: "calling", Cmd: "Calling", }); await remoteInvitation.refuse(); }, // 不同意邀请后返回首页 crosslightgoBack: (message) => { // Store 重置 Until.clearStore(); // 返回首页 wx.reLaunch({ url: '../index/index', }); wx.showToast({ title: message, icon: 'none', duration: 2000, mask: true, }); }, // 发送消息 sendMessage: (uid, message) => { console.log("发送消息", uid, message); Store.rtmClient && Store.rtmClient.sendMessageToPeer({ text: JSON.stringify(message) }, uid).catch(err => { console.log("发送消息失败", err); }); }, // 无网络连接结束当前呼叫 networkEndCall: () => { if (Store.rtcClient) { // RTC 挂断 } else { // 呼叫阶段 let oRtmSetInterval = setInterval(() => { // rtm 链接状态 if (Store.rtmNetWorkType == "CONNECTED") { clearInterval(oRtmSetInterval); // RTM 取消/拒绝呼叫 if (Store.localInvitation) { // 主叫取消呼叫 Store.localInvitation.cancel(); } else if (Store.remoteInvitation) { // 被叫拒绝呼叫 Store.remoteInvitation.refuse(); } } }, 500); } } } module.exports = { InItRtm, rtmInternal, }
util.js
项目中使用的方法封装:
-
时间转化
-
生成随机数
-
音视频通话变量置空
-
计时器
-
深克隆
-
事件监听封装,类似uniapp的 on,emit,remove(off)
const formatTime = date => { const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() const hour = date.getHours() const minute = date.getMinutes() const second = date.getSeconds() return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}` } const formatNumber = n => { n = n.toString() return n[1] ? n : `0${n}` } // 随机生成uid const generateNumber = (len) => { const numLen = len || 8; const generateNum = Math.ceil(Math.random() * Math.pow(10, numLen)); return generateNum < Math.pow(10, numLen - 1) ? generateNumber(numLen) : generateNum; } // 引入 store let Store = require("./store"); // 本地清除 const clearStore = () => { // 通话计时器 Store.callTimer && clearInterval(Store.callTimer); Store = Object.assign(Store, { // 视频通话0 语音通话1 Mode: 0, // 远端用户uid peerUserId: "", // 频道房间 channelId: "", // 是否正在通话 Calling: false, // 是否是单人通话 Conference: false, // 通话计时 callTime: 0, callTimer: null, }) } // 计时器 const calculagraph = () => { Store.callTime++; let oMin = Math.floor(Store.callTime / 60); let oSec = Math.floor(Store.callTime % 60); oMin >= 10 ? oMin : (oMin = "0" + oMin); oSec >= 10 ? oSec : (oSec = "0" + oSec); return oMin + ":" + oSec; } // 深克隆 function deepClone(obj) { if (typeof obj !== 'object') { return obj; } else { const newObj = obj.constructor === Array ? [] : {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { if (obj[key] && typeof obj[key] === 'object') { newObj[key] = deepClone(obj[key]); } else { newObj[key] = obj[key]; } } } return newObj; } } /** * 事件传递 */ // 用来保存所有绑定的事件 const events = {}; // 监听事件 function on(name, self, callback) { // self用来保存小程序page的this,方便调用this.setData()修改数据 const tuple = [self, callback]; const callbacks = events[name]; let isCallback = null; // 判断事件库里面是否有对应的事件 if (Array.isArray(callbacks)) { // 相同的事件不要重复绑定 const selfCallbacks = callbacks.filter(item => { return self === item[0]; }); if (selfCallbacks.length === 0) { callbacks.push(tuple); } else { for (const item of selfCallbacks) { if (callback.toString() !== item.toString()) { isCallback = true; } }!isCallback && selfCallbacks[0].push(callback); } } else { // 事件库没有对应数据,就将事件存进去 events[name] = [tuple]; } } // 移除监听的事件 function remove(name, self) { const callbacks = events[name]; if (Array.isArray(callbacks)) { events[name] = callbacks.filter(tuple => { return tuple[0] !== self; }); } } // 触发监听事件 function emit(name, data = {}) { const callbacks = events[name]; if (Array.isArray(callbacks)) { callbacks.map(tuple => { const self = tuple[0]; for (const callback of tuple) { if (typeof callback === 'function') { // 用call绑定函数调用的this,将数据传递过去 callback.call(self, deepClone(data)); } } }); } } module.exports = { formatTime, generateNumber, clearStore, on, remove, emit, calculagraph }
呼叫邀请页面 pageinvite
pageinvite.wxml
<view class="container"> <image class="icon_back" mode="scaleToFill" src="../img/icon_back.png" /> <view class="details"> <!-- 用户 --> <view style="padding: 80px 0 0;display: flex;flex-direction: column;align-items: center;"> <image class="head_portrait" src="../img/icon_head.png"></image> <text class="text_color">{{uid}}</text> </view> <!-- 加载中 --> <view class="loading"> <image class="img_size" src="../img/animation.png"></image> <text class="text_color m">{{CallFlse ? '收到邀请' : '呼叫中'}} </text> </view> <!-- 操作 --> <view style="width: 100%;"> <!-- 视频操作 --> <view class="operate" wx:if="{{mode == 0 && CallFlse}}"> <view style="visibility: hidden;"> <image class="img_size" src="../img/icon_switch_voice.png"></image> </view> <!-- 视频转语音 --> <view class="loading" bindtap="voiceCall"> <image class="img_size" src="../img/icon_switch_voice.png"></image> <text class="text_color m">转语音</text> </view> </view> <!-- 公共操作 --> <view class="operate m"> <!-- 挂断 --> <view class="loading" bindtap="cancelCall"> <image class="img_size" src="../img/icon_hangup.png"></image> <text class="text_color m">{{CallFlse ?'挂断':'取消'}}</text> </view> <!-- 接听 --> <view class="loading" wx:if="{{CallFlse}}" bindtap="acceptCall"> <image class="img_size" src="../img/icon_accept.png"></image> <text class="text_color m">接听</text> </view> </view> </view> </view> </view>
pageinvite.js(响铃音乐自行添加)
响铃音乐自行添加
// const RTM = require("../../utils/rtm") const Store = require("../../utils/store"); const Until = require("../../utils/util"); // pages/p2ppage/p2ppage.js // 响铃 // const innerAudioContext = wx.createInnerAudioContext(); // let innerAudioContext = null; Page({ /** * 页面的初始数据 */ data: { // 呼叫者 uid: "", // 通话方式 mode: 0, // 主叫/被叫 CallFlse: false, // 响铃 innerAudioContext: null, }, /** * 生命周期函数--监听页面加载 */ onl oad: function (options) { // 响铃音乐 // const innerAudioContext = wx.createInnerAudioContext(); // innerAudioContext.src = "/pages/audio/video_request.mp3"; // innerAudioContext.autoplay = true; // innerAudioContext.loop = true; // innerAudioContext.play(); Store.State = 1; this.setData({ uid: Store.peerUserId, mode: Store.Mode, CallFlse: options.call == 0 ? false : true, innerAudioContext }); }, /** * 生命周期函数--监听页面显示 */ onShow: function () { wx.hideHomeButton(); }, onUnload: function () { console.log("销毁"); // 停止响铃 // this.data.innerAudioContext.destroy(); }, // 取消呼叫 async cancelCall() { if (this.data.CallFlse) { // 被叫拒绝 Store.remoteInvitation && await Store.remoteInvitation.refuse(); } else { // 主叫取消 Store.localInvitation && await Store.localInvitation.cancel(); } }, // 接受邀请 async acceptCall() { if (Store.remoteInvitation) { console.log("接受邀请",Store.remoteInvitation); // 设置响应模式 Store.remoteInvitation.response = await JSON.stringify({ Mode: this.data.mode, Conference: false, UserData: "", SipData: "", }); // 本地模式 Store.Mode = this.data.mode; // 接受邀请 await Store.remoteInvitation.accept(); } }, // 语音接听 async voiceCall() { if (Store.remoteInvitation) { // 设置响应模式 Store.remoteInvitation.response = await JSON.stringify({ Mode: 1, Conference: false, UserData: "", SipData: "", }); // 本地模式 Store.Mode = 1; // 接受邀请 await Store.remoteInvitation.accept(); } } })
语音通话页面 pagecall
pagecall.wxml
<!--pages/pagecall/pagecall.wxml--> <!-- 视频通话 --> <view class="live" wx:if="{{mode === 0}}"> <!-- 可移动 --> <movable-area class="movable-area"> <movable-view direction="all" x="{{windowWidth-140}}" y="20" class="live-pusher"> <!-- 本地录制 --> <live-pusher v-if="{{livePusherUrl}}" url="{{livePusherUrl}}" mode="RTC" autopush bindstatechange="statechange" binderror="error" style="height: 100%;width: 100%;" /> </movable-view> </movable-area> <!-- 远端播放 --> <view class="live-player"> <live-player src="{{livePlayerUrl}}" mode="RTC" autoplay bindstatechange="statechange" binderror="error" style="height: 100%;width: 100%;position: absolute;z-index: -100;"> <!-- 通话计时 --> <cover-view class="calltime text_color">{{calltime}}</cover-view> <!-- 操作 --> <cover-view class="operate"> <cover-view class="operate-item" bindtap="switchAudio"> <cover-image class="operate_img" src="../img/icon_switch_voice.png"></cover-image> <cover-view class="text_color m">切换至语音</cover-view> </cover-view> <cover-view class="operate-item" bindtap="endCall"> <cover-image class="operate_img" src="../img/icon_hangup.png"></cover-image> <cover-view class="text_color m">挂断</cover-view> </cover-view> <cover-view class="operate-item" bindtap="switchCamera"> <cover-image class="operate_img" src="{{devicePosition == 'front' ? '../img/icon_switch.png':'../img/icon_switchs.png'}}"></cover-image> <cover-view class="text_color m"> {{devicePosition == 'front' ? '前' : '后'}}摄像头 </cover-view> </cover-view> </cover-view> </live-player> <!-- style="height: 100%;width: 100%;position: absolute;z-index: -100;" --> </view> </view> <!-- 语音通话 --> <view class="live" style="background-color: rgba(0, 0, 0, 0.5);" wx:else> <!-- 本地推流 关闭摄像头--> <live-pusher style="width: 0px;height: 0px;" mode='RTC' enable-camera='{{false}}' url='{{ livePusherUrl }}' autopush></live-pusher> <!-- 远端拉流 --> <live-player v-if="{{livePlayerUrl}}" style="width: 0px;height: 0px;" autoplay mode='RTC' src='{{ livePlayerUrl }}' binderror="error" bindstatechange="statechange" sound-mode='{{soundMode}}'></live-player> <!-- 远端用户信息 --> <view class="peerinfo"> <image class="icon_head" src="../img/icon_head.png"></image> <text class="text_color m">{{peerid}}</text> </view> <!-- 通话计时 --> <view class="calltime"> <text class="text_color">{{calltime}}</text> </view> <!-- 操作 --> <view class="operate"> <view class="operate-item" bindtap="muteAudio"> <image class="operate_img" src="{{enableMic ? '../img/icon_closeaudio.png' : '../img/icon_openaudio.png' }}"></image> <text class="text_color m">静音</text> </view> <view class="operate-item" bindtap="endCall"> <image class="operate_img" src="../img/icon_hangup.png"></image> <text class="text_color m">挂断</text> </view> <view class="operate-item" bindtap="handsFree"> <image class="operate_img" src="{{soundMode == 'speaker' ? '../img/icon_speakers.png':'../img/icon_speaker.png'}}"></image> <text class="text_color m">免提</text> </view> </view> </view>
pagecall.js
const Until = require("../../utils/util"); const Store = require("../../utils/store"); const RTC = require("../../utils/rtc"); const RTM = require("../../utils/rtm"); const liveCode = require("../../utils/live-code"); Page({ /** * 页面的初始数据 */ data: { // 可用宽度 windowWidth: "", // 通话方式 mode: 0, // 远端uid peerid: "", // 本地录制地址(小程序特有推流) livePusherUrl: "", // 远端播放(小程序特有拉流) livePlayerUrl: "", // 前置或后置,值为front, back devicePosition: 'front', // 开启或关闭麦克风 enableMic: false, // 开启免提 soundMode: 'speaker', calltime: "00:00" }, // 微信组件状态 statechange(e) { if (e.detail.code == 2004) { wx.hideLoading(); } if (e.detail.code != 1006 && e.detail.message) { wx.showToast({ title: liveCode[e.detail.code] || e.detail.message, icon: 'none', }) } console.log('live-pusher code:', e.detail) }, // 微信组件错误 error(e) { console.log(e.detail); switch (e.detail.errCode) { case 10001: wx.showToast({ title: '用户禁止使用摄像头', icon: 'none', duration: 2000 }) break; case 10002: wx.showToast({ title: '用户禁止使用录音', icon: 'none', duration: 2000 }) break; default: break; } }, /** * 生命周期函数--监听页面加载 */ onl oad: function (options) { const _this = this; Store.State = 2; // 推拉流变更 Until.on("livePusherUrlEvent", this, (data) => { _this.setData({ livePusherUrl: data.livePusherUrl ? data.livePusherUrl : _this.data.livePusherUrl, livePlayerUrl: data.livePlayerUrl ? data.livePlayerUrl : _this.data.livePlayerUrl, }) }); // 通话模式变更 Until.on("callModeChange", this, (data) => { _this.setData({ mode: data.mode, }); Store.Mode = data.mode; }) // 可用宽度 try { const oInfo = wx.getSystemInfoSync(); this.setData({ windowWidth: oInfo.windowWidth, mode: Store.Mode, // mode: 1, peerid: Store.peerUserId || '6666', }) // 开启通话计时 Store.callTimer = setInterval(() => { _this.setData({ calltime: Until.calculagraph() }) }, 1000) } catch (error) { console.log("error", error); } }, /** * 生命周期函数--监听页面卸载 */ onUnload: function () { Until.remove("livePusherUrlEvent", this); Until.remove("callModeChange",this); }, // 切换至语音 switchAudio() { this.setData({ peerid: Store.peerUserId, mode: 1, }); Store.Mode = 1; // 发送切换语音消息 RTM.rtmInternal.sendMessage(Store.peerUserId, { Cmd: "SwitchAudio", }) }, // 挂断 endCall() { RTC.rtcInternal.leaveChannel(true); }, // 翻转摄像头 switchCamera() { wx.createLivePusherContext().switchCamera(); this.setData({ devicePosition: this.data.devicePosition == 'front' ? 'back' : 'front' }) }, // 静音 muteAudio() { this.setData({ enableMic: this.data.enableMic ? false : true, }); RTC.rtcInternal.switchAudio(this.data.enableMic); }, // 免提 handsFree() { this.setData({ soundMode: this.data.soundMode == 'speaker' ? 'ear' : 'speaker', }); }, })
体验地址
微信搜索anyRTC视频云
点击AR 呼叫
即可体验小程序版 ARCall
代码地址
文件Call_watch
这篇关于技术分享| 小程序实现音视频通话的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-13微信小程序如何封装接口域名?-icode9专业技术文章分享
- 2024-11-13如何在微信小程序中实现直传功能?-icode9专业技术文章分享
- 2024-11-13如何在小程序的地图组件中添加标记和文字?-icode9专业技术文章分享
- 2024-11-13在微信小程序的地图组件中如何实现自定义标记和气泡?-icode9专业技术文章分享
- 2024-11-01微信小程序教程:零基础入门到实战
- 2024-11-01微信小程序全栈教程:从入门到实践
- 2024-10-31微信小程序怎么实现关注公众号功能-icode9专业技术文章分享
- 2024-10-30微信小程序cover-view,支持bindtap吗-icode9专业技术文章分享
- 2024-10-30微信小程序的cover-image支持bindtap吗-icode9专业技术文章分享
- 2024-10-30微信小程序web-view怎么设置高度?-icode9专业技术文章分享