当前位置:网站首页>技术分享| 小程序实现音视频通话
技术分享| 小程序实现音视频通话
2022-08-04 14:27:00 【anyRTC】
上一期我们把前期准备工作做完了,这一期就带大家实现音视频通话!
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,
},
/** * 生命周期函数--监听页面加载 */
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
代码地址
边栏推荐
- leetcode 48. Rotate Image (Medium)
- Analysis and application of portrait segmentation technology
- Set partition minimum difference problem (01 knapsack)
- Convolutional Neural Network Basics
- 如何才能有效、高效阅读?猿辅导建议“因材因时施教”
- idea去掉spark的日志
- 电子行业MES管理系统有哪些特殊功能
- 考研上岸又转行软件测试,从5k到13k完美逆袭,杭州校区小哥哥拒绝平庸终圆梦!
- Map常见的遍历方式-keySet 和 entrySet
- 数据库恢复
猜你喜欢
MPLS experiment
Kyushu Cloud attended the Navigator Online Forum to discuss the current status, challenges and future of 5G MEC edge computing
【模型部署与业务落地】基于量化芯片的损失分析
开发者独立搭建一个跨模态搜索应用有多难?
MySQL【窗口函数】【共用表表达式】
阿里老鸟终于把测试用例怎么写说的明明白白了,小鸟必看
基于 Next.js实现在线Excel
MySQL【触发器】
Database recovery
【 HMS core 】 【 Media 】 online video editing service 】 【 material can't show, or network anomalies have been Loading state
随机推荐
CF1527D MEX Tree(mex&树&容斥)
Theory 1: Deep Learning - Detailed Explanation of the LetNet Model
ACL 2022 | 社会科学理论驱动的言论建模
【问题解决】QT更新组件出现 “要继续此操作,至少需要一个有效且已启用的储存库”
[Opportunity Enlightenment-60]: "Soldiers, Stupid Ways"-1- Opening: "Death" and "Life" are the way of heaven
ICML 2022 | 图神经网络的局部增强
如何确定异步 I/O 瓶颈
【剑指offer59】队列的最大值
实际工作中的高级技术(训练加速、推理加速、深度学习自适应、对抗神经网络)
AVR学习笔记之熔丝位
[The Art of Hardware Architecture] Study Notes (1) The World of Metastability
物联网应用发展趋势
ASA归因:如何评估关键词的投放价值
如何查找endnote文献中pdf文件的位置
【 HMS core 】 【 Media 】 online video editing service 】 【 material can't show, or network anomalies have been Loading state
快解析结合千方百剂
LM2596有没有可以替代的?LM2576可以
Workaround without Project Facets
Problem solving-->Online OJ (18)
How to find the location of a pdf file in endnote literature