Harmony OS Next 之 音视频开发:如何为用户提供良好的音频体验 - 鸿蒙系统中的焦点策略

648 阅读14分钟

复杂的音视频使用场景

音视频开发时能够正常播放音视频流只是第一步,更重要的一步是合理适配操作系统的特性,根据系统的不同调度做出恰当的处理,带给用户良好的音频体验。由于音视频播放「不依赖前台」以及「强依赖焦点」的特殊性,开发者往往需要考虑更多维度的场景。

假设我们正在开发一款音乐播放软件,暂且称其为「扣扣」音乐,用户打开我们的「扣扣」音乐,选中播放列表开始播放音乐,然后退到后台,此时我们的「扣扣」音乐处于后台播放音乐的状态,此时就经常会发生与其他音视频流的并发打断场景,例如:

  • 用户打开了「网抑云音乐」(网易打钱),播放了其他音乐,或者用户觉得只听歌没意思,打开了「爱优腾」看视频了;
  • 用户这个时候在开车,打开了「高德导航」,开启了导航的语音提示;
  • 用户设置了闹钟,一个小时后要做其他事情;
  • 用户接到了 推销的骚扰电话 or 老板/女朋友的连环夺命call;
  • ...

我们希望我们的「扣扣」交互更自然,符合用户预期,给用户带来良好的音频体验,所以不同场景下的交互自然有所不同:

  • 打开「网抑云音乐」播放音乐或者打开「爱优腾」播放视频后,我们希望我们的「扣扣」音乐能够停止播放;
  • 使用「高德导航」时,导航的语音提示播放时,我们希望我们的「扣扣」音乐的音量稍微降低,保证用户听到的导航提示音是清晰的;提示音播放完成后则恢复正常音量;
  • 闹钟响起后,「扣扣」音乐能够暂停播放,待到闹钟结束后,「扣扣」音乐能够正常恢复播放;
  • 通话接通时,「扣扣」音乐暂停播放,通话挂断后,「扣扣」音乐能够正常恢复播放;

音视频焦点

主流的操作系统都有一套机制来最大程度的满足音视频播放不同场景下的交互需求,这套机制就是「音视频焦点」。操作系统借助「音视频焦点」来控制应用程序之间的音视频交互,Android如此,鸿蒙亦是如此。

那什么是音视频焦点

当应用播放或录制声音的时候,难免会发生与其他音频流的并发或打断现象,这对用户的体验影响很大。例如应用开始播放视频时,如果后台正在播放音乐,用户会希望音乐可以自动暂停,让视频优先播放,这就是音频焦点的作用。对于需要使用音频业务的应用,做好音频焦点管理是一项非常重要的工作,可以为用户提供良好的音频体验。

在应用启动播放或录制之前,需要首先申请音频焦点;在播放或录制完成后,需要释放音频焦点。在播放或录制的过程中,可能会因为其他音频流打断而失去焦点,此时需要应用根据焦点变化做出相应的处理

「鸿蒙」音视频开发:遥遥领先?

讲清楚音视频焦点的意义后,我们再来看鸿蒙系统中的音视频开发这次我们仍然以「扣扣」音乐为例,讲一下使用鸿蒙系统中avPlayer播放器开发音视频应用需要注意的地方。

鸿蒙中的音视频应用开发大致需要注意以下2点:

  1. 应用在启动播放或录制之前,需要根据音频的用途,使用合适的音频流类型,即正确指定StreamUsageSourceType
  1. 合理处理焦点变化及焦点事件(InterruptEvent),根据需要合理设置焦点策略;

使用合适的音频流类型

对于不同的音频流类型,我们往往希望其对焦点丢失、打断做出不同的交互,鸿蒙中的音频流类型对于定义音频数据的播放和录制方式的十分重要,对于播放流,其类型由StreamUsage决定;对于录制流,其类型由SourceType决定。音频流类型在音量控制、音频焦点管理、输入/输出设备选择等方面有决定性影响。

常用播放流类型

  • STREAM_USAGE_MUSIC:适用于音乐场景,也可用于其他默认场景。
  • STREAM_USAGE_MOVIE:适用于短视频、电影、电视剧等视频类场景。
  • STREAM_USAGE_AUDIOBOOK: 适用于有声读物、听新闻、播客等场景。
  • STREAM_USAGE_GAME:适用于游戏场景。
  • STREAM_USAGE_NAVIGATION:适用于导航场景。
  • STREAM_USAGE_VOICE_MESSAGE:适用于语音短消息场景。
  • STREAM_USAGE_VOICE_COMMUNICATION:适用于VoIP语音通话场景。
  • STREAM_USAGE_ALARM:适用于闹钟场景。
  • STREAM_USAGE_RINGTONE:适用于VoIP来电响铃等场景。
  • STREAM_USAGE_NOTIFICATION:适用于通知音、提示音等场景。

常用录制流类型

  • SOURCE_TYPE_MIC:适用于普通录音场景。
  • SOURCE_TYPE_VOICE_COMMUNICATION:适用于VoIP语音通话场景。
  • SOURCE_TYPE_VOICE_MESSAGE:适用于录制语音短消息的场景。

音频流类型决定音量类型

播放流类型决定了该音频流类型属于何种音量类型,不同音量类型拥有独立的音量值,互不干扰,鸿蒙中的音量类型分为4类:

  • 来电STREAM_USAGE_RINGTONE、信息STREAM_USAGE_VOICE_MESSAGE、通知STREAM_USAGE_NOTIFICATION等类型使用铃声音量
  • 闹钟STREAM_USAGE_RINGTONE类型使用闹钟音量
  • 通话STREAM_USAGE_VOICE_COMMUNICATION类型使用通话音量
  • 音乐STREAM_USAGE_MUSIC、视频STREAM_USAGE_MOVIE、游戏STREAM_USAGE_GAME等类型使用媒体音量。

不同音频流类型对于音频焦点有不同的处理方式

音频流类型在音频焦点管理中起着重要作用。不同类型的音频流(如音乐、通话、闹钟等)有不同的默认优先级和处理方式。当应用启动音频播放或者录制时,系统默认根据音频流类型申请焦点,可能打断其他音频或者降低其他音频的音量。

例如我们上文中的例子:开始播放导航时,会降低正在播放的音乐音量,导航停止播放时,音乐音量会恢复。

如何设置音频流类型

根据我们播放器实现方式的不同,设置音频流类型的方法也不尽相同,我们这里以avPlayer为例:

// 1.创建avPlayer
const avPlayer: media.AVPlayer = await media.createAVPlayer();
// 2.监听avPlayer状态变化
avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
  if(state == 'initialized') {
    // 3.通过audioRendererInfo设置音频流类型
    // 设置AVPlayer的audioRendererInfo属性时,只允许在initialized状态下设置
    // 若应用没有主动设置该属性,AVPlayer会根据媒体源是否包含视频,使用Music或Movie作为usage的默认值。
    avPlayer.audioRendererInfo = {
      usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
      rendererFlags: 0
    }
  }
})

应用需要根据自身的业务场景和实际需求,为音频选择合适的流类型,这样可以确保其音频行为符合预期,为用户提供良好的音频体验。

处理焦点

说到处理焦点时,主要包含三方面

  • 申请/释放 焦点
  • 设置音频焦点策略和焦点模式
  • 处理音频焦点变化

当应用开始播放或录制音频时,鸿蒙系统会自动为响应的音频流申请音频焦点,同样的,当应用结束播放或录制音频时,鸿蒙系统会自动为相应的音频流释放音频焦点。所以我们无需过于关注焦点的申请和释放逻辑,交由系统处理,集中精力处理焦点策略、模式和焦点变化逻辑即可。我们首先来看下音频焦点策略

若应用以静音状态开始播放音频(或视频),并且希望静音阶段不影响其他音频,当后续解除静音的时候,再以正常策略申请音频焦点,则可以调用静音并发播放模式的相关接口。

若应用不希望在音频流停止时立刻释放音频焦点,可以使用音频会话(AudioSession)的相关接口,达到延迟释放音频焦点的效果。

音频焦点策略(AudioSessionStrategy)

当一个音频流申请或释放音频焦点时,系统会根据音频焦点策略,对所有音频流(包含播放和录制)进行焦点管理,决策哪些音频流可以正常运行,哪些音频流需要被打断或执行其他操作。

对于多个音频流并发的场景,系统预设了默认的音频焦点策略,会对所有音频流(包含播放和录制)进行统一的焦点管理。

应用可以使用音频会话管理(AudioSessionManager)的相关接口,通过AudioSession主动管理本应用音频流的焦点,定制本应用音频流的焦点策略,修改本应用音频流释放音频焦点的时机,以满足应用特定的使用场景。

// 1.创建AudioSessionManager实例
const audioSessionManager: audio.AudioSessionManager = audioManager.getSessionManager();
// 2.创建焦点策略
const strategy: audio.AudioSessionStrategy = {
  // 创建焦点策略时,需要指定音频的并发模式
  concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_PAUSE_OTHERS
};
// 3.通过activateAudioSession接口激活当前应用的音频会话
audioSessionManager.activateAudioSession(strategy).then(() => {
  console.info('activateAudioSession SUCCESS');
}).catch((err: BusinessError) => {
  console.error(`ERROR: ${err}`);
})

这里面最关键的点就是音频的并发模式,由系统预设,包含以下四种:

  • 默认模式(CONCURRENCY_DEFAULT):即系统默认的音频焦点策略。
  • 并发模式(CONCURRENCY_MIX_WITH_OTHERS):和其它音频流并发。
  • 降低音量模式(CONCURRENCY_DUCK_OTHERS):和其他音频流并发,并且降低其他音频流的音量。
  • 暂停模式(CONCURRENCY_PAUSE_OTHERS):暂停其他音频流,待释放焦点后通知其他音频流恢复。

系统默认的焦点策略和音频并发模式还是挺合理的:

  • 开始播放视频时,会让正在播放的音乐停止,后续视频停止时,音乐也不会收到恢复通知。
  • 开始播放导航语音时,会让正在播放的音乐降低音量,后续导航语音停止时,音乐会恢复音量。
  • 音乐和游戏声音可以并发混音播放,互不影响。
  • 接通电话时,会让正在播放的音乐暂停,后续通话结束时,音乐会收到恢复通知。

AudioSession使用以上各模式时,系统会最大程度满足其焦点策略,但并不一定能在所有场景都能满足。

还是以「扣扣」音乐举例说明:设置并发模式为CONCURRENCY_PAUSE_OTHERS,当我们的「扣扣」音乐播放音乐时,会暂停播放其他App的音乐播放。但是如果系统这个时候正在通话,即VoiceCommunication流正在播放,则VoiceCommunication流不会被暂停。

焦点模式

针对同一应用创建的多个音频流,应用可通过设置焦点模式(InterruptMode),选择由应用自主管控,或由系统统一管控。系统预设了2种焦点模式:

  • 共享焦点模式(SHARE_MODE):由同一应用创建的多个音频流,共享一个音频焦点。这些音频流之间的并发规则由应用自主决定,音频焦点策略不会介入。当其他应用创建的音频流与该应用的音频流并发播放时,才会触发音频焦点策略的管控。
  • 独立焦点模式(INDEPENDENT_MODE):应用创建的每一个音频流均会独立拥有一个音频焦点,当多个音频流并发播放时,会触发音频焦点策略的管控。

应用可以按需选择合适的焦点模式,在创建音频流时,系统默认采用共享焦点模式(SHARE_MODE),应用可主动设置所需的模式。使用avPlayer可以通过修改其audioInterruptMode属性进行设置。

处理音频焦点变化

在应用播放或录制音频的过程中,若有其他音频流申请焦点,系统会根据音频焦点策略进行焦点处理。若判定本音频流的焦点有变化,需要执行暂停、继续、降低音量、恢复音量等操作,则系统会自动执行一些必要的操作,并通过音频焦点事件(InterruptEvent)通知应用。

因此,为了维持应用和系统的状态一致性,保证良好的用户体验,推荐应用监听音频焦点事件,并在焦点发生变化时,根据InterruptEvent做出必要的响应。

使用avPlayer时,通过调用on('audioInterrupt')接口,监听音频焦点事件InterruptEvent:

avPlayer.on('audioInterrupt', async(interruptEvent: audio.InterruptEvent) => {
    // 处理焦点变化逻辑
})

InturruptEvent中包含两个重要信息需要我们重点关注:打断类型(InterruptForceType)打断提示(InterruptHint),我们需要根据这两个信息做出相应处理,保持应用于系统状态一致,带给用户良好体验。

打断类型 InterruptForceType

InterruptForceType参数提示应用该焦点变化是否已由系统强制操作:

  • 强制打断类型(INTERRUPT_FORCE):由系统进行操作,强制执行。应用仅需要做一些必要的处理,例如更新状态、更新界面显示等。
  • 共享打断类型(INTERRUPT_SHARE):由应用进行操作,应用可以选择响应或忽略,系统不会干涉。

系统默认优先采用强制打断类型(INTERRUPT_FORCE),应用无法更改。需要注意的是:对于一些系统无法强制执行的操作(例如INTERRUPT_HINT_RESUME),会采用共享打断类型(INTERRUPT_SHARE)。

打断提示 InterruptHint

InterruptHint参数提示应用需要对音频流进行何种操作:

  • 继续(INTERRUPT_HINT_RESUME):音频可以继续播放或录制,仅在PAUSE之后收到。注意,此操作无法由系统强制执行,其对应的InterruptForceType一定为INTERRUPT_SHARE类型。
  • 暂停(INTERRUPT_HINT_PAUSE):音频暂停,暂时失去音频焦点。后续待焦点可用时,会再收到INTERRUPT_HINT_RESUME。
  • 停止(INTERRUPT_HINT_STOP):音频停止,彻底失去音频焦点。
  • 降低音量(INTERRUPT_HINT_DUCK):音频降低音量播放,而不会停止。默认降低至正常音量的20%。
  • 恢复音量(INTERRUPT_HINT_UNDUCK):音频恢复正常音量。
avPlayer.on('audioInterrupt', async(interruptEvent: audio.InterruptEvent) => {
    // 处理焦点变化逻辑
    // 在发生音频焦点变化时,avPlayer收到interruptEvent回调,此处根据其内容做相应处理
    // 1. 可选:读取interruptEvent.forceType的类型,判断系统是否已强制执行相应操作。
    // 注:默认焦点策略下,INTERRUPT_HINT_RESUME为INTERRUPT_SHARE类型,其余hintType均为INTERRUPT_FORCE类型。因此对forceType可不做判断。
    // 2. 必选:读取interruptEvent.hintType的类型,做出相应的处理。
    if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_FORCE) {
      // 强制打断类型(INTERRUPT_FORCE):音频相关处理已由系统执行,应用需更新自身状态,做相应调整
       switch (interruptEvent.hintType) {
        case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
          // 系统已将音频流暂停(临时失去焦点),为保持状态一致,应用需切换至音频暂停状态
          // 临时失去焦点:待其他音频流释放音频焦点后,本音频流会收到resume对应的音频焦点事件,到时可自行继续播放
          isPlay = false; // 此句为简化处理,代表应用切换至音频暂停状态的若干操作
          break;
        case audio.InterruptHint.INTERRUPT_HINT_STOP:
          // 系统已将音频流停止(永久失去焦点),为保持状态一致,应用需切换至音频暂停状态
          // 永久失去焦点:后续不会再收到任何音频焦点事件,若想恢复播放,需要用户主动触发。
          isPlay = false; // 此句为简化处理,代表应用切换至音频暂停状态的若干操作
          break;
        case audio.InterruptHint.INTERRUPT_HINT_DUCK:
          // 系统已将音频音量降低(默认降到正常音量的20%)
          isDucked = true; // 此句为简化处理,代表应用切换至降低音量播放状态的若干操作
          break;
        case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
          // 系统已将音频音量恢复正常
          isDucked = false; // 此句为简化处理,代表应用切换至正常音量播放状态的若干操作
          break;
        default:
          break;
      }
    } else if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_SHARE) {
      // 共享打断类型(INTERRUPT_SHARE):应用可自主选择执行相关操作或忽略音频焦点事件
      switch (interruptEvent.hintType) {
        case audio.InterruptHint.INTERRUPT_HINT_RESUME:
          // 临时失去焦点后被暂停的音频流此时可以继续播放,建议应用继续播放,切换至音频播放状态
          // 若应用此时不想继续播放,可以忽略此音频焦点事件,不进行处理即可
          // 继续播放,此处主动执行start(),以标识符变量started记录start()的执行结果
          await audioRenderer.start().then(() => {
            started = true; // start()执行成功
          }).catch((err: BusinessError) => {
            started = false; // start()执行失败
          });
          // 若start()执行成功,则切换至音频播放状态
          if (started) {
            isPlay = true; // 此句为简化处理,代表应用切换至音频播放状态的若干操作
          } else {
            // 音频继续播放的操作执行失败
          }
          break;
        default:
          break;
      }
   }
})