技术分享 | 实现音频和视频通话的小程序
最编程
2024-03-27 16:41:20
...
上一期我们把前期准备工作做完了,这一期就带大家实现音视频通话!
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 停止登录',
推荐阅读
-
0 销售额、全远程、跨时区?听听 StreamNative 的杰西-郭(Jesse Kuo)怎么说!
-
设计模式 - 适配器、装饰和外观模式 - 3 种外观模式
-
2024 年 5 月 1 日 深部采煤中冲击地压危险预测的数学建模 C 问题 原文分享
-
GMT、UTC、DST 和 CST 时区的含义
-
全面了解格林尼治标准时间、世界协调时、时区和夏令时
-
PHP 时间 8 小时差的 "8 "从何而来?如何获取正确的时区?如何优雅地处理时间?
-
格林尼治标准时间?UTC?CST?进来聊聊时区(Java 中的时区)
-
最完整的全球城市 ZoneId 和 UTC 时间偏移对照表
-
各时区的时差表,以及如何用 python 获取时区(支持夏令时)。
-
为什么插入 Clickhouse 数据库的日期格式不正确?