ARTICLE · 988158
微信小程序 VoIP 插件(二期,实战代码)
微信小程序 VoIP 插件(二期,实战代码)
本期直接上干货代码(由AI辅助生成,实际场景使用请自行测试)
VoIP 对接代码整理
配置 manifest.json
{”mp-weixin”: {”plugins”: {”wmpf-voip”: {”version”: ”2.5.3”,”provider”: ”wx***************”}},”permission”: {”scope.camera”: { ”desc”: ”用于人脸识别验证和音视频通话” },”scope.record”: { ”desc”: ”用于音视频通话时采集麦克风声音” }},”optimization”: {”subPackages”: true}}}
不要加:"lazyCodeLoading": "requiredComponents"(白屏的关键)
App.vue
import wmpfVoipMixin from '@/common/mixin/wmpf-voip-mixin'export default {mixins: [wmpfVoipMixin],onLaunch(res) {// #ifdef MP-WEIXINconst pluginLaunch = this.isVoipPluginLaunch(res)this.ensureVoipRuntimeReady()if (pluginLaunch) {setTimeout(() => this.logVoipColdStart('app.onLaunch', res), 0)} else {this.logVoipColdStart('app.onLaunch', res)}// #endif},onShow(res) {// #ifdef MP-WEIXINthis.initVoip()setTimeout(() => this.logVoipColdStart('app.onShow', res), 0)// #endif}}
common/mixin/wmpf-voip-mixin.js
import * as env from '@/config/env.js'import NetworkMonitor from '@/common/js/networkMonitor.js'import { getConfig } from '@/api/auth'let wmpfVoip = nulllet voipRealtimeLogger = nullconst VOIP_REALTIME_SKIP = { calling: true }const VOIP_REALTIME_WARN = {joinFailListener: true,joinFailCaller: true,abortVoip: true,rejectVoip: true,timeout: true,busy: true,forceHangUpVoip: true}const VOIP_REALTIME_ERROR = {pluginMissing: true,initByCallerFail: true,setVoipEndPagePathFail: true}// #ifdef MP-WEIXINtry {const plugin = requirePlugin('wmpf-voip')wmpfVoip = plugin && plugin.default ? plugin.default : plugin} catch (e) {wmpfVoip = null}try {if (typeof wx !== 'undefined' && wx.getRealtimeLogManager) {voipRealtimeLogger = wx.getRealtimeLogManager()}} catch (e) {voipRealtimeLogger = null}// #endiffunction isWmpfDeviceEnv() {// #ifdef MP-WEIXINreturn typeof wmpf !== 'undefined' && wmpf.Channel && wmpf.Channel.invoke// #endif// #ifndef MP-WEIXINreturn false// #endif}function getVoipPluginOptions(getterName) {if (!wmpfVoip || typeof wmpfVoip[getterName] !== 'function') return {}try {return wmpfVoip[getterName]() || {}} catch (e) {return {}}}function pickDefinedVoipFields(src) {const out = {}if (!src || typeof src !== 'object') return outObject.keys(src).forEach((key) => {if (key === 'path' || key === 'query') returnconst value = src[key]if (value === undefined || value === null || value === '') returnout[key] = value})return out}function getVoipQueryObject(src) {if (src && src.query && typeof src.query === 'object') return src.queryreturn {}}function isVoipPluginPath(path) {return typeof path === 'string' && path.indexOf('call-page-plugin') !== -1}function writeVoipRealtimeLog(level, payload) {const role = isWmpfDeviceEnv() ? 'caller' : 'listener'const msg = Object.assign({ role }, payload || {})console.log('[voip]', level, msg)if (!voipRealtimeLogger) returntry {voipRealtimeLogger.setFilterMsg('voip')const text = JSON.stringify(msg)if (level === 'error' && voipRealtimeLogger.error) voipRealtimeLogger.error(text)else if (level === 'warn' && voipRealtimeLogger.warn) voipRealtimeLogger.warn(text)else if (voipRealtimeLogger.info) voipRealtimeLogger.info(text)} catch (e) {console.error('[voip] realtime log fail', e)}}export default {data() {return {callOptions: {},deviceKeepTime: -1, // -1 不限时;>0 才限时。默认不能是 0voipEventListener: null,voipRuntimeReady: false,voipSessionPhase: 'idle', // idle | ringing | activevoipRoomId: '',lastHandledCallSeq: '',voipInitByCallerInFlight: false,networkState: { connected: true, type: '', available: true },networkMonitor: null}},mounted() {if (wmpfVoip && isWmpfDeviceEnv()) {this.initNetWorkMonitor()}},methods: {isWmpfDevice() {return isWmpfDeviceEnv()},isVoipPluginLaunch(launchRes) {const enterOptions = getVoipPluginOptions('getPluginEnterOptions')const onloadOptions = getVoipPluginOptions('getPluginOnloadOptions')const path = (launchRes && launchRes.path) || enterOptions.path || onloadOptions.path || ''return isVoipPluginPath(path)},getVoipSourceQuery(getterName) {const options = getVoipPluginOptions(getterName)return Object.assign({},pickDefinedVoipFields(options),pickDefinedVoipFields(getVoipQueryObject(options)))},getVoipEnterQuery() {return this.getVoipSourceQuery('getPluginEnterOptions')},getVoipOnloadQuery() {return this.getVoipSourceQuery('getPluginOnloadOptions')},getVoipPluginQuery() {return Object.assign({}, this.getVoipEnterQuery(), this.getVoipOnloadQuery())},isVoipPreLaunchQuery(query) {const q = query || {}return q.isPreLaunch === true || q.isPreLaunch === 'true' || q.isPreLaunch === '1'},isWmpfCallerPluginQuery(query) {const q = query || {}if (this.isVoipPreLaunchQuery(q)) return falseif (!q.callSeq) return falseif (q.isCaller === '0' || q.isCaller === 0 || q.isCaller === false) return falseif (String(q.isCaller) === '1') return truereturn !!(q.sn && q.openId)},ensureVoipRuntimeReady() {// #ifdef MP-WEIXINif (this.voipRuntimeReady) return trueif (!wmpfVoip) {this.logVoipEvent('pluginMissing')return false}this.setupVoipEventListener()this.syncKeepTimeFromPlugin()try {wmpfVoip.setVoipEndPagePath({url: '/pages/home/home',key: 'Call',routeType: 'switchTab'})} catch (e) {this.logVoipEvent('setVoipEndPagePathFail', {data: { errMsg: e && e.message ? e.message : String(e) }})}this.voipRuntimeReady = truereturn true// #endif// #ifndef MP-WEIXINreturn false// #endif},initVoip() {// #ifdef MP-WEIXINreturn new Promise((resolve) => {const ready = this.ensureVoipRuntimeReady()if (!ready || !wmpfVoip) {resolve(false)return}// 被叫:立刻让路if (!this.isWmpfDevice()) {resolve(true)return}const enterQuery = this.getVoipEnterQuery()if (this.isVoipPreLaunchQuery(enterQuery)) {resolve(false)return}// 主叫参数齐全:直接拨if (this.isWmpfCallerPluginQuery(enterQuery)) {this.startCallerFromPluginQuery(enterQuery).then(resolve).catch(() => resolve(false))return}// query 未齐:等 callPageOnShowif (this.isVoipPluginLaunch()) {resolve(true)return}// 兜底getConfig().then((res) => {if (!res.isShowFace) {resolve(false)return}uni.getNetworkType({success: (netRes) => {if (netRes.networkType === 'none') {this.$util.msg('当前设备没有网络')resolve(false)return}const query = this.getVoipPluginQuery()this.syncKeepTimeFromPlugin()if (this.isVoipPreLaunchQuery(query)) {resolve(false)return}if (query.isCaller === '1') {this.startCallerFromPluginQuery(query).then(resolve).catch(() => resolve(false))} else {resolve(true)}}})}).catch(() => resolve(false))})// #endif// #ifndef MP-WEIXINreturn Promise.resolve(false)// #endif},startCallerFromPluginQuery(query) {const q = query || this.getVoipEnterQuery()if (!this.isWmpfDevice() || !this.isWmpfCallerPluginQuery(q)) {return Promise.resolve()}const callSeq = String(q.callSeq)if (this.lastHandledCallSeq && this.lastHandledCallSeq === callSeq) {return Promise.resolve()}if (this.voipInitByCallerInFlight) {return Promise.resolve()}this.voipInitByCallerInFlight = truethis.syncKeepTimeFromPlugin()if (q.sn) this.deviceSn = q.snif (q.roomType) this.roomType = q.roomTypetry {wmpfVoip.setUIConfig({ handsFree: false })} catch (e) {}return this.checkCallerAuthorization(Object.assign({}, q)).then((ok) => {this.lastHandledCallSeq = callSeqreturn ok}).catch((err) => {this.logVoipEvent('initByCallerFail', {data: { errMsg: err && err.errMsg ? err.errMsg : String(err) }})}).then(() => {this.voipInitByCallerInFlight = false})},checkCallerAuthorization(options) {const { sn, openId, roomType, callName, famName } = options || {}// #ifdef MP-WEIXINreturn new Promise((resolve, reject) => {if (!wmpfVoip) {resolve(false)return}let miniprogramState = 'trial'if (env.IS_TRIAL) miniprogramState = 'trial'else if (env.IS_RELEASE) miniprogramState = 'formal'wmpfVoip.getIotBindContactList({sn,model_id: env.DEVICE_ID,openid_list: [openId]}).then((res) => {const contactList = res.contact_list || []if (!(contactList.length > 0 && contactList[0].status == 1)) {this.sendVoipEventToDevice('callError', { msg: '该家长未对此设备授权' })reject(new Error('contact unauthorized'))return}wmpfVoip.initByCaller({caller: { id: sn, name: callName },listener: { id: openId, name: famName },roomType,businessType: 1,miniprogramState}).then((roomRes) => {const { groupId, roomId, isSuccess } = roomResif (!isSuccess) {this.sendVoipEventToDevice('callError', { msg: '拨打失败' })reject(new Error('initByCaller isSuccess false'))return}this.rememberVoipRoomId(roomId || groupId)this.setVoipPhase('ringing')resolve(true)}).catch((err) => {this.sendVoipEventToDevice('callError', { msg: '创建房间失败' })this.sendVoipEventToDevice('endVoip', { reason: 'initByCallerFail' })this.forceHangUpVoipSession('initByCallerFail')this.resetVoipSession()reject(err)})}).catch((err) => {this.sendVoipEventToDevice('callError', { msg: '该家长未对此设备授权' })reject(err)})})// #endif// #ifndef MP-WEIXINreturn Promise.resolve(false)// #endif},setupVoipEventListener() {// #ifdef MP-WEIXINif (!wmpfVoip) returnif (this.voipEventListener) return // 只绑一次this.voipEventListener = wmpfVoip.onVoipEvent((event) => {const { eventName, data, roomId, groupId } = eventthis.rememberVoipRoomId(roomId || groupId)if (!VOIP_REALTIME_SKIP[eventName]) {this.logVoipEvent(eventName, { data: data || {}, roomId, groupId })}this.sendVoipEventToDevice(eventName, data)switch (eventName) {case 'startVoip':this.setVoipPhase('active')breakcase 'inviteVoip':this.setVoipPhase('ringing')breakcase 'acceptVoip':case 'hangOnVoip':this.setVoipPhase('active')breakcase 'calling':this.handleCalling(data)breakcase 'callPageOnShow':this.handleCallPageOnShow(data)breakcase 'joinedRoomByCaller':if (this.voipSessionPhase === 'idle') this.setVoipPhase('ringing')breakcase 'abortVoip':case 'cancelVoip':case 'rejectVoip':case 'hangUpVoip':case 'endVoip':case 'busy':case 'timeout':case 'joinFailCaller':case 'joinFailListener':case 'finishVoip':this.resetVoipSession()break}})// #endif},handleCallPageOnShow(data) {if (!this.isWmpfDevice()) {if (this.voipSessionPhase === 'idle') this.setVoipPhase('ringing')setTimeout(() => this.logVoipColdStart('plugin.callPageOnShow', data), 0)return}const onloadQuery = this.getVoipOnloadQuery()this.syncKeepTimeFromPlugin()if (this.isWmpfCallerPluginQuery(onloadQuery)) {this.startCallerFromPluginQuery(onloadQuery)return}if (this.voipSessionPhase === 'idle') this.setVoipPhase('ringing')setTimeout(() => this.logVoipColdStart('plugin.callPageOnShow', data), 0)},handleCalling(data) {this.setVoipPhase('active')const keepTime = data.keepTime * 1if (!(this.deviceKeepTime > 0)) this.syncKeepTimeFromPlugin()if (!(this.deviceKeepTime > 0)) returnif ((this.deviceKeepTime - keepTime) === 30) {this.$util.msg('剩余通话时长不足30秒')}if (keepTime >= this.deviceKeepTime) {this.sendVoipEventToDevice('endVoip', data)this.forceHangUpVoipSession('timeLimit')this.resetVoipSession()}},syncKeepTimeFromPlugin() {// #ifdef MP-WEIXINtry {const query = this.getVoipPluginQuery()if (query.keepTime === undefined || query.keepTime === null || query.keepTime === '') returnconst n = query.keepTime * 1if (!Number.isNaN(n)) this.deviceKeepTime = n} catch (e) {}// #endif},setVoipPhase(phase) {this.voipSessionPhase = phase},resetVoipSession() {this.voipSessionPhase = 'idle'this.voipRoomId = ''},rememberVoipRoomId(roomId) {if (roomId) this.voipRoomId = roomId},forceHangUpVoipSession(reason) {// #ifdef MP-WEIXINif (!wmpfVoip) returntry {if (this.voipRoomId) wmpfVoip.forceHangUpVoip(this.voipRoomId)else wmpfVoip.forceHangUpVoip()this.logVoipEvent('forceHangUpVoip', {data: { origin: 'forceFromApp', errMsg: reason }})} catch (e) {}// #endif},sendVoipEventToDevice(eventType, eventData) {// #ifdef MP-WEIXINtry {if (typeof wmpf !== 'undefined' && wmpf.Channel && wmpf.Channel.invoke) {wmpf.Channel.invoke({command: eventType,data: JSON.stringify(eventData || {}),success() {},fail() {}})}} catch (e) {}// #endif},logVoipEvent(eventName, extra) {const data = (extra && extra.data) || extra || {}let level = 'info'if (VOIP_REALTIME_ERROR[eventName]) level = 'error'else if (VOIP_REALTIME_WARN[eventName]) level = 'warn'writeVoipRealtimeLog(level, {eventName,roomId: (extra && extra.roomId) || this.voipRoomId || '',groupId: (extra && extra.groupId) || '',errMsg: data.errMsg || (data.error && data.error.errMsg) || '',status: data.status || '',origin: data.origin || ''})},logVoipColdStart(stage, launchRes) {const enterOptions = getVoipPluginOptions('getPluginEnterOptions')const onloadOptions = getVoipPluginOptions('getPluginOnloadOptions')writeVoipRealtimeLog('info', {eventName: 'coldStart',stage,hasWmpf: isWmpfDeviceEnv(),path: (launchRes && launchRes.path) || '',scene: (launchRes && launchRes.scene) || '',pluginPath: enterOptions.path || '',pluginQuery: enterOptions.query || {},onloadPath: onloadOptions.path || '',onloadQuery: onloadOptions.query || {}})},initNetWorkMonitor() {this.networkMonitor = new NetworkMonitor()this.networkMonitor.setCallback((state) => {this.networkState = stateif (!state.available) {this.$util.msg('当前设备没有网络,请重新启动')setTimeout(() => {if (this.voipSessionPhase !== 'idle') {this.forceHangUpVoipSession('networkUnavailable')this.resetVoipSession()}if (this.networkMonitor) this.networkMonitor.destroy()}, 300)}})},destroyVoipEventListener() {// #ifdef MP-WEIXINif (this.voipEventListener) {this.voipEventListener()this.voipEventListener = nullthis.voipRuntimeReady = false}// #endif}},beforeDestroy() {this.destroyVoipEventListener()if (this.networkMonitor) this.networkMonitor.destroy()},onHide() {// #ifdef MP-WEIXINif (!this.isWmpfDevice() || !wmpfVoip) returnif (this.voipSessionPhase === 'idle') returnthis.forceHangUpVoipSession('onHide')this.resetVoipSession()// #endif}}
调用链
设备主叫:onLaunch → ensureVoipRuntimeReadyonShow → initVoip → startCallerFromPluginQuery → getIotBindContactList → initByCallerquery 未齐时 → callPageOnShow → getVoipOnloadQuery → startCallerFromPluginQuery手机被叫:onLaunch → ensureVoipRuntimeReady(只绑事件)onShow → initVoip → 非设备端立刻 return插件自己进房 → hangOnVoip
query 字段
callSeq / isCaller / isPreLaunch / sn / openId / roomType / keepTime / callName / famName注意
deviceKeepTime 默认 -1,不能是 0onVoipEvent 只绑一次被叫 initVoip 立刻 return结束页 setVoipEndPagePath → 你们的 tab 首页实时日志 FilterMsg = voip