技術分享| 小程式實現音視訊通話
上一期我們把前期準備工作做完了,這一期就帶大家實現音視訊通話!
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
程式碼地址
「其他文章」
- 天翼雲全場景業務無縫替換至國產原生作業系統CTyunOS!
- 以羊了個羊為例,淺談小程式抓包與響應報文修改
- 這幾種常見的 JVM 調優場景,你知道嗎?
- 如此狂妄,自稱高效能佇列的Disruptor有啥來頭?
- 為什麼要學習GoF設計模式?
- 827. 最大人工島 : 簡單「並查集 列舉」運用題
- 手把手教你如何使用 Timestream 實現物聯網時序資料儲存和分析
- 850. 矩形面積 II : 掃描線模板題
- Java 併發程式設計解析 | 基於JDK原始碼解析Java領域中的併發鎖,我們可以從中學習到什麼內容?
- 【手把手】光說不練假把式,這篇全鏈路壓測實踐探索
- 大廠鍾愛的全鏈路壓測有什麼意義?四種壓測方案詳細對比分析
- 寫個續集,填坑來了!關於“Thread.sleep(0)這一行‘看似無用’的程式碼”裡面留下的坑。
- 857. 僱傭 K 名工人的最低成本 : 列舉 優先佇列(堆)運用題
- Vue3 實現一個自定義toast(小彈窗)
- 669. 修剪二叉搜尋樹 : 常規樹的遍歷與二叉樹性質
- 讀完 RocketMQ 原始碼,我學會了如何優雅的建立執行緒
- 效能調優——小小的log大大的坑
- 1582. 二進位制矩陣中的特殊位置 : 簡單模擬題
- elementui原始碼學習之仿寫一個el-switch
- 646. 最長數對鏈 : 常規貪心 DP 運用題