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

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});},// 视频的旋转信息以及远端用户的 IDvideoRotation: ({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) {// 引入 RTMconst 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})// 登录 RTMawait 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 呼叫中进入RTCconsole.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 语音通话 1Conference: 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() + 1const 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 语音通话1Mode: 0,// 远端用户uidpeerUserId: "",// 频道房间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,// 远端uidpeerid: "",// 本地录制地址(小程序特有推流)livePusherUrl: "",// 远端播放(小程序特有拉流)livePlayerUrl: "",// 前置或后置,值为front, backdevicePosition: '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

技术分享| 小程序实现音视频通话相关推荐

  1. (精华)2020年8月7日 微信小程序 实时音视频通话

    微信小程序集成实时音视频通话功能 背景 在项目的开发当中,很多时候,我们会有音频.视频通话的需求,但是一般都不会自己来写,所以我们就需要借助第三方来实现.尤其是这次的项目开发当中,需要在微信小程序当中 ...

  2. 微信原生组件|基于小程序实现音视频通话

    1 微信小程序原生推拉流组件功能简介 本文将介绍如何使用微信小程序原生推拉流组件 <live-pusher> 和 <live-player> 进行推拉流,快速实现一个简单的实时 ...

  3. 微信小程序 RTMP 音视频 通话 ffmpeg_WebRTC与微信小程序音视频互通方案设计与实现...

    背景 在之前的WebRTC实时音视频通话之语音通话设计与实践中介绍了58 TEG部门基于 WebRTC 的实时音视频通话解决方案. 考虑到腾讯微信的小程序平台提供了音视频通话与直播的支持,如果能打通基 ...

  4. 刘连响:小程序实时音视频在互动场景下的应用

    本文来自腾讯云技术沙龙,本次沙龙主题为在线教育个性化教学技术实践 作者简介:刘连响,一起玩耍科技创始人.2013年起开始研究WebRTC, 对音视频处理. 直播.实时音视频相关技术非常感兴趣,具有多个 ...

  5. 直播带货这么火,如何在小程序中实现视频通话及直播互动功能?

    最近,新东方在线旗下直播间东方甄选以双语带货.寓教于乐等特色火速出圈.被东方甄选直播"圈粉"的人直呼,"躲过了薇娅.李佳琦,却没躲过董宇辉".近年来,视频.直播 ...

  6. 微云二手车运营版 公众号+小程序v1.1.20+微信小程序+抖音端源码安装教程

    系统包括PC+公众号+小程序v1.1.20+微信小程序+抖音小程序前端,使用前先在后台初始化下数据.播播资源网技术小编看了下说明是带PC端功能,但后台没找到设置的地方,不过公众号+小程序+抖音也够用了 ...

  7. 【微信技术-微信小程序】------ 小程序调用第三方的url地址页面例如调用:www.pipixia.com/index/index.html的页面

    一,通过微信小程序调用第三方的url地址页面. 1.微信官方文档说明可以支持调用第三方的url地址页面如下:  2.注意事项:(在微信开发工具中测试不用先配置,真正使用需要配置) 1.需要在" ...

  8. 十三、制作 iVX音乐分享小程序

    功能介绍 通过前几节的学习,我们对完成一个应用已经有了一些自己的心得.在此再次再制作一个小的音乐小程序应用.该应用一共分为首页.榜单页.音乐分享页和音乐搜索页. 首页: 榜单内容页: 音乐分享页: 音 ...

  9. android 分享小程序,Android分享微信小程序给微信好友,封面bitmap的处理

    image.png 第一步:url转换为bitmap val imgPath = "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/ ...

  10. android 分享小程序到微信,Android 分享微信小程序之图片优化

    小菜上周接入了微信分享小程序的入口,基本功能实现都没问题,有需要的朋友可以了解一下 Android 分享微信小程序失败二三事,虽然功能都正常,但整体测试发现图片展示效果不佳.于是小菜整理了一个简单的小 ...

最新文章

  1. linux-进程的替换exec函数族
  2. ruby gsub gsub! chomp chomp! 以及所有类似函数用法及区别
  3. oracle数据库部署
  4. Java学习个人备忘录之线程间的通信
  5. BUUCTF(pwn)mrctf2020_easy_equation
  6. 三维点云网络——PointNet论文解读
  7. Servlet过滤器示例及分析----日志过滤器 以及对flush()理解
  8. javascript捕获事件event
  9. Git Pull Failed:Could not read from remote repository
  10. 如何开发一个基于Docker的Python 应用
  11. Java中的MD5加密
  12. Markdown插入代码
  13. 为什么边缘概率密度是联合概率密度的积分_5.27005柏林联合VS美因茨
  14. 扩展ExoPlayer实现多音轨同时播放
  15. 视觉十四讲第六章G2O实践出错后的解决方法
  16. 冰汽朋克侦查机器人_寒霜朋克全系统解析攻略 冰汽时代全法典全科技详解
  17. 计算机网络速度测试指令,测试网速命令_在电脑的运行中输入什么指令 测网速...
  18. mysql插入失败39_Mysql错误:Duplicateentryamp;#39;127amp;#39;forkeyamp;#39;PRIMARYamp;#39;的解决方法-一团网...
  19. 无线安全测试方法_解决无线安全性的5种有效方法
  20. 蓝桥杯摔手机测试次数

热门文章

  1. 【记录一次windows技术学习】使用笔记本DOS命令搭建WLAN热点
  2. 中国社科中外合作办学双证博士创新与领导力管理学博士
  3. xposed+justtrustme
  4. 服务器审计资质证书,利用ACS服务器实现用户的认证、授权和审计
  5. 用安装包修复服务器,修复服务器
  6. Tony Chen的专栏
  7. Mybatis Plus 3.1.1 lambda 表达式查询时异常 cannot find the corresponding database column name!
  8. Lucene倒排索引简述 之倒排表
  9. html中图片放大镜效果图,HTML5使用不同精度的图片来实现图像放大镜效果
  10. 《自控力》——[美]Kelly McGonigal