推送应用内通话消息
场景介绍
应用内通话消息,支持应用实现网络音视频通话的能力。当终端处于锁屏或解锁两种不同状态时,Push Kit将分别进行以下处理:
- 终端处于锁屏状态时,可在锁屏上点击接听或拒绝按钮。锁屏状态下只支持接听语音。
- 终端处于解锁状态时,网络音视频通话呼叫消息显性展示于横幅,支持用户接听视频或语音。
接听视频时会拉起应用内的接听界面。接通后,可以正常挂断(主动挂断/被动挂断)应用内通话消息。
应用内通话消息样式可参考如下示例,真实样式请以实际效果为准:
| 锁屏 | 来电横幅 |
|---|---|
约束与限制
应用内通话消息支持Phone、Tablet设备,并且从6.1.0(23)版本开始,新增支持Lite Wearable设备。
开通权益
推送应用内通话消息需要申请场景化消息权益,请参见申请推送应用内通话消息权益。
频控规则
调测阶段,每个项目每日全网最多可推送1000条测试消息。发送测试消息需设置testMessage为true。
正式发布阶段,单设备单应用下每日推送消息总条数受设备消息频控限制,系统会根据现网使用场景和流量进行管控,不合理的使用场景系统会进行频控。
开发步骤
-
参见指导获取Push Token。
-
在您的工程内创建一个UIAbility类型的组件,如VoIPUIAbility.ets(在项目工程的src/main/ets/entryability目录下),负责处理应用内通话消息的主流程,并完成onCreate()、onWindowStageCreate()、onDestroy()方法的覆写,代码示例如下:
// 文件路径: src/main/ets/entryability/VoIPUIAbility.etsimport { UIAbility } from '@kit.AbilityKit';import { pushService } from '@kit.PushKit';import { window } from '@kit.ArkUI';import { hilog } from '@kit.PerformanceAnalysisKit';import { VoipCallService } from '../service/VoipCallService';import { BusinessError } from '@kit.BasicServicesKit';export default class VoIPUIAbility extends UIAbility {onCreate(): void {hilog.info(0x0000, 'testTag', `VoIPUIAbility onCreate`);try {pushService.receiveMessage('VoIP', this, async (data) => {// process message,并建议对Callback进行try-catchtry {await VoipCallService.processVoIPMainMsg(data.data, this.context);} catch (error) {hilog.error(0x0000, 'testTag', 'Failed to process VoIP message: %{public}d %{public}s',error.code,error.message);}});} catch (e) {hilog.error(0x0000, 'testTag', `Failed to register VOIP, error: ${e.code}, ${e.message}.`);}}onWindowStageCreate(windowStage: window.WindowStage): void {hilog.info(0x0000, 'testTag', `VoIPUIAbility onWindowStageCreate`);windowStage.loadContent('pages/CalleePage').catch((err: BusinessError) => {hilog.error(0x0000, 'testTag', `Failed to load content, error: ${err.code}, ${err.message}.`);});}onDestroy(): void {hilog.info(0x0000, 'testTag', 'VoIPUIAbility onDestroy');}}VoipCallService.ets(在项目工程的src/main/ets/service目录下),处理应用内通话消息,代码示例如下:
// 文件路径: src/main/ets/service/VoipCallService.etsimport { voipCall } from '@kit.CallServiceKit';import { hilog } from '@kit.PerformanceAnalysisKit';import { common } from '@kit.AbilityKit';import { image } from '@kit.ImageKit';import { resourceManager } from '@kit.LocalizationKit';import { BusinessError } from '@kit.BasicServicesKit';export interface VoipScene {scene: string;}export interface Content {data: string;header: string;callId: string;}export class VoipCallService {private static callId: string | undefined;public static async processVoIPMainMsg(data: string,context: common.UIAbilityContext): Promise<void> {hilog.info(0x0000, 'testTag', `Process VoIP message: ${data}`);let content: Content = JSON.parse(data);let scene: VoipScene = JSON.parse(content.data);let callId: string = content.callId;if (!callId) {hilog.error(0x0000, 'testTag', `CallId is null`);}VoipCallService.callId = callId;try {// 注册voipCallUiEvent事件voipCall.on('voipCallUiEvent', async (event) => {hilog.info(0x0000, 'testTag', `Process voip call ui event: ${JSON.stringify(event)}.`);await VoipCallService.processVoipCallEvent(event.voipCallUiEvent);});} catch (err) {let e: BusinessError = err as BusinessError;hilog.error(0x0000, 'testTag', 'Failed to register event: %{public}d %{public}s', e.code, e.message);}const resourceMgr: resourceManager.ResourceManager = context.resourceManager;// example.png表示用户头像,取值为“/resources/rawfile”路径下的文件名let fileData: Uint8Array = new Uint8Array(0);try {fileData = await resourceMgr.getRawFileContent('example.png');} catch (e) {hilog.error(0x0000, 'testTag', 'Failed to get raw file: %{public}d %{public}s', e.code, e.message);}const buffer = fileData.buffer;const imageSource: image.ImageSource = image.createImageSource(buffer);const pixelMap: image.PixelMap = await imageSource.createPixelMap();if (pixelMap) {pixelMap.getImageInfo((err, imageInfo) => {if (imageInfo) {hilog.info(0x0000, 'testTag',`User profile imageInfo: ${imageInfo.size.width} * ${imageInfo.size.height}.`);}});}// 构造上报来电的参数。注意,voipCallType.scene为您自定义的场景类型字段,从云侧推送消息时,请注意与端侧取值保持一致let call: voipCall.VoipCallAttribute = {callId: callId,voipCallType: scene?.scene === 'video' ? voipCall.VoipCallType.VOIP_CALL_VIDEO :voipCall.VoipCallType.VOIP_CALL_VOICE,userName: 'push',userProfile: pixelMap,abilityName: 'VoIPUIAbility',voipCallState: voipCall.VoipCallState.VOIP_CALL_STATE_RINGING};try {// 上报来电let error = await voipCall.reportIncomingCall(call);hilog.info(0x0000, 'testTag', `ReportIncomingCall result: ${error}.`);} catch (err) {let e: BusinessError = err as BusinessError;hilog.error(0x0000, 'testTag', 'Failed to report incoming call: %{public}d %{public}s', e.code, e.message);}// ...应用播放振动和铃声}public static async processVoipCallEvent(event: voipCall.VoipCallUiEvent) {try {switch (event) {case voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_VOICE_ANSWER:case voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_VIDEO_ANSWER:// 立即向Call Service Kit上报answered状态await voipCall.reportCallStateChange(VoipCallService.callId,voipCall.VoipCallState.VOIP_CALL_STATE_ANSWERED);// ...在应用内完成接听// 应用内接听后,向Call Service Kit上报active状态await voipCall.reportCallStateChange(VoipCallService.callId,voipCall.VoipCallState.VOIP_CALL_STATE_ACTIVE);break;case voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_REJECT:case voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_HANGUP:// ...应用内完成挂断// 向Call Service Kit上报通话状态await voipCall.reportCallStateChange(VoipCallService.callId,voipCall.VoipCallState.VOIP_CALL_STATE_DISCONNECTED);break;default: {break;}}} catch (err) {let e: BusinessError = err as BusinessError;hilog.error(0x0000, 'testTag', 'Failed to report call state change: %{public}d %{public}s', e.code, e.message);}}public static close(): void {hilog.info(0x0000, 'testTag', `Close VoIP`);VoipCallService.processVoipCallEvent(voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_HANGUP);try {voipCall.off('voipCallUiEvent');} catch (err) {let e: BusinessError = err as BusinessError;hilog.error(0x0000, 'testTag', 'Failed to unregister event: %{public}d %{public}s', e.code, e.message);}}}需要在项目工程的src/main/resources/rawfile目录下添加example.png,表示来电时的用户头像。
- UIAbility.onCreate是同步接口,不支持异步回调,需要在onCreate生命周期的入口,完成pushService.receiveMessage()注册,并且保证在注册前没有等待异步方法执行的调用。
- 在receiveMessage()回调中接收应用内通话消息,建议应用提前和服务器建连,用户点击接听后可以立即进行通话,并调用voipCall.on()接口注册监听通话状态回调。用户点击接听或者拒绝接听之后,系统会通过应用注册的事件监听通话状态回调结果。
- 应用需要在10秒内调用voipCall.reportIncomingCall()接口上报通话来电状态,调用完成之后,系统会弹出应用内通话横幅通知。voipCall.reportIncomingCall()接口入参中的callId需要使用receiveMessage()回调中的callId。
- 如果应用来电消息建立失败,需要调用voipCall.reportIncomingCallError()通知来电消息建立失败。如果应用在前台,通过自己的网络连接接收到来电消息,调用voipCall.reportIncomingCall()接口上报了通话来电状态,后面才收到Push推送的应用内通话消息,在该消息处理中需要调用voipCall.reportIncomingCallError()上报应用线路忙。
- 应用内通话主要有三种回调状态,分别为:接听状态、拒绝状态和挂断状态。
- 在接听状态回调中,应用在建立连接成功之后,需要调用voipCall.reportCallStateChange()接口上报通话激活状态。
- 在拒绝接听状态回调中,应用断开和服务器的连接之后,需要调用voipCall.reportCallStateChange()接口上报通话断开状态。
- 在应用进行应用内通话的同时,若运营商来电,会弹出运营商来电接听界面,用户点击接听运营商来电之后,会回调应用内通话挂断状态,在回调方法中应用需要自行断开和服务器的连接,并调用voipCall.reportCallStateChange()接口上报通话断开状态。
- 有关应用内通话回调状态的更多信息,详情请参见Call Service Kit简介。
- 应用上报通话来电状态之后,可以调用vibrator.startVibration触发振动,有关振动的更多详情,请参见Sensor Service Kit简介。可以使用AVPlayer播放应用铃声,音频流建议设置为铃声,usage设置为STREAM_USAGE_RINGTONE,效果为开始响铃,播放的音乐会暂停播放。同时推荐使用AudioSession管理音频焦点,可以保证接听过程中、通话过程中都保持音频焦点,详情请参见Audio Kit简介。
- 进行音视频通话时,若您的应用处于Overhead场景(设备发热严重或负载较重,Level=4),请降低码率和帧率,或关闭视频流降级为音频。相关说明请参见Basic Services Kit(基础服务)提供的接口getLevel()。
-
在项目工程的 src/main/ets/pages目录添加:视频接听页面CalleePage.ets,代码示例如下:
// 文件路径: src/main/ets/pages/CalleePage.etsimport CallComponent from '../component/CallComponent';import { hilog } from '@kit.PerformanceAnalysisKit';@Entry@Componentstruct CalleePage {@StorageLink('close') @Watch('close') end: boolean | undefined = undefined;aboutToAppear() {hilog.info(0x0000, 'testTag', `CalleePage aboutToAppear`);this.end = false;}private close() {if (this.end) {hilog.info(0x0000, 'testTag', `CalleePage close`);this.getUIContext().getRouter().back(); // 此处仅为示例(跳转返回),请根据实际情况设定路由}}aboutToDisappear() {hilog.info(0x0000, 'testTag', `CalleePage aboutToDisappear`);}build() {Column() {CallComponent({})}}}CallComponent.ets(在项目工程的src/main/ets/component目录下),代码示例如下:
// 文件路径: src/main/ets/component/CallComponent.etsimport { VoipCallService } from '../service/VoipCallService';import { voipCall } from '@kit.CallServiceKit';@Componentexport default struct CallComponent {@StorageLink('close') end: boolean | undefined = undefined;build() {Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween }) {Row() {}.width('100%').justifyContent(FlexAlign.Center)Row({ space: 30 }) {Column() {Button().width(80).height(80).backgroundColor(Color.Green).onClick(() => {VoipCallService.processVoipCallEvent(voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_VIDEO_ANSWER);})Text('Answer').fontColor(Color.White).padding({ top: 5 })}Column() {Button().width(80).height(80).backgroundColor(Color.Red).onClick(() => {this.end = true;VoipCallService.close();})Text('Hang Up').fontColor(Color.White).padding({ top: 5 })}}.width('100%').justifyContent(FlexAlign.Center)}.padding('30 10').backgroundColor(Color.Black)}}在项目工程的 src/main/resources/base/profile/main_pages.json添加page目录,示例如下:
{"src": ["pages/Index","pages/CalleePage"]}示例代码提供的页面效果仅供开发参考,不代表最终效果。
-
在项目工程的 src/main/module.json5 文件的abilities模块中配置VoIPUIAbility的 actions 信息。
"abilities": [{"name": "VoIPUIAbility","srcEntry": "./ets/entryability/VoIPUIAbility.ets","launchType": "singleton","description": "VoIPUIAbility test","startWindowIcon": "$media:startIcon","startWindowBackground": "$color:start_window_background","exported": false,"skills": [// 保持现有skill对象不变// 新增一个独立的skill对象,配置actions参数{"actions": ["action.ohos.push.listener"]}]}]- actions:内容为action.ohos.push.listener,有且只能有一个ability定义该action,若同时添加uris参数,则uris内容需为空。
-
应用服务端调用REST API推送消息,消息详情可参见场景化消息API接口功能介绍。
应用内通话消息
-
如果您需要呼叫,应用服务器可以调用REST API推送应用内通话消息,请求示例如下:
// Request URLPOST "https://push-api.cloud.huawei.com/v3/[projectId]/messages:send"// Request HeaderContent-Type: application/jsonAuthorization: Bearer eyJr*****OiIx---****.eyJh*****iJodHR--***.QRod*****4Gp---****push-type: 10// Request Body{"pushOptions": {"ttl": 30},"payload": {"extraData": "{\"scene\": \"voice\"}"},"target": {"token": ["MAMzLg**********aZW"]}}- [projectId]:项目ID,登录AppGallery Connect网站,选择“开发与服务”,在项目列表中选择对应的项目,左侧导航栏选择“项目设置”,在该页面获取。
- Authorization:JWT格式字符串,可参见基于服务账号生成鉴权令牌进行获取。
- push-type:10表示应用内通话消息场景。
- token:Push Token,可参见获取Push Token章节获取。
- extraData:携带的额外数据,字符串类型。详情参见VoIPCallPayload 应用内通话消息中extraData参数用法。extraData数据获取请参考示例代码。
- ttl:消息缓存时间,建议设置为30~60秒,详见pushOptions.ttl。
- 应用内通话消息只能用于音视频通话场景唤醒应用,完成呼叫,不要通过此种类型消息来挂断来电或者和应用通信,应用应该使用自己建立的网络连接和应用通信。相比应用服务器推送Push消息,使用现有的网络连接和应用通信通常会更快,在网络不佳的情况下,推送的Push消息可能无法到达应用。
- 应用无论是否在前台,自己的网络连接存在时,建议您通过Push推送应用内通话消息,再通过自己的网络连接发送通话消息,保证该呼叫能够到达应用。
未接来电通知
-
如果您需要给被叫方发送未接来电通知,应用服务器可以调用REST API推送通知消息。以通知消息为例,请求示例如下:
// Request URLPOST "https://push-api.cloud.huawei.com/v3/[projectId]/messages:send"// Request HeaderContent-Type: application/jsonAuthorization: Bearer eyJr*****OiIx---****.eyJh*****iJodHR--***.QRod*****4Gp---****push-type: 0// Request Body{"pushOptions": {"ttl":86400},"payload": {"notification": {"category": "MISS_CALL","title": "通知标题","body": "通知内容","clickAction": {"actionType": 0},"appMessageId": "12345"}},"target": {"token": ["MAMzLg**********aZW"]}}- push-type:0表示通知消息场景。
- category:消息自分类类别,设置为MISS_CALL,请参见参数说明,发送消息前请确保您已申请通知消息自分类权益。
- appMessageId:应用消息的唯一标识。被叫挂断,被叫方VoIP应用在前台时应用可以通过调用Notification Kit(用户通知服务)发送未接来电通知。被叫方VoIP应用在后台时,可以通过Push推送未接来电通知。应用可能存在前后台状态判断不准确,同一电话会产生两条未接来电,建议您通过Notification Kit和Push Kit推送的未接来电通知使用相同的appMessageId,系统会进行通知去重。
- 其他参数说明可参见通知消息请求体参数说明。