头图

HarmonyOS Next 最佳实践之呼叫页面响铃与震动实现

1、背景介绍

image.png
在开发音视频通话模块中,在发起呼叫和接收呼叫页面一般都会有呼叫铃声,类似与微信的视频通话页面,在被叫方一般还会伴随着振动。在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的播放状态变化示意图:
image.png

接下来介绍铃声播放的工具RingAVPlayer,播放器的一般流程如下:

  1. 创建播放器:let avPlayer: media.AVPlayer = await media.createAVPlayer();
  2. 设置状态变化回调:avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {});
  3. 获取本地音频资源fd:let fileDescriptor = await context.resourceManager.getRawFd('ring.mp3');
  4. 为fdSrc赋值触发initialized状态机上报:avPlayer.fdSrc = fileDescriptor;
  5. 状态回调中收到initialized回调,调用avPlayer.prepare()触发资源加载;
  6. 状态回调中收到prepared回调,调用avPlayer.play()启动播放,在启动播放前调用avPlayer.loop = true;设置为循环播放;
  7. 调用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。


轻口味
28.1k 声望4.5k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei