写点什么

技术分享| 小程序实现音视频通话

作者:anyRTC开发者
  • 2022 年 8 月 04 日
  • 本文字数:21406 字

    阅读完需:约 70 分钟

上一期我们把前期准备工作做完了,这一期就带大家实现音视频通话!

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 语音通话1Mode: 0,// 当前场景 0:首页 1:呼叫页面 2:通信页面State: 0,
// 本地用户uiduserId: "",// 远端用户uidpeerUserId: "",// 频道房间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 二测封装,方便调用


// 引入 RTCconst ArRTC = require("ar-rtc-miniapp");// 引入 untilconst Until = require("./util");// 引入 storelet Store = require("./store");// 引入 SDK 配置const Config = require("./config");
// 初始化 RTCconst 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");// 引入 untilconst Until = require("./util");// 引入 storelet Store = require("./store");// 引入 SDK 配置const Config = require("../utils/config");// 引入 RTCconst 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}`}
// 随机生成uidconst 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;}
// 引入 storelet 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, },
/** * 生命周期函数--监听页面加载 */ onLoad: 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; } }, /** * 生命周期函数--监听页面加载 */ onLoad: 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



发布于: 刚刚阅读数: 3
用户头像

实时交互,万物互联! 2020.08.10 加入

实时交互,万物互联,全球实时互动云服务商领跑者!

评论

发布
暂无评论
技术分享| 小程序实现音视频通话_小程序_anyRTC开发者_InfoQ写作社区