更新记录

2.0.10(2024-12-04)

  1. iOS增加自定义消息推送图标功能

2.0.9(2024-12-03)

  1. android启动兼容更多机型

2.0.8(2024-12-02)

  1. 更新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

集成步骤

  1. 拷贝demo里的nativeplugins、nativeResources、Info.plist、AndroidManifest.xml文件到项目根目录
  2. ios需要商店证书的Identifiers对应app的Capabilities里勾选Push Notifications、Communication Notification,然后在下载打包的.mobileprovision
  3. 勾选manifest.json里的app模块配置Push(消息推送)
  4. nativeResources/ios/UniApp.entitlements文件里的aps-environment对应的值是development、production,一般开发阶段使用development,生产发布使用production,根据情况设置
  5. 修改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的签名文件
  1. 集成插件,集成插件步骤请参考 https://www.cnblogs.com/wenrisheng/p/18323027
  2. android需要在app设置里开启“自启动”、“悬浮窗”权限,在系统设置里面搜索“无障碍”,进入“已下载的服务”里开启无障碍服务,在系统设置里面搜索“使用情况访问权限”,开启“允许查看使用情况”

iOS VOIP服务器配置

  1. 登录苹果开发者系统生成VOIP证书(百度有很多资料),这个证书是voip的证书(Certificates->Services->VoIP Services Certificate),不是消息推送的证书
  2. 将cer证书和p12文件生成各自后端语言支持的证书,如:
  3. 其他语言可以参考苹果官方https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns

技术方案

iOS

iOS采用通过后端接口调用官方的voip接口发送数据给app或者调用官方的远程消息推送接口,app收到数据后可以拉起系统来电界面或发送通知到消息栏,通过/voip接口参数判断哪种方式

  1. 接口/voip,拉起系统来电界面,此方式上架中国地区可能被拒(有些审核过了,有些审核被拒),如果要上架中国地区建议采用发送通知到消息栏方式,上架国外地区不受限制 用户接听或拒绝来电都会拉起app

{
  "token": "xxxxx",
  "payload": {
    "business": {
      "type": 0,
      "incomingCallParams": {
        "hasVideo": true,
        "localizedCallerName": "张三来电",
        "timeout": null,
        "remoteHandle": {
          "type": 1,
          "value": "12312222222"
        }
      }
    }
  }
}
  1. 接口/voip,发送通知到消息栏,插件收到voip消息后会发送本地通知到通知栏,用户点击通知唤醒app(app未启动时,有些机型收到消息会延迟严重,建议采用方案3采用官方的远程消息来提醒)

{
  "token": "xxxxxx",
  "payload": {
    "business": {
      "type": 1,
      "notificationParams": {
        "title": "张三来电",
        "subtitle": "您有一个新的来电",
        "body": "您收到一个新的来电",
        "sound": "ring.wav",
        "badge": 1,
        "userInfo": {
          "name": "张三",
          "age": 12
        }
      }
    }
  }
}
  1. 接口/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需要开启无障碍服务和自启动权限)

  1. 接口/sendData
pkg:包名
{"code": "0", "pkg": "uni.UNI806E8E1"}

快速跑通demo

  1. 下载demo示例,使用HBuilderX导入,导入时选择vue3
  2. 把demo里的static/project-voip-server文件夹移动到其他文件夹(避免打包进app里面),project-voip-server是nodeJS服务端,包含了iOS的官方的voip接口调用,Android的websocket服务器,用于跑通所需功能的服务
  3. 按照project-voip-server/README.md的说明启动服务
  4. 将demo里的wrs-uts-voip鼠标右键升级插件到最新版,删除本地基座,重新自定义基座运行
  5. 将index.vue里的websocket地址改成服务的地址
  6. iOS的HBuilderx控制台有打印voip token和消息推送token,Android默认的userId是123456
  7. 电脑浏览器输入服务的swagger的地址(如http://127.0.0.1:3000/doc#/)
  8. 找到拉起Android的接口地址/sendData输入参数即可拉起Android
  9. 找到拉起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

  1. 当type为txt时,data是字符串

{
    data: "userID:010",
    type: "txt" 
}
  1. 当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()

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

使用中有什么不明白的地方,就向插件作者提问吧~ 我要提问