HarmonyOS AudioRenderer 中断恢复实战:焦点竞争、路由变化与状态机收口

音频播放最容易被忽略的是中断之后怎么回来。来电、语音播报、蓝牙耳机断开、系统焦点变化都会影响 AudioRenderer,如果只在按钮里写 start 和 pause,播放状态很快会和真实设备状态脱节。本文把焦点中断、输出设备变化和播放器状态机放在一起写。

请添加图片描述

本文先把播放恢复场景讲清

这一节关注播放过程中的真实打断:来电、系统提示音、耳机拔出、蓝牙切换都可能改变 AudioRenderer 的运行条件。文章的目标不是写一个最小播放器,而是让播放状态在这些变化之后还能解释得清、恢复得准、释放得干净。

  • AudioRenderer 创建、监听、播放、暂停、释放由同一个类收口。
  • 中断恢复同时判断系统 hint 和用户主动暂停意图。
  • 输出设备变化只处理当前音频流,避免全局误判。
  • 资源释放前解绑监听,减少切页后的回调访问。

这几件事连在一起看,读者就能从配置、API 调用、状态维护和排查方法四个角度复用本文方案,而不是只复制某一段示例代码。

AudioRenderer 资料与声明入口

项目 内容
官方能力 AudioRenderer 用于播放流式音频数据,支持中断事件和输出设备变化监听。
本地声明 D:/harmonyos/SDK/23/ets/api/@ohos.multimedia.audio.d.ts
关键事件 audioInterrupt、outputDeviceChangeWithInfo、writeData。
场景边界 音乐、语音播报、音频书等需要不同 StreamUsage。

音频渲染的版本与设备边界

项目 内容
SDK HarmonyOS SDK 23,已核对 createAudioRenderer、start、pause、release。
示例格式 48kHz、双声道、S16LE PCM。
恢复策略 系统强制暂停不自动抢回,用户主动暂停不自动恢复。
不覆盖 本文不讲音频解码器,只处理渲染器状态和中断恢复。

请添加图片描述

请添加图片描述

先定义播放器自己的状态

AudioRenderer 的内部状态不等于业务状态。业务要知道用户是否主动暂停、中断是否来自系统、路由是否变化。

export enum PlayerRunState {
  Idle = 'Idle',
  Preparing = 'Preparing',
  Playing = 'Playing',
  PausedByUser = 'PausedByUser',
  PausedByInterrupt = 'PausedByInterrupt',
  Released = 'Released'
}

状态枚举描述业务意图。后续中断恢复时先看这个状态,避免用户暂停后被系统事件重新拉起播放。

创建 AudioRenderer 时明确 usage

不同 usage 的焦点策略不同。音乐播放、语音通信和提示音不要混用同一套配置。

import audio from '@ohos.multimedia.audio';

export function buildMusicRendererOptions(): audio.AudioRendererOptions {
  return {
    streamInfo: {
      samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
      channels: audio.AudioChannel.CHANNEL_2,
      sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
      encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
    },
    rendererInfo: {
      usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
      rendererFlags: 0
    }
  };
}

配置层只描述音频流特征和用途。业务按钮、播放列表和缓存逻辑不应该修改这些底层参数。

把监听注册放在创建之后

渲染器创建成功后立即注册中断和设备变化监听,避免刚开始播放就遇到焦点事件却没有处理器。

export class FocusAwareRenderer {
  private renderer?: audio.AudioRenderer;
  private state: PlayerRunState = PlayerRunState.Idle;
  private userPaused = false;

  async prepare(): Promise<void> {
    this.state = PlayerRunState.Preparing;
    this.renderer = await audio.createAudioRenderer(buildMusicRendererOptions());
    this.renderer.on('audioInterrupt', event => this.onInterrupt(event));
    this.renderer.on('outputDeviceChangeWithInfo', info => this.onRouteChanged(info));
    this.state = PlayerRunState.Idle;
  }
}

类拥有 renderer 和监听器生命周期。页面只调用 prepare、play、pause、release,不直接注册事件。

中断处理不要一律恢复

系统给出 RESUME 只是允许恢复,不代表一定要恢复。用户主动暂停、页面已释放、播放列表切换都应该阻止自动恢复。

private async onInterrupt(event: audio.InterruptEvent): Promise<void> {
  if (event.hintType === audio.InterruptHint.INTERRUPT_HINT_PAUSE ||
      event.hintType === audio.InterruptHint.INTERRUPT_HINT_STOP) {
    if (this.state === PlayerRunState.Playing) {
      this.state = PlayerRunState.PausedByInterrupt;
    }
    return;
  }
  if (event.hintType === audio.InterruptHint.INTERRUPT_HINT_RESUME &&
      this.state === PlayerRunState.PausedByInterrupt && !this.userPaused) {
    await this.renderer?.start();
    this.state = PlayerRunState.Playing;
  }
}

中断回调只处理焦点变化,不改播放源。恢复前同时判断状态和 userPaused,避免强行播放。

DUCK 和 UNDUCK 要保存原始音量

短时提示音常见处理是降低音量而不是暂停。降低前保存业务音量,恢复时回到原值。

private normalVolume = 1.0;
private ducked = false;

private async applyDuck(event: audio.InterruptEvent): Promise<void> {
  if (event.hintType === audio.InterruptHint.INTERRUPT_HINT_DUCK && !this.ducked) {
    this.ducked = true;
    await this.renderer?.setVolume(0.25);
  }
  if (event.hintType === audio.InterruptHint.INTERRUPT_HINT_UNDUCK && this.ducked) {
    this.ducked = false;
    await this.renderer?.setVolume(this.normalVolume);
  }
}

音量调整和播放状态分开处理。这样提示音结束后不会出现音量长期偏小的问题。

输出设备变化要从当前流判断

蓝牙、扬声器、耳机切换不应只看全局设备列表。使用当前 AudioRenderer 的 outputDeviceChangeWithInfo,才能知道这一路播放是否受影响。

private onRouteChanged(info: audio.AudioStreamDeviceChangeInfo): void {
  console.info(`[AudioRoute] reason=${info.changeReason} devices=${info.devices.length}`);
  if (this.state === PlayerRunState.Playing && info.devices.length === 0) {
    this.state = PlayerRunState.PausedByInterrupt;
  }
}

路由回调只记录当前流的设备变化。页面可以据此展示“耳机已断开”,而不是误判其他音频流。

播放和暂停入口要保留用户意图

用户点击暂停与系统中断暂停不同。入口方法必须写入 userPaused,否则恢复回调无法判断是否自动继续。

async play(): Promise<void> {
  this.userPaused = false;
  await this.renderer?.start();
  this.state = PlayerRunState.Playing;
}

async pauseByUser(): Promise<void> {
  this.userPaused = true;
  await this.renderer?.pause();
  this.state = PlayerRunState.PausedByUser;
}

按钮入口记录用户意图。中断回调只在非用户暂停时恢复,交互会更符合预期。

释放时先解绑再 release

页面销毁或切换播放源时,先解绑回调再 release,可以减少释放后回调访问空对象的概率。

async release(): Promise<void> {
  if (!this.renderer) {
    return;
  }
  this.renderer.off('audioInterrupt');
  this.renderer.off('outputDeviceChangeWithInfo');
  await this.renderer.release();
  this.renderer = undefined;
  this.state = PlayerRunState.Released;
}

release 是资源边界。它保证渲染器和监听器一起结束,页面重新进入时从 prepare 开始。

AudioRenderer 排查表:从中断异常倒查

现象 优先查看 处理方式
来电后没有恢复 是否处于 PausedByInterrupt 且 userPaused=false 恢复前同时判断状态和用户意图。
耳机断开仍外放 是否处理当前流的设备变化 监听 outputDeviceChangeWithInfo 并按产品策略暂停。
提示音后音量变小 DUCK 后没有处理 UNDUCK 保存 normalVolume 并恢复。
切页后崩溃 release 后回调还在访问 renderer 释放前 off 对应监听。

播放链路释放前的核对清单

这份清单建议在提交代码、写入团队文档或交给测试同学前逐项过一遍。它不是形式化备注,而是把本文的配置边界、运行时行为、异常兜底和可观测信息压成可以执行的确认项。

  • StreamUsage 与产品场景一致。
  • 中断恢复区分用户暂停和系统暂停。
  • DUCK/UNDUCK 有音量恢复逻辑。
  • 当前流输出设备变化有处理策略。
  • 页面销毁时解绑监听并释放 renderer。

如果其中任意一项还没有办法给出明确证据,优先回到对应实现小节补日志、补校验或补生命周期处理,再进入下一轮联调。

音频恢复小结

AudioRenderer 的稳定性来自状态机,而不是 start 和 pause 两个按钮。把焦点、路由、用户意图和释放顺序写清楚,音频体验才不会在真实设备切换中失控。

AudioRenderer 参考资料

下面列出的资料用于核对 API 名称、能力范围和版本边界。实际落地时还需要结合项目使用的 SDK 版本、设备 API 级别以及团队已有封装做一次复核。

  • 音频播放能力说明:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/audio-playback
  • 音频输出设备变化 FAQ:https://developer.huawei.com/consumer/cn/doc/harmonyos-center-guides/faqs-audio-17-0000002592944368
  • HarmonyOS SDK 23 本地 API 声明:@ohos.multimedia.audio.d.ts
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐