更新记录
2.0.10(2024-12-04)
- iOS增加自定义消息推送图标功能
2.0.9(2024-12-03)
- android启动兼容更多机型
2.0.8(2024-12-02)
- 更新demo
平台兼容性
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里的nativeplugins、nativeResources、Info.plist、AndroidManifest.xml文件到项目根目录
- ios需要商店证书的Identifiers对应app的Capabilities里勾选Push Notifications、Communication Notification,然后在下载打包的.mobileprovision
- 勾选manifest.json里的app模块配置Push(消息推送)
- nativeResources/ios/UniApp.entitlements文件里的aps-environment对应的值是development、production,一般开发阶段使用development,生产发布使用production,根据情况设置
- 修改nativeplugins/wrs-notifiction的文件(ios自定义推送消息图标icon功能): 可参考uniapp官方集成文档https://nativesupport.dcloud.net.cn/NativePlugin/course/package.html#ios-extension
- manifest.json app原生插件配置勾选WRSNotification插件
- ios-extension.json里的identifier改为打包包名+自定义后缀(${包名}.${自定义后缀}),如:
包名为com.wrs.project.WRSVoipProject
后缀为WRSNotificactionServiceExtension
identifier为com.wrs.project.WRSVoipProject.WRSNotificactionServiceExtension
- ios-extension.json里的profile替换为iOS的extension签名文件名,文件名要以ios-为前缀
- ios-com_notification.mobileprovision替换为ios-extension.json里profile的签名文件
- 集成插件,集成插件步骤请参考 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
技术方案
iOS
iOS采用通过后端接口调用官方的voip接口发送数据给app或者调用官方的远程消息推送接口,app收到数据后可以拉起系统来电界面或发送通知到消息栏,通过/voip接口参数判断哪种方式
- 接口/voip,拉起系统来电界面,此方式上架中国地区可能被拒(有些审核过了,有些审核被拒),如果要上架中国地区建议采用发送通知到消息栏方式,上架国外地区不受限制 用户接听或拒绝来电都会拉起app
{
"token": "xxxxx",
"payload": {
"business": {
"type": 0,
"incomingCallParams": {
"hasVideo": true,
"localizedCallerName": "张三来电",
"timeout": null,
"remoteHandle": {
"type": 1,
"value": "12312222222"
}
}
}
}
}
- 接口/voip,发送通知到消息栏,插件收到voip消息后会发送本地通知到通知栏,用户点击通知唤醒app(app未启动时,有些机型收到消息会延迟严重,建议采用方案3采用官方的远程消息来提醒)
{
"token": "xxxxxx",
"payload": {
"business": {
"type": 1,
"notificationParams": {
"title": "张三来电",
"subtitle": "您有一个新的来电",
"body": "您收到一个新的来电",
"sound": "ring.wav",
"badge": 1,
"userInfo": {
"name": "张三",
"age": 12
}
}
}
}
}
- 接口/notification,调用苹果官方的远程消息推送接口发送消息,用户点击消息唤醒app,可靠文档,可以自定义铃声(微信采用这种方式) 注意这里的token不是voip的token,是普通的消息推送token
{
"token": "xx", // 远程消息推送的手机token,由app生成token
"alert": "您有一个音视频来电",
"body": "音视频来电",
"topic": "com.wrs.project.WRSVoipProject", // app包名
"sound": "ring.wav", // 消息通知铃声
"payload": {
"updateNotification": { // 修改消息通知的图标和标题,mutableContent需要设置为1,仅支持iOS 15以上系统
"icon": "http://192.168.0.104:3000/static/wechat.png",
"displayName": "张三来电"
},
"businessParams": {
"name": "wrs123",
"password": 123455
}
},
"mutableContent": 1,
"contentAvailable": 1,
"rawPayload": null
}
app收到的消息结构是:
{
"businessParams": {
"name": "wrs123",
"password": 123455
},
"updateNotification": {
"icon": "http://172.16.11.21:3000/static/wechat.png",
"displayName": "张三来电"
},
"aps": {
"alert": "您有一个音视频来电",
"mutable-content": 1,
"content-available": 1
}
}
android
android通过无障碍服务长期连接websocket,app接收websocket发过来的数据来拉起app websocket发送app的报文如下则会自动拉起app(app需要开启无障碍服务和自启动权限)
- 接口/sendData
pkg:包名
{"code": "0", "pkg": "uni.UNI806E8E1"}
快速跑通demo
- 下载demo示例,使用HBuilderX导入,导入时选择vue3
- 把demo里的static/project-voip-server文件夹移动到其他文件夹(避免打包进app里面),project-voip-server是nodeJS服务端,包含了iOS的官方的voip接口调用,Android的websocket服务器,用于跑通所需功能的服务
- 按照project-voip-server/README.md的说明启动服务
- 将demo里的wrs-uts-voip鼠标右键升级插件到最新版,删除本地基座,重新自定义基座运行
- 将index.vue里的websocket地址改成服务的地址
- iOS的HBuilderx控制台有打印voip token和消息推送token,Android默认的userId是123456
- 电脑浏览器输入服务的swagger的地址(如http://127.0.0.1:3000/doc#/)
- 找到拉起Android的接口地址/sendData输入参数即可拉起Android
- 找到拉起iOS的接口地址/notification或者/voip输入参数即可拉起iOS
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 -legacy -out key.pem
// openssl pkcs12 -in voiptext.p12 -nodes -password pass:1234 -legacy -out key.pem
或者去掉-legacy,有些版本的openssl不需要-legacy参数:
openssl pkcs12 -in voiptext.p12 -nodes -legacy -out key.pem
// 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';
java语言服务器
依赖下面第三方,具体实现接口百度有很多,有需要可以咨询作者 java语言需要cer和p12转成一个p12
<dependency>
<groupId>com.turo</groupId>
<artifactId>pushy</artifactId>
<version>0.13.10</version>
</dependency>
接口
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;
// 获取远程消息token
case "onToken": {
let token = resp.token
if (token) {
// token上传给后端接口服务器,服务器调用苹果或Google的接口发送通知
this.showMsg("远程消息推送 token:" + token)
}
}
break;
// 获取token失败
case "onTokenFail":
{
this.showMsg("远程消息推送token获取失败:" + JSON.stringify(resp))
}
break;
// onWillPresent仅支持iOS
case "onWillPresent":
break;
// app运行中点击了消息,仅支持iOS
case "didReceive": {
this.showMsg("点击了消息:" + JSON.stringify(resp))
}
break;
default:
break;
}
})
如果使用自己的业务服务器,后端接口发起voip参数如下(具体参考project-voip-server服务的swagger文档和nodejs源码):
// app收到voip后拉起来电参数
const payload = {
business: {
type: 0, // 0: 自动拉起电话接听界面
incomingCallParams: { // 参数
hasVideo: true,
localizedCallerName: "张三来电",
timeout: 5,// 可选参数,单位秒,传了timeout参数,无论用户有没有接听电话,定时器到了都会挂断电话,如果想取消定时器请调用deleteWaitingResponseUuid接口取消定时器
remoteHandle: {
type: 1, // 1: generic 2:phoneNumber 3: emailAddress
value: "18820406059"
}
}
}
};
// app收到voip后发送消息通知
const payload = {
business: {
type: 1, // 消息通知
notificationParams: {
title: "标题",
subtitle: "自标题",
body: "body体"
}
}
};
- 注册iOS远程消息推送,仅支持iOS
// 注册iOS远程消息推送,token结果从onCallback里回调
UTSVoipMgr.registerForRemoteNotifications()
- 获取所有消息通知,仅支持iOS
UTSLocalNotification.getDeliveredNotifications((resp)=>{
// {"deliveredNotifications":[{"date":1732975328.0408335,"request":{"identifier":"AF2B91D8-092A-450E-8C9E-BF25BD5E852A","content":{"body":"支付结果通知","summaryArgumentCount":0,"userInfo":{"age":12,"name":"张三","aps":{"alert":"支付结果通知","sound":"ring.wav"}},"categoryIdentifier":"","title":"","summaryArgument":"","launchImageName":"","subtitle":"","threadIdentifier":""}}},{"date":1732975319.8605058,"request":{"identifier":"8092F37E-1DCF-4BDA-A929-C57B7C56D835","content":{"userInfo":{"age":12,"name":"张三","aps":{"alert":"支付结果通知","sound":"ring.wav"}},"body":"支付结果通知","summaryArgumentCount":0,"categoryIdentifier":"","title":"","summaryArgument":"","launchImageName":"","subtitle":"","threadIdentifier":""}}}]}
const deliveredNotifications = resp.deliveredNotifications
})
- 删除某个消息通知,仅支持iOS
const identifier = “”
UTSLocalNotification.removeDeliveredNotifications([identifier])
- 删除所有消息通知,仅支持iOS
UTSLocalNotification.removeAllDeliveredNotifications()
- 设置通知回调,仅支持iOS
UTSLocalNotification.onCallback((resp)=>{
let opt = resp.opt
switch(opt) {
case "willPresent":
break;
case "didReceive":
break;
default:
break;
}
})
- 初始化本地通知,仅支持iOS
UTSLocalNotification.notificationInit()
- 发送本地通知,仅支持iOS
// 发送本地通知
UTSLocalNotification.add({
title: "张三来电",
subtitle: "VOIP",
body: "您收到了一条新消息",
sound: "default",
trigger: {
timeInterval: 0.2,
repeats: false
},
identifier: "123456"
}, (notResp)=>{
})
- 获取点击消息通知启动app时的消息参数
// 一般用于判断用户点击了哪个通知消息启动的
// {"localNotification":{"hasAction":true,"alertBody":"您收到一个新的来电","userInfo":{"name":"张三","age":12},"regionTriggersOnce":true,"fireDate":"2024-11-29T17:20:56+0800","alertTitle":"张三来电","repeatInterval":0,"soundName":"UILocalNotificationDefaultSoundName","applicationIconBadgeNumber":1}}
let launchParams = UTSVoipMgr.getLanuchParams()
- 来电,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()