更新记录
1.0.15(2024-11-18)
- Android socket增加data类型不对强制转换问题
1.0.14(2024-11-18)
- ios增加UTSVoipMgr.setVOIPEnable(enable)接口
1.0.13(2024-11-13)
- 增加cancelHandleBusiness取消接口
平台兼容性
Vue2 | Vue3 |
---|---|
√ | √ |
App | 快应用 | 微信小程序 | 支付宝小程序 | 百度小程序 | 字节小程序 | QQ小程序 |
---|---|---|---|---|---|---|
HBuilderX 3.6.8,Android:5.0,iOS:10,HarmonyNext:不确定 | × | × | × | × | × | × |
钉钉小程序 | 快手小程序 | 飞书小程序 | 京东小程序 |
---|---|---|---|
× | × | × | × |
H5-Safari | Android Browser | 微信浏览器(Android) | QQ浏览器(Android) | Chrome | IE | Edge | Firefox | PC-Safari |
---|---|---|---|---|---|---|---|---|
× | × | × | × | × | × | × | × | × |
VOIP,唤醒未启动app、后台app
后台保活插件(支持熄屏)https://ext.dcloud.net.cn/plugin?id=20810
集成步骤
- 拷贝demo里的nativeResources、Info.plist、AndroidManifest.xml文件到项目根目录
- 勾选manifest.json里的app模块配置Push(消息推送),如果勾选了Push模块,删除nativeResources/ios/UniApp.entitlements
- nativeResources/ios/UniApp.entitlements文件里的aps-environment对应的值是development、production,一般开发阶段使用development,生产发布使用production,根据情况设置
- ios打包的.mobileprovision需要包含消息推送功能
- 集成插件,集成插件步骤请参考 https://www.cnblogs.com/wenrisheng/p/18323027
- android需要在app设置里开启“自启动”、“悬浮窗”权限,在系统设置里面搜索“无障碍”,进入“已下载的服务”里开启无障碍服务,在系统设置里面搜索“使用情况访问权限”,开启“允许查看使用情况”
iOS VOIP服务器配置
- 登录苹果开发者系统生成VOIP证书(百度有很多资料),这个证书是voip的证书(Certificates->Services->VoIP Services Certificate),不是消息推送的证书
- 将cer证书和p12文件生成各自后端语言支持的证书,如:
- 其他语言可以参考苹果官方https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns
nodeJS语言服务器
服务器代码在demo/static/project-voip-server下 nodeJS语言需要cer和p12转成pem
cer转pem:
openssl x509 -inform DER -in voip_services.cer -outform PEM -out cer.pem
p12转pem:
openssl pkcs12 -in voiptext.p12 -nodes -password pass:1234 -legacy -out key.pem
或者去掉-legacy,有些版本的openssl不需要-legacy参数:
openssl pkcs12 -in voiptext.p12 -nodes -password pass:1234 -legacy -out key.pem
将cer.pem、key.pem文件分别替换project-voip-server/customer_cer/mine下的文件 修改project-voip-server/src/app.controller.ts代码下的passphrase证书密码:
// 证书密码
const passphrase = '123456';
进入project-voip-server文件夹执行启动安装依赖和启动服务命令:
// 安装nestJS
npm i -g @nestjs/cli
// 安装依赖
npm install
// 启动服务
npm run start:dev
浏览器或postman调用接口:
token的值替换成app项目里获取到的token
http://127.0.0.1:3000/apn?token=xxxxxx
java语言服务器
依赖下面第三方,具体实现接口百度有很多,有需要可以咨询作者 java语言需要cer和p12转成一个p12
<dependency>
<groupId>com.turo</groupId>
<artifactId>pushy</artifactId>
<version>0.13.10</version>
</dependency>
接口
ios voip原理:
- app端生成token,上传给服务器,服务器调用苹果的voip接口,然后app无论是否启动都会收到voip来电,点击接听/拒绝按钮都可以唤醒app
Android voip原理: android有2个方案唤醒app
-
方案1: 运行webSocket服务
- Android开启无障碍服务后会自动连接提前设置好的websocket,通过webSocket接收数据来唤醒app
-
方案2:监听手机来电
- Android开启无障碍服务后,当收到voip电话或普通来电后,会调用app提前设置好的服务器接口,根据服务器接口返回数据决定是否唤醒app,无论app有没有启动都可以唤醒
- app被唤醒后一般需要通知拨打方挂断电话
import {
UTSVoipMgr,
UTSLocalNotification
} from "@/uni_modules/wrs-uts-voip"
- 请求通知权限,仅支持ios
// 请求通知权限
UTSLocalNotification.requestAuthorization({
types: ["badge", "sound", "alert"]
}, (resp) => {
let flag = resp.flag
if (!flag) { // 请求权限失败
console.log("requestAuthorization:" + JSON.stringify(resp))
}
})
- 设置VOIP回调
// ios流程:
// 1. app把token上传给后端
// 2. 后端调用苹果的apn接口发送数据给苹果服务器
// 3. 苹果服务器会把消息转发给手机
// 4. 手机app收到后,先回调didReceiveIncomingPush接口
// 5. 如果消息体里的数据符合插件里定义的voip数据(消息体参考文档),则会自动拉起接听电话界面
// 6. 用户接听或拒绝电话
UTSVoipMgr.onCallback((resp) => {
this.showMsg(JSON.stringify(resp))
let opt = resp.opt
switch (opt) {
// 仅支持iOS,获取到本机token,需要上传给后端,后端发送voip时需要token参数,token可以理解为手机的ID
case "didUpdatePushCredentials": {
let token = resp.token
console.log("token:" + token)
}
break;
// 仅支持iOS,收到后端发送的数据
case "didReceiveIncomingPush": {
console.log("didReceiveIncomingPush:" + JSON.stringify(resp))
}
break;
// 仅支持iOS,拉起接听电话界面结果
case "startCall": {
getApp().globalData.uuid = resp.uuid
// 用户接听电话后,如果有定时器关闭电话则调用接口deleteWaitingResponseUuid取消定时器
UTSVoipMgr.deleteWaitingResponseUuid(resp.uuid)
}
break;
// 仅支持iOS,用户接听了电话
case "performAnswerCall": {
console.log("performAnswerCall:" + JSON.stringify(resp))
}
break;
// 仅支持iOS,电话挂断
case "performEndCall": {
console.log("performEndCall:" + JSON.stringify(resp))
}
break;
// 仅支持Android,websocket连接成功
case "opt_onConnectSuc": {
}
break;
// 仅支持Android,websocket连接失败
case "opt_onConnectFail": {
}
break;
// 仅支持Android,websocket收到数据
case "onMessage": {
}
break;
// 仅支持Android,websocket关闭
case "onClosed": {
}
break;
// 仅支持Android,websocket连接成功
case "onCallStateChanged": {
let state = resp.state
switch (state) {
// 手机空闲 CALL_STATE_IDLE
case 0:
break;
// 手机响铃/来电 CALL_STATE_RINGING
case 1:
break;
// 电话挂起/去电 CALL_STATE_OFFHOOK
case 2:
break;
}
}
break;
default:
break;
}
})
后端接口发起voip参数如下:
const payload = {
business: {
type: 0, // 0: 自动拉起电话接听界面
params: { // 参数
hasVideo: true,
localizedCallerName: "张三来电",
timeout: 5,// 可选参数,单位秒,传了timeout参数,无论用户有没有接听电话,定时器到了都会挂断电话,如果想取消定时器请调用deleteWaitingResponseUuid接口取消定时器
remoteHandle: {
type: 1, // 1: generic 2:phoneNumber 3: emailAddress
value: "18820406059"
}
}
}
};
- 设置通知回调,仅支持iOS
- 初始化本地通知,仅支持iOS
UTSLocalNotification.notificationInit()
- 发送本地通知,仅支持iOS
// 发送本地通知
UTSLocalNotification.add({
title: "张三来电",
subtitle: "VOIP",
body: "您收到了一条新消息",
sound: "default",
trigger: {
timeInterval: 0.2,
repeats: false
},
identifier: "123456"
}, (notResp)=>{
})
- 来电,app主动拉起来电界面,仅支持iOS
let params = { // 参数
hasVideo: true,
localizedCallerName: "张三来电",
timeout: 5, // 可选参数,单位秒,传了timeout参数,无论用户有没有接听电话,定时器到了都会挂断电话,如果想取消定时器请调用deleteWaitingResponseUuid接口取消定时器
remoteHandle: {
type: 1, // 1: generic 2:phoneNumber 3: emailAddress
value: "18820406059"
}
}
UTSVoipMgr.startCall(params, (resp) => {
let flag = resp.flag
if(flag) {
getApp().globalData.uuid = resp.uuid
}
})
- 挂断电话,包括来电和去电
let params = {}
if (this.isAndroid) {
} else { // ios
params.uuid = getApp().globalData.uuid
}
UTSVoipMgr.endCall(params, (resp) => {
console.log(JSON.stringify(resp))
})
- 接听来电,仅支持iOS
let params = {}
if (this.isAndroid) {
} else { // ios
params.uuid = getApp().globalData.uuid
}
UTSVoipMgr.answerCall(params, (resp) => {
console.log(JSON.stringify(resp))
})
- 去电,仅支持iOS
let params = { // 参数
type: "generic",// generic、phoneNumber、emailAddress
value: "张三号码",
}
UTSVoipMgr.startOffCall(params, (resp) => {
let flag = resp.flag
if(flag) {
getApp().globalData.uuid = resp.uuid
}
})
- 是否静音,仅支持iOS
let params = {}
if (this.isAndroid) {
} else { // ios
params.uuid = getApp().globalData.uuid
}
params.muted = true // true、false
UTSVoipMgr.mutedCall(params, (resp) => {
let flag = resp.flag
if(flag) {
getApp().globalData.uuid = resp.uuid
}
})
- 是否保持通话,仅支持iOS
let params = {}
if (this.isAndroid) {
} else { // ios
params.uuid = getApp().globalData.uuid
}
params.onHold = true // true、false
UTSVoipMgr.heldCall(params, (resp) => {
let flag = resp.flag
if(flag) {
getApp().globalData.uuid = resp.uuid
}
})
- 取消自动挂断电话,仅支持iOS
UTSVoipMgr.deleteWaitingResponseUuid({
uuid: "xxx"
})
- 获取app运行状态,仅支持iOS
let state = UTSVoipMgr.getApplicationState()
switch (state) {
// active
case 0:
break;
// unactive
case 1:
break;
// background
case 2:
break;
default:
break;
}
- 设置app是否处理后端发起的voip
let enable = true // 默认为true,当为false时,app不处理后端发送的voip业务
UTSVoipMgr.setVOIPEnable(enable)
- 设置桌面icon角标,仅支持iOS
let badge = 0
UTSLocalNotification.setBadgeNum(badge, (resp)=>{
console.log(JSON.stringify(resp))
})
- 设置来电调用接口的数据,仅支持Android
let biz = {}
// 第一种:code = 0
// 设置来电时调用的接口服务器
// 服务器收到的数据如下:
// pkg、callState、appState、phoneNumber这几个字段是插件自动添加的
// pkg: 包名
// callState: 来电状态,0: 空闲/挂断 1: 来电响铃 2:去电挂起
// phoneNumber: 来电手机号,不一定能获取到
// 请求
// request: {"userId":"xxx","pkg":"uni.UNI806E8E1","appState":2,"callState":1,"phoneNumber":"18820406059"}
// 响应
// response: {"code": "0", "pkg": "uni.UNI806E8E1"}
// code: 0表示要唤醒app,pkg表示唤醒app的包名
// biz.code = 0 // 0: 收到来电时,当app未启动、后台运行、后台运行时,自动调用服务器接口
// biz.serverInfo = {
// url: "http://192.168.0.101:3000/post",
// method: "POST",
// data: {
// userId: "xxx"
// },
// header: {
// token: "45asdfasfd"
// },
// timeout: 5
// }
// 第二种:code = 1: 采用websocket,自动调用服务器接口,通过服务端webSocket来唤醒app
// 当收到数据为: {"code": "0", "pkg": "uni.UNI806E8E1"}
// code: 0表示要唤醒app,pkg表示唤醒app的包名
biz.code = 1 // 1: 采用websocket,自动调用服务器接口,通过服务端webSocket来唤醒app
biz.serverInfo = {
url: "ws://172.16.11.13:8088",
onConnectSucSendData: { // 连接成功后会发送一次数据,一般用来校验userID/token之类的
data: "userID:010",
type: "txt" // type支持txt、bytes,当type为txt时,data是字符串,当type为bytes,data是十六进制字节数字
},
heartbeatData: { // 心跳
// data: [0x00, 0x01, 0x00, 0x01],
// type: "byte"
data: "heartbeat",
type: "txt"
},
heartbeatTimer: 3000 // 心跳间隔时间
}
biz.notification = this.getAndroidNotification() // 设置前台保活通知
UTSVoipMgr.setHandleBusiness(biz)
type:type支持txt、bytes
- 当type为txt时,data是字符串
{
data: "userID:010",
type: "txt"
}
- 当type为bytes,data是十六进制字节数字
{
data: [0x00, 0x01, 0x00, 0x01],
type: "bytes"
}
webSocket、http响应数据的处理:
{"code": "0", "pkg": "uni.UNI806E8E1"}
-
code业务码,如果更多业务请联系作者定制,0:表示唤醒app到前台运行
-
pkg: 唤醒app的包名
-
取消HandleBusiness业务,会断开socket
UTSVoipMgr.cancelHandleBusiness()
- 初始化电话监听,仅支持Android
UTSVoipMgr.initPhoneStateListener()
- 去电拨打电话,仅支持Android
let params = {}
params.uri = "tel:" + this.tel
UTSVoipMgr.startCall(params)
- 是否开启悬浮窗权限,没有开启则跳转到开启页面,仅支持Android
let hasPermission = UTSVoipMgr.canDrawOverlays();
if (!hasPermission) {
this.showModel("当前没有悬浮窗权限,是否去打开权限?", () => {
UTSVoipMgr.goToOpenOverlaysSetting()
}, () => {
})
}
- 跳转到app设置页,仅支持Android
UTSVoipMgr.goToAppSettings()
- 跳转到无障碍服务设置页面,仅支持Android
UTSVoipMgr.goToAccessibilitySettings()