欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

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

最编程 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 停止登录',