const log = require("./log"); const util = require('./util'); const crypt = require('./WXBizMsgCrypt'); const wmpfVoip = requirePlugin('wmpf-voip').default // 判断当前是否为设备端拉起小程序 const isWmpf = (typeof wmpf !== 'undefined'); // 指定接听方使用的小程序版本。formal/正式版(默认);trial/体验版;developer/开发版 const miniprogramState = (() => { const accountInfo = wx.getAccountInfoSync(); if(accountInfo && accountInfo.miniProgram){ const platform = { develop: 'developer', trial: 'trial', release: 'formal' } return platform[accountInfo.miniProgram.envVersion] } })() console.log('miniprogramState='+miniprogramState) // 「设备接入」从微信公众平台获取的 model_id const modelId = 'DSAF58AS5F3SA2FD2SA33DS55'; // 设备名称,用于授权时显示给用户 const deviceName = '好宝宝'; // 通话监听 const onVoipEvent = () => { wmpfVoip.onVoipEvent((event) => { const eventName = event.eventName; // 定义挂断事件 const filterEvent = ['hangUpVoip', 'cancelVoip', 'timeout', 'rejectVoip']; // 挂断动作向设备端发送消息通知 if(eventName == 'endVoip' || filterEvent.indexOf(eventName) > -1){ // 每次通话只能推送一次挂断信息 if(event.groupId && wx.getStorageSync('currcall') != event.groupId){ sendMsgDevice('EndVoip'); wx.setStorageSync('currcall', event.groupId) log.info('通知设备关闭小程序') } } // 通话记录参数 const callData = { groupId: event.groupId, params: { eventName: eventName, data: event.data }, } // 挂断上报通话记录 filterEvent.indexOf(eventName) > -1 && updateCallRecord(callData) // 为避免上面几种情况执行异常,在结束通话时再次上报,延迟执行是为了避免并发 if(eventName == 'endVoip'){ setTimeout(() => { updateCallRecord(callData); }, 1500) } // 非通话中,打印调试 (eventName != 'calling') && log.info(`onVoipEvent`, event); // 设备打微信,微信端插件通话页面 onShow if(eventName == 'callPageOnShow' && !isWmpf){ setUIConfig(1) } }) } // 设置通话结束跳转地址 const setVoipEndPagePath = (recordId) => { if(!isWmpf){ wmpfVoip.setVoipEndPagePath({ url: '/pages/call/index', key: 'Call', }) } } /** * 小程序传递消息给设备 * @param {*} command */ const sendMsgDevice = (command) => { if(isWmpf){ wmpf.Channel.invoke({ command: command, success: function(res) { wx.setStorageSync(command+'_invoke', 1) setTimeout(() => { wx.removeStorageSync(command+'_invoke'); }, 1000) log.info('wmpf.Channel.invoke success:', res.data) }, fail: function(res){ log.error('wmpf.Channel.invoke fail:', res); } }) } } /** * 微信打设备 * @param {拨打方用户 openId} openid * @param {拨打对方用户信息 name:拨打方名字,仅显示用;roomType:voice音频房间,video音视频房间;voipToken:从设备获取的 pushToken} contact */ const wechatCallDevice = (contact) => { wx.showLoading({title: '呼叫中',mask: true}), setTimeout(() => {wx.hideLoading()}, 3000) const initByCaller = async (config, data) => { log.info('====>通话请求参数', data, config) setUIConfig(2); const { groupId, isSuccess, errCode, errMsg } = await wmpfVoip.initByCaller(data) wx.hideLoading(); if (isSuccess) { requestCallVoip(groupId, data.roomType, 0, config); const callPagePlugin = 'plugin-private://wxf830863afde621eb/pages/call-page-plugin/call-page-plugin' wx.redirectTo({ url: `${callPagePlugin}?isCaller=1&roomType=${data.roomType}&groupId=${groupId}`, }) } else { log.error('拨打请求失败:errCode='+errCode+';errMsg='+errMsg, config, data) if(errCode == 14){ return wx.showToast({title: '手机微信拨打硬件设备模式,voipToken 错误',icon: 'error'}) } wx.showToast({title: '拨打失败',icon: 'error'}) } } const init = () => { checkDeviceAuth(contact.deviceId, ()=>{ getCallConfigParams(contact.id, contact.relId, (config) => { initByCaller(config, { caller: { id: config.openid, name: config.name }, listener: { id: config.deviceId }, roomType: contact.roomtype, voipToken: config.pushToken, businessType: 2, miniprogramState: miniprogramState, }) }) }) } // 通话结束延长1秒才能再次拉起通话,0.2秒检查一次 const checkCallEnd = () => { if(wx.getStorageSync('EndVoip_invoke')){ setTimeout(() => { checkCallEnd(); }, 200) }else{ init() } } checkCallEnd(); } /** * 设备拨打微信 */ const deviceCallWechat = () => { const { query } = wmpfVoip.getPluginEnterOptions() log.info('====>设备拨打微信传参:', query) setUIConfig(query.isCaller === '1' ? 3 : 4); if(query.isPreLaunch == 'false' && query.isCaller === '1' && query.contactId){ var contactId = query.contactId; var relId = query.deviceId; if(query.isRecord == '1'){ contactId = -2; relId = query.contactId; } wx.setStorageSync('token', query.token); getCallConfigParams(contactId, relId, (config) => { wmpfVoip.initByCaller({ caller: { id: query.deviceId }, listener: { id: config.openid,name: config.name }, roomType: query.roomType, voipToken: query.voipToken, businessType: 1, miniprogramState: miniprogramState, }).then((res)=>{ log.info('===========>设备打微信initByCaller success', res); res.isSuccess && requestCallVoip(res.groupId, query.roomType, 1, config); if(!res.isSuccess){ if(res.errCode == 9){ wx.showToast({title: '未授权设备无法使用通话功能',icon: 'error'}) } if(res.errCode == 13){ // 传递参数给设备处理 sendMsgDevice('VoipTokenErr13'); } sendMsgDevice('errorEnd'); } }).catch((e) => { sendMsgDevice('errorEnd'); log.error('==========>设备打微信initByCaller fail', e); }) }, () => { sendMsgDevice('errorEnd'); }) } } /** * 获取通话配置参数 * @param {通讯录id} contactId */ const getCallConfigParams = (contactId, relId, callback, errback) => { util.rqt({ url: '/eeop/wechatcall/getCallConfig', data: { id: contactId,appid: crypt.appid,relId: relId }, callBack(res){ const encryptOpenid = res.data.encryptOpenid; if(!encryptOpenid){ errback && errback(); log.error('获取配置参数openid失败', res.data); return; } res.data.openid = crypt.decrypt(encryptOpenid.TimeStamp, encryptOpenid.Nonce, encryptOpenid.Encrypt, encryptOpenid.MsgSignature); callback && callback(res.data); }, errCallBack(res){ errback && errback(); log.error('请求后台配置出现异常', res); } }) } /** * 拨打电话 */ const requestCallVoip = (groupId, roomType, callWay, config) => { updateCallRecord({ groupId: groupId, userId: config.userId, deviceId: config.deviceId, callType: (roomType == 'video' ? 1 : 0), callWay: callWay, params: { eventName: 'startVoip' } }) } const updateCallRecord = (data) => { util.rqt({ url: '/eeop/wechatcall/updateCallRecord', method: 'POST', data: data, callBack(res){ log.info('上报通话返回结果', res, data); }, errCallBack(res){ log.error('上报通话数据返回异常', res, data) } }) } /** * 添加联系人 * @param {*} options * @param {*} callback */ const addContact = (options, callback) => { if(!options.deviceId || !options.code){ return wx.showToast({title: '扫描失败',icon: 'none'}) } checkDeviceAuth(options.deviceId, () => { util.rqt({ url: '/eeop/wechatcall/bind', data: options, callBack(res){ open('添加成功', true); }, errCallBack(res){ if(res.code == 3010){ return open('二维码已过期', false); } open(res.msg, false); } }) const open = (content, status) => { wx.showToast({ title: content, icon: 'none', success(res){ setTimeout(() => { callback && callback(status); }, 1500) } }) } }) } /** * 更新token */ const updatePushToken = () => { // 根据设备deviceId更新token信息到后台服务器 const updatePushToken = (deviceId, pushToken) => { util.rqt({ url: '/eeop/wechatcall/pushToken', data: { deviceId: deviceId,pushToken: pushToken }, callBack(result){ log.info('=====>设备端拉起小程序时向后台请求更新token返回结果callBack:deviceId='+deviceId+';pushToken='+pushToken, result); }, errCallBack(result){ log.info('=====>设备端拉起小程序时向后台请求更新token返回结果errCallBack:deviceId='+deviceId+';pushToken='+pushToken, result); } }); } // 获取pushToken const getWmpfPushToken = (deviceId) => { log.info('=====>获取pushToken', deviceId); wmpf.getWmpfPushToken({ success(res){ log.info('=====>wmpf.getWmpfPushToken success', res); const pushToken = res.token; if(pushToken && pushToken != 'undefined'){ updatePushToken(deviceId, pushToken); } }, fail(res){ log.error('=====>wmpf.getWmpfPushToken fail', res); } }) } // 获取设备deviceId const { query } = wmpfVoip.getPluginEnterOptions() log.info('设备拉起小程序获取参数:', query); query.token && wx.setStorageSync('token', query.token); query.deviceId && getWmpfPushToken(query.deviceId); } /** * 检测是否授权 * @param {用户信息} contact */ const checkDeviceAuth = (deviceId, callback) => { const requestDeviceVoIP = (snTicket) => { wx.hideLoading(); wx.requestDeviceVoIP({ sn: deviceId, // 向用户发起通话的设备 sn(需要与设备注册时一致) snTicket: snTicket, // 获取的 snTicket modelId: modelId, // 「设备接入」从微信公众平台获取的 model_id deviceName: deviceName, // 设备名称,用于授权时显示给用户 success(res) { callback && callback(); log.info(`requestDeviceVoIP success:`, res) }, fail(err) { if(err.errCode == 10021){ wx.showModal({ title: '是否要打开设置页面', content: '需要获取您的音视频通话授权信息,请到小程序的设置中打开授权', success(res) { res.confirm && wx.openSetting() } }) } log.error(`requestDeviceVoIP fail:`, err) }, }) } const getSnTicket = () => { util.rqt({ url: '/eeop/wechatcall/setting/getSnTicket', data: { deviceId: deviceId }, callBack(res){ requestDeviceVoIP(res.data.snTicket) }, errCallBack(res){ log.error(`getSnTicket fail:`, err) } }) } util.checkWechatVersion('2.30.3') && wx.getDeviceVoIPList({ success(res) { log.info('当前用户授权的设备:', res.list) const isAuth = res.list.some(element => { return element.sn == deviceId && element.status == 1; }) if(isAuth){ callback && callback() }else{ getSnTicket(); } } }) } /** * 自定义UI相关配置 * @param {拨打场景: 1设备打微信,微信端接听,微信端设置;2微信打设备,微信端设置;3设备打微信,设备端设置;4微信打设备,设备端接听,设备端设置} scene */ const setUIConfig = (scene) => { return; // 显示设备端 var UI_1 = { cameraRotation: 0, // caller的视频画面旋转角度,有效值为 0, 90, 180, 270。默认 0 aspectRatio: 3/4, // 纵横比,caller的视频画面会进行适配比例,有效值 数字。默认 4/3 horMirror: false, // 横向镜像,boolean 值,默认 false vertMirror: false, // 竖直镜像,同上 enableToggleCamera: false, // 是否支持切换摄像头,false 则不显示「摄像头开关」按钮。默认false 【该配置项在wmpf无效,wmpf默认开摄像头,且不显示开关按钮】 } // 显示微信端 var UI_2 = { cameraRotation: 0, aspectRatio: 4/3, horMirror: false, vertMirror: false, enableToggleCamera: false, } if(scene == 1){ wmpfVoip.setUIConfig({ btnText: null, callerUI: UI_1, listenerUI: UI_2 }) } if(scene == 2){ wmpfVoip.setUIConfig({ btnText: null, callerUI: UI_1, listenerUI: UI_2 }) } if(scene == 3){ wmpfVoip.setUIConfig({ btnText: null, callerUI: UI_1, listenerUI: UI_2 }) } if(scene == 4){ wmpfVoip.setUIConfig({ btnText: null, callerUI: UI_2, listenerUI: UI_1 }) } } module.exports = { isWmpf, updatePushToken, wechatCallDevice, deviceCallWechat, checkDeviceAuth, onVoipEvent, setVoipEndPagePath, addContact }