应用内通话消息,支持应用实现网络音视频通话的能力。当终端处于锁屏或解锁两种不同状态时,Push Kit将分别进行以下处理:
终端处于锁屏状态时,可在锁屏上点击接听或拒绝按钮。锁屏状态下只支持接听语音。
终端处于解锁状态时,网络音视频通话呼叫消息显性展示于横幅,支持用户接听视频或语音。
接听视频时会拉起应用内的接听界面。接通后,可以正常挂断(主动挂断/被动挂断)应用内通话消息。
应用内通话消息样式可参考如下示例,真实样式请以实际效果为准:
| 锁屏 | 来电横幅 |
|---|---|
![]() | ![]() |
调测阶段,每个项目每日全网最多可推送1000条测试消息。发送测试消息需设置testMessage为true。
正式发布阶段,单设备单应用下每日推送消息总条数受设备消息频控限制,系统会根据使用场景和流量进行管控,不合理的使用场景系统会进行频控。
参见指导获取Push Token。
在您的工程内创建一个UIAbility类型的组件,如VoIPUIAbility.ets(在项目工程的src/main/ets/entryability目录下),负责处理应用内通话消息的主流程,并完成onCreate()、onWindowStageCreate()、onDestroy()方法的覆写,代码示例如下:
import { 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';
const DOMAIN = 0x0000;
export default class VoIPUIAbility extends UIAbility {
onCreate(): void {
hilog.info(DOMAIN, 'testTag', 'VoIPUIAbility onCreate');
try {
pushService.receiveMessage('VoIP', this, (data) => {
// 处理应用内通话消息数据
try {
VoipCallService.processVoIPMainMsg(data.data, this.context);
} catch (error) {
hilog.error(DOMAIN, 'testTag', 'Failed to process VoIP message: %{public}d %{public}s', error.code,
error.message);
}
});
} catch (e) {
hilog.error(DOMAIN, 'testTag', `Failed to register VoIP, error: ${e.code}, ${e.message}.`);
}
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, 'testTag', 'VoIPUIAbility onWindowStageCreate.');
windowStage.loadContent('pages/CalleePage').catch((err: BusinessError) => {
hilog.error(DOMAIN, 'testTag', `Failed to load content, error: ${err.code}, ${err.message}.`);
});
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', 'VoIPUIAbility onDestroy');
}
} VoipCallService.ets(在项目工程的src/main/ets/service目录下),处理应用内通话消息,代码示例如下:
import { 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;
}
const DOMAIN = 0x0000;
export class VoipCallService {
private static callId: string | undefined;
public static async processVoIPMainMsg(data: string,
context: common.UIAbilityContext): Promise<void> {
hilog.info(DOMAIN, 'testTag', `Process VoIP message: ${data}`);
let content: Content;
let scene: VoipScene;
let callId: string;
try {
content = JSON.parse(data);
scene = JSON.parse(content.data);
callId = content.callId;
if (!callId) {
hilog.error(DOMAIN, 'testTag', 'CallId is null');
}
VoipCallService.callId = callId;
} catch (e) {
let err: BusinessError = e as BusinessError;
hilog.error(DOMAIN, 'testTag', 'Failed to parse VoIP message data: %{public}d %{public}s', err.code, err.message);
return;
}
try {
// 注册voipCallUiEvent事件
voipCall.on('voipCallUiEvent', async (event) => {
hilog.info(DOMAIN, 'testTag', `Process voip call ui event: ${JSON.stringify(event)}.`);
await VoipCallService.processVoipCallEvent(event.voipCallUiEvent);
});
} catch (err) {
let e: BusinessError = err as BusinessError;
hilog.error(DOMAIN, 'testTag', 'Failed to register event: %{public}d %{public}s', e.code, e.message);
}
const resourceMgr: resourceManager.ResourceManager = context.resourceManager;
let fileData: Uint8Array = new Uint8Array(0);
try {
// example.png表示用户头像,取值为“/resources/rawfile”路径下的文件名
fileData = await resourceMgr.getRawFileContent('example.png');
} catch (e) {
hilog.error(DOMAIN, '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(DOMAIN, 'testTag',
`User profile imageInfo: ${imageInfo.size.width} * ${imageInfo.size.height}.`);
} else {
hilog.error(DOMAIN, 'testTag', `Failed to obtain the image information.code is ${err.code}, message is ${err.message}`);
}
});
}
// 构造上报来电的参数。注意,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(DOMAIN, 'testTag', `ReportIncomingCall result: ${error}.`);
} catch (err) {
let e: BusinessError = err as BusinessError;
hilog.error(DOMAIN, '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(DOMAIN, 'testTag', 'Failed to report call state change: %{public}d %{public}s', e.code, e.message);
}
}
public static close(): void {
hilog.info(DOMAIN, '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(DOMAIN, 'testTag', 'Failed to unregister event: %{public}d %{public}s', e.code, e.message);
}
}
}
需要在项目工程的src/main/resources/rawfile目录下添加example.png,表示来电时的用户头像。
在项目工程的 src/main/ets/pages目录添加:视频接听页面CalleePage.ets,代码示例如下:
import CallComponent from '../component/CallComponent';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0000;
@Entry
@Component
struct CalleePage {
@StorageLink('close') @Watch('close') end: boolean | undefined = undefined;
aboutToAppear() {
hilog.info(DOMAIN, 'testTag', 'CalleePage aboutToAppear');
this.end = false;
}
private close() {
if (this.end) {
hilog.info(DOMAIN, 'testTag', 'CalleePage close');
this.getUIContext().getRouter().back(); // 此处仅为示例(跳转返回),请根据实际情况设定路由
}
}
aboutToDisappear() {
hilog.info(DOMAIN, 'testTag', 'CalleePage aboutToDisappear');
}
build() {
Column() {
CallComponent({})
}
}
} CallComponent.ets(在项目工程的src/main/ets/component目录下),代码示例如下:
import { VoipCallService } from '../service/VoipCallService';
import { voipCall } from '@kit.CallServiceKit';
@Component
export 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": "$string:module_desc",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": false,
"skills": [
// 保持现有skill对象不变
// 新增一个独立的skill对象,配置actions参数为action.ohos.push.listener,有且只能有一个ability定义该action
{
"actions": [
"action.ohos.push.listener"
]
}
]
}
// ...
] 应用服务端调用REST API推送消息,消息详情可参见场景化消息API接口功能介绍。
如果您需要呼叫,应用服务器可以调用REST API推送应用内通话消息,请求示例如下:
// Request URL
POST "https://push-api.cloud.huawei.com/v3/[projectId]/messages:send"
// Request Header
Content-Type: application/json
Authorization: Bearer eyJr*****OiIx---****.eyJh*****iJodHR--***.QRod*****4Gp---****
push-type: 10
// Request Body
{
"pushOptions": {
"ttl": 30
},
"payload": {
"extraData": "{\"scene\": \"voice\"}"
},
"target": {
"token": ["MAMzLg**********aZW"]
}
}
应用内通话消息只能用于音视频通话场景唤醒应用,完成呼叫,不要通过此种类型消息来挂断来电或者和应用通信,应用应该使用自己建立的网络连接和应用通信。相比应用服务器推送Push消息,使用现有的网络连接和应用通信通常会更快,在网络不佳的情况下,推送的Push消息可能无法到达应用。
应用无论是否在前台,自己的网络连接存在时,建议您通过Push推送应用内通话消息,再通过自己的网络连接发送通话消息,保证该呼叫能够到达应用。
如果您需要给被叫方发送未接来电通知,应用服务器可以调用REST API推送通知消息。以通知消息为例,请求示例如下:
// Request URL
POST "https://push-api.cloud.huawei.com/v3/[projectId]/messages:send"
// Request Header
Content-Type: application/json
Authorization: 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"]
}
}