上一期我們把前期準備工作做完了,這一期就帶大家實作音視訊通話!
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',
});
},
})