HarmonyOS Next 最佳实践之呼叫页面响铃与震动实现
1、背景介绍
在开发音视频通话模块中,在发起呼叫和接收呼叫页面一般都会有呼叫铃声,类似与微信的视频通话页面,在被叫方一般还会伴随着振动。在Android端可以使用MediaPlayer循环播放raw中的mp3铃声,使用Vibrator实现振动效果。本文介绍在HarmonyOS Next如何实现铃声播放与振动效果。
2、响铃实现
出于包体积大小考虑,一般我们会在应用内放一个简短的铃声,通过重复播放实现持续响铃。一般通话页面会设置最长时长一分钟的限制,铃声最多可以持续播放一分钟。
我们将铃声文件放置到rawfile中,通过resourceManager读取。HarmonyOS 提供了两种方式播放音频:
- SoundPool:可以实现低时延短音播放。当应用开发时,经常需要使用一些急促简短的音效(如相机快门音效、系统通知音效等),此时建议调用SoundPool,实现一次加载,多次低时延播放。SoundPool当前支持播放1MB以下的音频资源,大小超过1MB的长音频将截取1MB大小数据进行播放。
- AVPlayer:使用AVPlayer可以实现端到端播放原始媒体资源,
由于SoundPool的load加载资源方法不支持加载rawfile目录资源,所以我们主要介绍AVPlayer实现音频播放。
AVPlayer播放的全流程包含:创建AVPlayer,设置播放资源,设置播放参数(音量/倍速/焦点模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。
AVPlayer类似于Android的MediaPlayer,里面维护了一个状态机,可以通过AVPlayer的state属性主动获取当前状态或使用on('stateChange')方法监听状态变化。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。
下面是AVPlayer的播放状态变化示意图:
接下来介绍铃声播放的工具RingAVPlayer,播放器的一般流程如下:
- 创建播放器:
let avPlayer: media.AVPlayer = await media.createAVPlayer();
- 设置状态变化回调:
avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {});
- 获取本地音频资源fd:
let fileDescriptor = await context.resourceManager.getRawFd('ring.mp3');
- 为fdSrc赋值触发initialized状态机上报:
avPlayer.fdSrc = fileDescriptor;
- 状态回调中收到initialized回调,调用avPlayer.prepare()触发资源加载;
- 状态回调中收到prepared回调,调用avPlayer.play()启动播放,在启动播放前调用
avPlayer.loop = true;
设置为循环播放; - 调用stop方法停止播放。
整体封装工具如下:
import { media } from '@kit.MediaKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { audio } from '@kit.AudioKit';
import { Logg } from '../../../../../../../Index';
const TAG = 'RingAVPlayer';
export class RingAVPlayer {
private mAVPlayer: media.AVPlayer|undefined = undefined;
private count: number = 0;
// 注册avplayer回调函数
setAVPlayerCallback(avPlayer: media.AVPlayer) {
// seek操作结果回调函数
avPlayer.on('seekDone', (seekDoneTime: number) => {
Logg.i(TAG, `AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
})
// error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程
avPlayer.on('error', (err: BusinessError) => {
Logg.e(TAG, `Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
avPlayer.reset(); // 调用reset重置资源,触发idle状态
})
// 状态机变化回调函数
avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info('AVPlayer state idle called.');
avPlayer.release(); // 调用release接口销毁实例对象
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
Logg.i(TAG, 'AVPlayer state initialized called.');
avPlayer.audioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
rendererFlags: 0
}
avPlayer.prepare();
break;
case 'prepared': // prepare调用成功后上报该状态机
Logg.i(TAG, 'AVPlayer state prepared called.');
avPlayer.loop = true;
avPlayer.play(); // 调用播放接口开始播放
break;
case 'playing': // play成功调用后触发该状态机上报
Logg.i(TAG, 'AVPlayer state playing called.');
break;
case 'paused': // pause成功调用后触发该状态机上报
Logg.i(TAG, 'AVPlayer state paused called.');
break;
case 'completed': // 播放结束后触发该状态机上报
Logg.i(TAG, 'AVPlayer state completed called.');
break;
case 'stopped': // stop接口成功调用后触发该状态机上报
Logg.i(TAG, 'AVPlayer state stopped called.');
break;
case 'released':
Logg.i(TAG, 'AVPlayer state released called.');
break;
default:
Logg.i(TAG, 'AVPlayer state unknown called.');
break;
}
})
}
// 以下demo为使用资源管理接口获取打包在HAP内的媒体资源文件并通过fdSrc属性进行播放示例
async startAVPlayer() {
// 创建avPlayer实例对象
this.mAVPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数
this.setAVPlayerCallback(this.mAVPlayer);
// 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址
// 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度
let context = getContext(this) as common.UIAbilityContext;
let fileDescriptor = await context.resourceManager.getRawFd('call_music.mp3');
let avFileDescriptor: media.AVFileDescriptor =
{ fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };
// 为fdSrc赋值触发initialized状态机上报
this.mAVPlayer.fdSrc = avFileDescriptor;
}
stopAVPlayer() {
if (this.mAVPlayer) {
this.mAVPlayer.stop();
this.mAVPlayer.release();
}
}
}
3、震动实现
Android震动使用Vibrator,可以用下面代码启动振动:
protected long[] patter = { 1000, 1000, 1000, 1000 };
mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
mVibrator.vibrate(patter, 0);
数组中的每个元素表示振动的持续时间(毫秒),数组的第一个元素表示启动振动前的等待时间,后续元素交替表示振动和停止的时间。repeat
参数决定了振动的重复次数,-1
表示不重复,0
表示一直振动。{ 1000, 1000, 1000, 1000 }
表示先等待1秒,然后振动1秒,再等待1秒,再振动一秒,重复振动。最后可以调用cancel方法取消振动。
HarmonyOS Next也提供了对应震动能力的方法,当设备需要设置不同的振动效果时,可以调用Vibrator模块,例如:设备的按键可以设置不同强度和不同时长的振动,闹钟和来电可以设置不同强度和时长的单次或周期振动。
目前支持三类振动效果,如下所示:
名称 | 说明 |
---|---|
固定时长振动 | 传入一个固定时长,马达按照默认强度和频率触发振动,振动效果描述请参考VibrateTime |
预置振动 | 系统中的预置振动效果,这些效果适用于某些固定场景,比如效果"haptic.clock.timer"通常用于用户调整计时器时的振感反馈,振动效果描述请参考VibratePreset |
自定义振动 | 自定义振动提供给用户设计自己所需振动效果的能力,用户可通过自定义振动配置文件,并遵循相应规则编排所需振动形式,使能更加开放的振感交互体验,效果描述请参考VibrateFromFile |
下面是示例代码:
按照指定持续时间触发马达振动:
import { vibrator } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
try {
// 触发马达振动
vibrator.startVibration({
type: 'time',
duration: 1000,
}, {
id: 0,
usage: 'alarm'
}, (error: BusinessError) => {
if (error) {
console.error(`Failed to start vibration. Code: ${error.code}, message: ${error.message}`);
return;
}
console.info('Succeed in starting vibration');
});
} catch (err) {
let e: BusinessError = err as BusinessError;
console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}
//停止固定时长振动
import { vibrator } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
try {
// 按照VIBRATOR_STOP_MODE_TIME模式停止振动
vibrator.stopVibration(vibrator.VibratorStopMode.VIBRATOR_STOP_MODE_TIME, (error: BusinessError) => {
if (error) {
console.error(`Failed to stop vibration. Code: ${error.code}, message: ${error.message}`);
return;
}
console.info('Succeed in stopping vibration');
})
} catch (err) {
let e: BusinessError = err as BusinessError;
console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}
按照预置振动效果触发马达振动,可先查询振动效果是否被支持,再调用振动接口:
import { vibrator } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
try {
// 查询是否支持'haptic.effect.soft'
vibrator.isSupportEffect('haptic.effect.soft', (err: BusinessError, state: boolean) => {
if (err) {
console.error(`Failed to query effect. Code: ${err.code}, message: ${err.message}`);
return;
}
console.info('Succeed in querying effect');
if (state) {
try {
// 触发马达振动
vibrator.startVibration({
type: 'preset',
effectId: 'haptic.effect.soft',
count: 1,
intensity: 50,
}, {
usage: 'unknown'
}, (error: BusinessError) => {
if (error) {
console.error(`Failed to start vibration. Code: ${error.code}, message: ${error.message}`);
} else {
console.info('Succeed in starting vibration');
}
});
} catch (error) {
let e: BusinessError = error as BusinessError;
console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}
}
})
} catch (error) {
let e: BusinessError = error as BusinessError;
console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}
//停止预置振动
import { vibrator } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
try {
// 按照VIBRATOR_STOP_MODE_PRESET模式停止振动
vibrator.stopVibration(vibrator.VibratorStopMode.VIBRATOR_STOP_MODE_PRESET, (error: BusinessError) => {
if (error) {
console.error(`Failed to stop vibration. Code: ${error.code}, message: ${error.message}`);
return;
}
console.info('Succeed in stopping vibration');
})
} catch (err) {
let e: BusinessError = err as BusinessError;
console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}
effectId有几种可选的情况。
按照自定义振动配置文件触发马达振动:
import { vibrator } from '@kit.SensorServiceKit';
import { resourceManager } from '@kit.LocalizationKit';
import { BusinessError } from '@kit.BasicServicesKit';
const fileName: string = 'xxx.json';
// 获取文件资源描述符
let rawFd: resourceManager.RawFileDescriptor = getContext().resourceManager.getRawFdSync(fileName);
// 触发马达振动
try {
vibrator.startVibration({
type: "file",
hapticFd: { fd: rawFd.fd, offset: rawFd.offset, length: rawFd.length }
}, {
id: 0,
usage: 'alarm'
}, (error: BusinessError) => {
if (error) {
console.error(`Failed to start vibration. Code: ${error.code}, message: ${error.message}`);
return;
}
console.info('Succeed in starting vibration');
});
} catch (err) {
let e: BusinessError = err as BusinessError;
console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}
// 关闭文件资源描述符
getContext().resourceManager.closeRawFdSync(fileName);
//停止所有振动
import { vibrator } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
try {
// 停止所有模式的马达振动
vibrator.stopVibration((error: BusinessError) => {
if (error) {
console.error(`Failed to stop vibration. Code: ${error.code}, message: ${error.message}`);
return;
}
console.info('Succeed in stopping vibration');
})
} catch (error) {
let e: BusinessError = error as BusinessError;
console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}
4、参考文档
4、总结
本文介绍HarmonyOS Next系统在音视频通话场景中,呼叫页面中的自定义铃声播放和振动效果的事情,重点介绍了AVPlayer与vibrator相关API。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。