HarmonyOS AudioRenderer 中断恢复实战:焦点竞争、路由变化与状态机收口
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
更多推荐




所有评论(0)