HarmonyOS开发中的音频焦点:多应用音频协调、焦点抢占与监听全解
HarmonyOS开发中的音频焦点:多应用音频协调、焦点抢占与监听全解
📌 核心要点:掌握 HarmonyOS 音频焦点机制——从 AudioInterrupt 焦点请求与释放,到焦点抢占策略与多应用音频协调,实现多应用共存下的优雅音频体验
一、背景与动机
你一定遇到过这样的场景:正在听音乐,突然来了个电话,音乐自动暂停,接完电话后音乐又自动恢复。或者正在看视频,打开另一个视频 App,前一个视频的声音自动变小或暂停。这就是音频焦点在起作用。
音频焦点,说白了就是"谁有资格发声"的协调机制。手机的音频输出通道是共享的,多个应用可能同时想播放声音,但物理上只能有一个声音(或有限的混合)输出。如果没有协调机制,多个应用同时播放,用户体验就是灾难。
为什么音频焦点如此重要?
- 用户体验底线:通话时音乐不停,这是不可接受的
- 系统级协调:操作系统需要一套统一的规则来仲裁音频冲突
- 应用合规性:不正确处理音频焦点的应用,可能被系统强制静音甚至杀进程
- 多应用共存:导航提示音和音乐如何共存?语音助手和播客如何协调?
- HarmonyOS 生态:分布式场景下,焦点协调更加复杂
二、核心原理
2.1 音频焦点机制总览
音频焦点是一种请求-授权-监听的机制:
- 请求焦点:应用在播放音频前,向系统声明自己的音频类型和使用场景
- 系统仲裁:系统根据音频类型优先级,决定是否授予焦点,以及是否打断其他应用
- 焦点监听:应用监听焦点变化,当被抢占时主动降低音量或暂停

flowchart TB
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
A[应用A: 音乐播放器] -->|请求焦点| S[系统焦点管理器]
B[应用B: 语音通话] -->|请求焦点| S
C[应用C: 导航提示] -->|请求焦点| S
S -->|授予焦点| A
S -->|抢占焦点| A
S -->|授予焦点| B
S -->|共享焦点| C
S --> D{焦点仲裁规则}
D --> D1[通话 > 一切]
D --> D2[闹钟 > 音乐]
D --> D3[导航可与音乐共享]
D --> D4[同类型互斥]
A -->|监听中断| E[中断回调]
E --> E1[INTERRUPT_HINT_PAUSE 暂停]
E --> E2[INTERRUPT_HINT_DUCK 降低音量]
E --> E3[INTERRUPT_HINT_RESUME 恢复]
class A primary
class B error
class C warning
class S info
class D purple
class E warning
2.2 音频流类型与焦点优先级
不同的音频流类型有不同的焦点优先级,系统据此进行仲裁:
| 优先级 | 流类型(StreamUsage) | 说明 | 典型场景 |
|---|---|---|---|
| 1(最高) | VOICE_COMMUNICATION | 语音通话 | 电话、VoIP |
| 2 | VOICE_ASSISTANT | 语音助手 | 小艺语音 |
| 3 | RINGTONE / ALARM | 铃声/闹钟 | 来电铃声、闹钟 |
| 4 | NOTIFICATION | 通知 | 消息提示音 |
| 5 | NAVIGATION | 导航 | 地图语音导航 |
| 6 | MEDIA | 媒体 | 音乐、视频、游戏 |
| 7(最低) | SYSTEM | 系统 | 系统音效 |
2.3 焦点中断类型与提示
当焦点被抢占时,系统会发送中断事件,包含中断类型和提示:
中断类型(InterruptType):
| 类型 | 说明 |
|---|---|
| INTERRUPT_TYPE_BEGIN | 中断开始(被抢占) |
| INTERRUPT_TYPE_END | 中断结束(恢复) |
中断提示(InterruptHint):
| 提示 | 说明 | 应用应该做的 |
|---|---|---|
| INTERRUPT_HINT_NONE | 无特定提示 | — |
| INTERRUPT_HINT_PAUSE | 暂停播放 | 立即暂停 |
| INTERRUPT_HINT_STOP | 停止播放 | 停止并释放资源 |
| INTERRUPT_HINT_DUCK | 降低音量(“鸭化”) | 降低到约 20% 音量 |
| INTERRUPT_HINT_UNDUCK | 恢复音量 | 恢复原始音量 |
| INTERRUPT_HINT_RESUME | 恢复播放 | 恢复播放 |
2.4 焦点模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
| SHARE_MODE | 共享模式,允许与其他应用混合播放 | 导航提示 + 音乐 |
| INDEPENDENT_MODE | 独占模式,抢占其他应用焦点 | 语音通话、闹钟 |
flowchart LR
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
A[音乐App SHARE_MODE] -->|共存| B[导航App SHARE_MODE]
A -->|被暂停| C[通话App INDEPENDENT_MODE]
B -->|被暂停| C
D[音乐App INDEPENDENT_MODE] -->|互斥| E[视频App INDEPENDENT_MODE]
class A primary
class B warning
class C error
class D info
class E purple
三、代码实战
3.1 音频焦点请求与监听
实现一个完整的音频焦点管理器,请求焦点、监听中断、正确响应:
import { audio } from '@kit.AudioKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct AudioFocusManager {
// 音频渲染器
private audioRenderer: audio.AudioRenderer | null = null;
// 原始音量(用于 duck/unduck 恢复)
private originalVolume: number = 1.0;
// 是否被焦点中断暂停
@State isInterruptedPaused: boolean = false;
// 状态
@State focusStatus: string = '未请求焦点';
@State isPlaying: boolean = false;
@State currentVolume: number = 1.0;
@State interruptLogs: string[] = [];
aboutToAppear(): void {
this.initAudioRenderer();
}
aboutToDisappear(): void {
this.releaseRenderer();
}
/**
* 初始化音频渲染器并注册焦点监听
*/
async initAudioRenderer(): Promise<void> {
try {
const rendererOptions: audio.AudioRendererOptions = {
streamInfo: {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
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
}
};
this.audioRenderer = await audio.createAudioRenderer(rendererOptions);
// ★ 核心:注册音频中断监听
this.audioRenderer.on('audioInterrupt', (interruptEvent: audio.InterruptEvent) => {
this.handleAudioInterrupt(interruptEvent);
});
this.focusStatus = '渲染器已创建,焦点监听已注册';
this.addLog('✅ 音频渲染器初始化完成');
} catch (err) {
const error = err as BusinessError;
console.error(`[焦点] 初始化失败: ${error.message}`);
this.focusStatus = '初始化失败';
}
}
/**
* ★★★ 核心:处理音频中断事件 ★★★
*/
handleAudioInterrupt(interruptEvent: audio.InterruptEvent): void {
// interruptEvent 包含:
// eventType: INTERRUPT_TYPE_BEGIN(中断开始) / INTERRUPT_TYPE_END(中断结束)
// hintType: 具体的中断提示
const eventType = interruptEvent.eventType;
const hintType = interruptEvent.hintType;
this.addLog(`📢 中断事件: type=${eventType === audio.InterruptType.INTERRUPT_TYPE_BEGIN ? '开始' : '结束'}, hint=${this.getHintName(hintType)}`);
if (eventType === audio.InterruptType.INTERRUPT_TYPE_BEGIN) {
// 中断开始 —— 我们被打断了
switch (hintType) {
case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
// 系统要求我们暂停(如来了电话)
this.pauseByInterrupt();
this.focusStatus = '🔴 被暂停(高优先级音频抢占)';
break;
case audio.InterruptHint.INTERRUPT_HINT_STOP:
// 系统要求我们停止
this.stopByInterrupt();
this.focusStatus = '🔴 被停止';
break;
case audio.InterruptHint.INTERRUPT_HINT_DUCK:
// 系统要求我们降低音量(如导航提示音出现)
this.duckVolume();
this.focusStatus = '🟡 音量已降低(与其他音频共存)';
break;
default:
break;
}
} else if (eventType === audio.InterruptType.INTERRUPT_TYPE_END) {
// 中断结束 —— 可以恢复了
switch (hintType) {
case audio.InterruptHint.INTERRUPT_HINT_RESUME:
// 系统允许我们恢复播放
this.resumeFromInterrupt();
this.focusStatus = '🟢 已恢复播放';
break;
case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
// 系统允许我们恢复音量
this.unduckVolume();
this.focusStatus = '🟢 音量已恢复';
break;
default:
break;
}
}
}
/**
* 被中断时暂停播放
*/
pauseByInterrupt(): void {
if (!this.audioRenderer) return;
try {
// 保存当前音量(用于恢复)
this.originalVolume = this.currentVolume;
this.audioRenderer.pause();
this.isPlaying = false;
this.isInterruptedPaused = true;
this.addLog('⏸ 因焦点中断而暂停');
} catch (err) {
console.error('[焦点] 暂停失败');
}
}
/**
* 被中断时停止播放
*/
stopByInterrupt(): void {
if (!this.audioRenderer) return;
try {
this.audioRenderer.stop();
this.isPlaying = false;
this.isInterruptedPaused = false;
this.addLog('⏹ 因焦点中断而停止');
} catch (err) {
console.error('[焦点] 停止失败');
}
}
/**
* 降低音量(Duck)
*/
duckVolume(): void {
if (!this.audioRenderer) return;
try {
// 保存原始音量
this.originalVolume = this.currentVolume;
// 降低到 20%
const duckedVolume = this.originalVolume * 0.2;
this.audioRenderer.setVolume(duckedVolume);
this.currentVolume = duckedVolume;
this.addLog(`🔇 音量降低: ${this.originalVolume.toFixed(2)} → ${duckedVolume.toFixed(2)}`);
} catch (err) {
console.error('[焦点] 降低音量失败');
}
}
/**
* 恢复音量(Unduck)
*/
unduckVolume(): void {
if (!this.audioRenderer) return;
try {
this.audioRenderer.setVolume(this.originalVolume);
this.currentVolume = this.originalVolume;
this.addLog(`🔊 音量恢复: ${this.originalVolume.toFixed(2)}`);
} catch (err) {
console.error('[焦点] 恢复音量失败');
}
}
/**
* 从中断中恢复播放
*/
resumeFromInterrupt(): void {
if (!this.audioRenderer || !this.isInterruptedPaused) return;
try {
this.audioRenderer.start();
this.isPlaying = true;
this.isInterruptedPaused = false;
this.addLog('▶️ 从焦点中断中恢复播放');
} catch (err) {
console.error('[焦点] 恢复播放失败');
}
}
/**
* 手动开始播放
*/
async startPlaying(): Promise<void> {
if (!this.audioRenderer) return;
try {
await this.audioRenderer.start();
this.isPlaying = true;
this.focusStatus = '🟢 播放中(已持有焦点)';
this.addLog('▶️ 开始播放');
} catch (err) {
const error = err as BusinessError;
this.addLog(`❌ 播放失败: ${error.message}`);
}
}
/**
* 手动暂停播放
*/
async pausePlaying(): Promise<void> {
if (!this.audioRenderer) return;
try {
await this.audioRenderer.pause();
this.isPlaying = false;
this.focusStatus = '🟡 已暂停';
this.addLog('⏸ 手动暂停');
} catch (err) {
console.error('[焦点] 暂停失败');
}
}
/**
* 获取中断提示名称
*/
getHintName(hint: audio.InterruptHint): string {
const names: Record<number, string> = {
[audio.InterruptHint.INTERRUPT_HINT_NONE]: 'NONE',
[audio.InterruptHint.INTERRUPT_HINT_PAUSE]: 'PAUSE(暂停)',
[audio.InterruptHint.INTERRUPT_HINT_STOP]: 'STOP(停止)',
[audio.InterruptHint.INTERRUPT_HINT_DUCK]: 'DUCK(降低音量)',
[audio.InterruptHint.INTERRUPT_HINT_UNDUCK]: 'UNDUCK(恢复音量)',
[audio.InterruptHint.INTERRUPT_HINT_RESUME]: 'RESUME(恢复播放)',
};
return names[hint] || `UNKNOWN(${hint})`;
}
/**
* 添加日志
*/
addLog(message: string): void {
const time = new Date().toLocaleTimeString();
this.interruptLogs.unshift(`[${time}] ${message}`);
if (this.interruptLogs.length > 30) {
this.interruptLogs = this.interruptLogs.slice(0, 30);
}
}
/**
* 释放渲染器
*/
async releaseRenderer(): Promise<void> {
try {
if (this.audioRenderer) {
this.audioRenderer.off('audioInterrupt');
await this.audioRenderer.release();
this.audioRenderer = null;
}
} catch (err) {
console.error('[焦点] 释放失败');
}
}
build() {
Column({ space: 16 }) {
// 标题
Text('🎯 音频焦点管理器')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 焦点状态
Text(this.focusStatus)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.padding(12)
.borderRadius(8)
.backgroundColor('#252540')
.width('90%')
.textAlign(TextAlign.Center)
// 当前音量
Row({ space: 12 }) {
Text('音量:')
.fontSize(14)
Progress({
value: this.currentVolume * 100,
total: 100,
type: ProgressType.Linear
})
.width('60%')
.color('#4CAF50')
Text(`${Math.round(this.currentVolume * 100)}%`)
.fontSize(14)
.fontColor('#4CAF50')
}
.width('90%')
// 播放控制
Row({ space: 16 }) {
Button(this.isPlaying ? '⏸ 暂停' : '▶️ 播放')
.width(120)
.backgroundColor(this.isPlaying ? '#FF9800' : '#4CAF50')
.onClick(() => {
if (this.isPlaying) {
this.pausePlaying();
} else {
this.startPlaying();
}
})
}
// 中断日志
Column({ space: 8 }) {
Text('📋 焦点中断日志')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
List({ space: 4 }) {
ForEach(this.interruptLogs, (log: string, index: number) => {
ListItem() {
Text(log)
.fontSize(11)
.fontColor(
log.includes('暂停') || log.includes('停止') ? '#F44336' :
log.includes('降低') ? '#FF9800' :
log.includes('恢复') ? '#4CAF50' : '#aaa'
)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
}, (log: string, index: number) => `${index}`)
}
.width('100%')
.height(250)
}
.width('90%')
.padding(16)
.borderRadius(12)
.backgroundColor('#252540')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(20)
.backgroundColor('#1a1a2e')
}
}
3.2 不同音频流类型的焦点策略
不同场景需要使用不同的音频流类型,这直接影响焦点优先级。下面展示如何为不同场景选择正确的流类型:
import { audio } from '@kit.AudioKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 场景配置
interface AudioSceneConfig {
name: string;
streamUsage: audio.StreamUsage;
contentType: audio.ContentType;
interruptMode: audio.InterruptMode;
description: string;
icon: string;
}
@Entry
@Component
struct AudioSceneSelector {
// 当前渲染器
private currentRenderer: audio.AudioRenderer | null = null;
// 原始音量
private originalVolume: number = 1.0;
@State isInterruptedPaused: boolean = false;
// 状态
@State currentScene: string = '未选择';
@State isPlaying: boolean = false;
@State focusInfo: string = '';
@State interruptEvents: string[] = [];
// 预定义场景
private scenes: AudioSceneConfig[] = [
{
name: '音乐播放',
streamUsage: audio.StreamUsage.STREAM_USAGE_MUSIC,
contentType: audio.ContentType.CONTENT_TYPE_MUSIC,
interruptMode: audio.InterruptMode.SHARE_MODE,
description: '中等优先级,可被通话暂停,可与导航共存',
icon: '🎵'
},
{
name: '语音通话',
streamUsage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
contentType: audio.ContentType.CONTENT_TYPE_SPEECH,
interruptMode: audio.InterruptMode.INDEPENDENT_MODE,
description: '最高优先级,会暂停其他所有音频',
icon: '📞'
},
{
name: '语音助手',
streamUsage: audio.StreamUsage.STREAM_USAGE_VOICE_ASSISTANT,
contentType: audio.ContentType.CONTENT_TYPE_SPEECH,
interruptMode: audio.InterruptMode.INDEPENDENT_MODE,
description: '高优先级,会暂停音乐但不会打断通话',
icon: '🤖'
},
{
name: '导航提示',
streamUsage: audio.StreamUsage.STREAM_USAGE_NAVIGATION,
contentType: audio.ContentType.CONTENT_TYPE_SONIFICATION,
interruptMode: audio.InterruptMode.SHARE_MODE,
description: '共享模式,会让音乐降低音量而非暂停',
icon: '🗺️'
},
{
name: '通知提示',
streamUsage: audio.StreamUsage.STREAM_USAGE_NOTIFICATION,
contentType: audio.ContentType.CONTENT_TYPE_SONIFICATION,
interruptMode: audio.InterruptMode.SHARE_MODE,
description: '低优先级,短暂提示音',
icon: '🔔'
},
{
name: '闹钟',
streamUsage: audio.StreamUsage.STREAM_USAGE_ALARM,
contentType: audio.ContentType.CONTENT_TYPE_MUSIC,
interruptMode: audio.InterruptMode.INDEPENDENT_MODE,
description: '高优先级,会暂停音乐',
icon: '⏰'
},
];
aboutToDisappear(): void {
this.releaseCurrentRenderer();
}
/**
* 选择场景并创建对应的渲染器
*/
async selectScene(scene: AudioSceneConfig): Promise<void> {
// 先释放之前的渲染器
await this.releaseCurrentRenderer();
try {
const rendererOptions: audio.AudioRendererOptions = {
streamInfo: {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
channels: audio.AudioChannel.CHANNEL_2,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
},
rendererInfo: {
usage: scene.streamUsage,
rendererFlags: 0
}
};
this.currentRenderer = await audio.createAudioRenderer(rendererOptions);
// 设置中断模式
this.currentRenderer.on('audioInterrupt', (interruptEvent: audio.InterruptEvent) => {
this.handleInterrupt(interruptEvent, scene.name);
});
this.currentScene = scene.name;
this.focusInfo = `流类型: ${this.getStreamUsageName(scene.streamUsage)}\n中断模式: ${scene.interruptMode === audio.InterruptMode.SHARE_MODE ? '共享' : '独占'}`;
this.addEvent(`✅ 场景切换: ${scene.icon} ${scene.name}`);
} catch (err) {
const error = err as BusinessError;
this.addEvent(`❌ 场景切换失败: ${error.message}`);
}
}
/**
* 处理中断事件
*/
handleInterrupt(event: audio.InterruptEvent, sceneName: string): void {
const isBegin = event.eventType === audio.InterruptType.INTERRUPT_TYPE_BEGIN;
const hintName = this.getHintName(event.hintType);
this.addEvent(`${isBegin ? '🔴' : '🟢'} [${sceneName}] ${isBegin ? '中断开始' : '中断结束'}: ${hintName}`);
if (isBegin) {
switch (event.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
this.currentRenderer?.pause();
this.isPlaying = false;
this.isInterruptedPaused = true;
break;
case audio.InterruptHint.INTERRUPT_HINT_DUCK:
this.originalVolume = 1.0;
this.currentRenderer?.setVolume(0.2);
break;
}
} else {
switch (event.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_RESUME:
if (this.isInterruptedPaused) {
this.currentRenderer?.start();
this.isPlaying = true;
this.isInterruptedPaused = false;
}
break;
case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
this.currentRenderer?.setVolume(this.originalVolume);
break;
}
}
}
/**
* 获取流类型名称
*/
getStreamUsageName(usage: audio.StreamUsage): string {
const names: Record<number, string> = {
[audio.StreamUsage.STREAM_USAGE_MUSIC]: 'MUSIC(媒体)',
[audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION]: 'VOICE_COMM(通话)',
[audio.StreamUsage.STREAM_USAGE_VOICE_ASSISTANT]: 'VOICE_ASSIST(助手)',
[audio.StreamUsage.STREAM_USAGE_NAVIGATION]: 'NAVIGATION(导航)',
[audio.StreamUsage.STREAM_USAGE_NOTIFICATION]: 'NOTIFICATION(通知)',
[audio.StreamUsage.STREAM_USAGE_ALARM]: 'ALARM(闹钟)',
};
return names[usage] || `UNKNOWN(${usage})`;
}
/**
* 获取中断提示名称
*/
getHintName(hint: audio.InterruptHint): string {
const names: Record<number, string> = {
[audio.InterruptHint.INTERRUPT_HINT_PAUSE]: '暂停',
[audio.InterruptHint.INTERRUPT_HINT_STOP]: '停止',
[audio.InterruptHint.INTERRUPT_HINT_DUCK]: '降低音量',
[audio.InterruptHint.INTERRUPT_HINT_UNDUCK]: '恢复音量',
[audio.InterruptHint.INTERRUPT_HINT_RESUME]: '恢复播放',
};
return names[hint] || `未知(${hint})`;
}
/**
* 添加事件
*/
addEvent(message: string): void {
const time = new Date().toLocaleTimeString();
this.interruptEvents.unshift(`[${time}] ${message}`);
if (this.interruptEvents.length > 30) {
this.interruptEvents = this.interruptEvents.slice(0, 30);
}
}
/**
* 释放当前渲染器
*/
async releaseCurrentRenderer(): Promise<void> {
if (this.currentRenderer) {
try {
this.currentRenderer.off('audioInterrupt');
if (this.isPlaying) {
await this.currentRenderer.stop();
}
await this.currentRenderer.release();
this.currentRenderer = null;
this.isPlaying = false;
} catch (err) {
console.error('[场景] 释放失败');
}
}
}
build() {
Column({ space: 16 }) {
// 标题
Text('🎭 音频场景与焦点策略')
.fontSize(22)
.fontWeight(FontWeight.Bold)
// 当前场景信息
if (this.currentScene !== '未选择') {
Column({ space: 8 }) {
Text(`当前场景: ${this.currentScene}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#4CAF50')
Text(this.focusInfo)
.fontSize(12)
.fontColor('#888')
}
.width('90%')
.padding(12)
.borderRadius(8)
.backgroundColor('#252540')
}
// 场景选择列表
Text('选择音频场景')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('90%')
List({ space: 8 }) {
ForEach(this.scenes, (scene: AudioSceneConfig) => {
ListItem() {
Column({ space: 8 }) {
Row({ space: 12 }) {
Text(scene.icon)
.fontSize(28)
Column({ space: 4 }) {
Text(scene.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(this.currentScene === scene.name ? '#4CAF50' : '#fff')
Text(scene.description)
.fontSize(12)
.fontColor('#888')
.maxLines(2)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 选中标记
if (this.currentScene === scene.name) {
Text('✓')
.fontSize(16)
.fontColor('#4CAF50')
}
}
// 焦点策略标签
Row({ space: 6 }) {
Text(scene.interruptMode === audio.InterruptMode.SHARE_MODE ? '共享模式' : '独占模式')
.fontSize(10)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor(scene.interruptMode === audio.InterruptMode.SHARE_MODE ? '#2196F3' : '#F44336')
.fontColor('#fff')
}
}
.width('100%')
.padding(12)
.borderRadius(8)
.backgroundColor(this.currentScene === scene.name ? 'rgba(76,175,80,0.1)' : '#252540')
.onClick(() => this.selectScene(scene))
}
}, (scene: AudioSceneConfig) => scene.name)
}
.width('90%')
.layoutWeight(1)
// 中断事件日志
if (this.interruptEvents.length > 0) {
Column({ space: 4 }) {
Text('📋 焦点事件')
.fontSize(14)
.fontWeight(FontWeight.Medium)
List({ space: 2 }) {
ForEach(this.interruptEvents.slice(0, 5), (event: string, index: number) => {
ListItem() {
Text(event)
.fontSize(10)
.fontColor('#aaa')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
})
}
.height(80)
}
.width('90%')
.padding(8)
.borderRadius(8)
.backgroundColor('#252540')
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(20)
.backgroundColor('#1a1a2e')
}
}
3.3 多应用音频协调实战
模拟一个真实场景:音乐播放器需要正确处理来自通话、导航、通知等场景的焦点中断,并实现优雅的恢复逻辑:
import { audio } from '@kit.AudioKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 焦点事件记录
interface FocusEventRecord {
timestamp: number;
eventType: string;
hintType: string;
action: string;
}
@Entry
@Component
struct MultiAppAudioCoordinator {
// 音乐播放器渲染器
private musicRenderer: audio.AudioRenderer | null = null;
// 原始音量
private savedVolume: number = 1.0;
// 是否因焦点中断而暂停
private pausedByInterrupt: boolean = false;
// 是否因焦点中断而降低音量
private duckedByInterrupt: boolean = false;
// 状态
@State musicPlaying: boolean = false;
@State musicVolume: number = 100;
@State currentStatus: string = '空闲';
@State focusHistory: FocusEventRecord[] = [];
@State autoResumeEnabled: boolean = true;
aboutToAppear(): void {
this.initMusicPlayer();
}
aboutToDisappear(): void {
this.releaseMusicPlayer();
}
/**
* 初始化音乐播放器
*/
async initMusicPlayer(): Promise<void> {
try {
const rendererOptions: audio.AudioRendererOptions = {
streamInfo: {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
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
}
};
this.musicRenderer = await audio.createAudioRenderer(rendererOptions);
// ★ 注册焦点中断监听
this.musicRenderer.on('audioInterrupt', (event: audio.InterruptEvent) => {
this.onMusicInterrupt(event);
});
this.currentStatus = '🎵 音乐播放器就绪';
this.addFocusRecord('初始化', '—', '渲染器创建完成,焦点监听已注册');
} catch (err) {
const error = err as BusinessError;
console.error(`[协调] 初始化失败: ${error.message}`);
}
}
/**
* ★★★ 核心回调:音乐播放器的焦点中断处理 ★★★
*/
onMusicInterrupt(event: audio.InterruptEvent): void {
const isBegin = event.eventType === audio.InterruptType.INTERRUPT_TYPE_BEGIN;
const hint = event.hintType;
if (isBegin) {
// ========== 中断开始 ==========
switch (hint) {
case audio.InterruptHint.INTERRUPT_HINT_PAUSE: {
// 场景:来电、闹钟等高优先级音频抢占
// 行为:立即暂停播放,等待恢复
this.pauseByFocus();
this.addFocusRecord('中断-暂停', 'PAUSE', '高优先级音频抢占,暂停播放');
this.currentStatus = '🔴 被暂停(通话/闹钟等)';
break;
}
case audio.InterruptHint.INTERRUPT_HINT_STOP: {
// 场景:系统强制停止
// 行为:停止播放
this.stopByFocus();
this.addFocusRecord('中断-停止', 'STOP', '系统强制停止播放');
this.currentStatus = '🔴 被停止';
break;
}
case audio.InterruptHint.INTERRUPT_HINT_DUCK: {
// 场景:导航提示音、通知音等需要"共存"的音频
// 行为:降低音量到 20%,不完全暂停
this.duckByFocus();
this.addFocusRecord('中断-降低', 'DUCK', '导航/通知等共存音频,降低音量');
this.currentStatus = '🟡 音量降低(导航/通知共存)';
break;
}
}
} else {
// ========== 中断结束 ==========
switch (hint) {
case audio.InterruptHint.INTERRUPT_HINT_RESUME: {
// 场景:通话结束、闹钟关闭
// 行为:恢复播放(如果之前是因焦点中断而暂停的)
if (this.autoResumeEnabled && this.pausedByInterrupt) {
this.resumeByFocus();
this.addFocusRecord('恢复-播放', 'RESUME', '高优先级音频结束,恢复播放');
this.currentStatus = '🟢 已恢复播放';
} else {
this.addFocusRecord('恢复-提示', 'RESUME', '高优先级音频结束(自动恢复已关闭)');
this.currentStatus = '🟡 可恢复播放(需手动)';
}
break;
}
case audio.InterruptHint.INTERRUPT_HINT_UNDUCK: {
// 场景:导航提示音结束
// 行为:恢复原始音量
this.unduckByFocus();
this.addFocusRecord('恢复-音量', 'UNDUCK', '共存音频结束,恢复音量');
this.currentStatus = '🟢 音量已恢复';
break;
}
}
}
}
/**
* 因焦点中断而暂停
*/
pauseByFocus(): void {
if (!this.musicRenderer) return;
try {
this.savedVolume = this.musicVolume / 100;
this.musicRenderer.pause();
this.musicPlaying = false;
this.pausedByInterrupt = true;
} catch (err) {
console.error('[协调] 暂停失败');
}
}
/**
* 因焦点中断而停止
*/
stopByFocus(): void {
if (!this.musicRenderer) return;
try {
this.musicRenderer.stop();
this.musicPlaying = false;
this.pausedByInterrupt = false;
this.duckedByInterrupt = false;
} catch (err) {
console.error('[协调] 停止失败');
}
}
/**
* 因焦点中断而降低音量
*/
duckByFocus(): void {
if (!this.musicRenderer) return;
try {
this.savedVolume = this.musicVolume / 100;
const duckedVolume = this.savedVolume * 0.2;
this.musicRenderer.setVolume(duckedVolume);
this.musicVolume = Math.round(duckedVolume * 100);
this.duckedByInterrupt = true;
} catch (err) {
console.error('[协调] 降低音量失败');
}
}
/**
* 因焦点恢复而恢复音量
*/
unduckByFocus(): void {
if (!this.musicRenderer) return;
try {
const restoredVolume = this.savedVolume;
this.musicRenderer.setVolume(restoredVolume);
this.musicVolume = Math.round(restoredVolume * 100);
this.duckedByInterrupt = false;
} catch (err) {
console.error('[协调] 恢复音量失败');
}
}
/**
* 因焦点恢复而恢复播放
*/
resumeByFocus(): void {
if (!this.musicRenderer) return;
try {
this.musicRenderer.start();
this.musicPlaying = true;
this.pausedByInterrupt = false;
} catch (err) {
console.error('[协调] 恢复播放失败');
}
}
/**
* 手动播放/暂停
*/
async togglePlay(): Promise<void> {
if (!this.musicRenderer) return;
try {
if (this.musicPlaying) {
await this.musicRenderer.pause();
this.musicPlaying = false;
this.currentStatus = '🟡 手动暂停';
} else {
await this.musicRenderer.start();
this.musicPlaying = true;
this.pausedByInterrupt = false;
this.currentStatus = '🟢 播放中';
}
} catch (err) {
console.error('[协调] 切换失败');
}
}
/**
* 添加焦点事件记录
*/
addFocusRecord(eventType: string, hintType: string, action: string): void {
this.focusHistory.unshift({
timestamp: Date.now(),
eventType: eventType,
hintType: hintType,
action: action
});
if (this.focusHistory.length > 20) {
this.focusHistory = this.focusHistory.slice(0, 20);
}
}
/**
* 格式化时间
*/
formatTimestamp(ts: number): string {
const date = new Date(ts);
return `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
}
/**
* 释放播放器
*/
async releaseMusicPlayer(): Promise<void> {
if (this.musicRenderer) {
try {
this.musicRenderer.off('audioInterrupt');
await this.musicRenderer.release();
this.musicRenderer = null;
} catch (err) {
console.error('[协调] 释放失败');
}
}
}
build() {
Column({ space: 16 }) {
// 标题
Text('🎼 多应用音频协调器')
.fontSize(22)
.fontWeight(FontWeight.Bold)
// 当前状态
Text(this.currentStatus)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.padding(12)
.borderRadius(8)
.backgroundColor('#252540')
.width('90%')
.textAlign(TextAlign.Center)
// 音量控制
Row({ space: 12 }) {
Text('🔊')
.fontSize(20)
Slider({
value: this.musicVolume,
min: 0,
max: 100,
style: SliderStyle.OutSet
})
.width('60%')
.onChange((value: number) => {
this.musicVolume = Math.round(value);
this.musicRenderer?.setVolume(value / 100);
})
Text(`${this.musicVolume}%`)
.fontSize(14)
.width(40)
}
.width('90%')
// 播放控制
Row({ space: 16 }) {
Button(this.musicPlaying ? '⏸ 暂停' : '▶️ 播放')
.width(120)
.backgroundColor(this.musicPlaying ? '#FF9800' : '#4CAF50')
.onClick(() => this.togglePlay())
}
// 自动恢复开关
Row({ space: 12 }) {
Text('中断后自动恢复')
.fontSize(14)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.autoResumeEnabled })
.onChange((isOn: boolean) => {
this.autoResumeEnabled = isOn;
})
.selectedColor('#4CAF50')
}
.width('90%')
// 焦点事件历史
Column({ space: 8 }) {
Text('📋 焦点事件历史')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
List({ space: 4 }) {
ForEach(this.focusHistory, (record: FocusEventRecord, index: number) => {
ListItem() {
Row({ space: 8 }) {
Text(this.formatTimestamp(record.timestamp))
.fontSize(10)
.fontColor('#666')
.width(60)
Text(record.hintType)
.fontSize(10)
.padding({ left: 4, right: 4, top: 2, bottom: 2 })
.borderRadius(4)
.backgroundColor(
record.hintType.includes('暂停') || record.hintType.includes('停止') ? '#F44336' :
record.hintType.includes('降低') ? '#FF9800' :
record.hintType.includes('恢复') ? '#4CAF50' : '#333'
)
.fontColor('#fff')
Text(record.action)
.fontSize(10)
.fontColor('#aaa')
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
}
}, (record: FocusEventRecord, index: number) => `${index}`)
}
.width('100%')
.height(200)
}
.width('90%')
.padding(16)
.borderRadius(12)
.backgroundColor('#252540')
// 焦点优先级说明
Column({ space: 4 }) {
Text('📊 焦点优先级(高→低)')
.fontSize(14)
.fontWeight(FontWeight.Medium)
ForEach([
'📞 语音通话 → 暂停一切',
'🤖 语音助手 → 暂停音乐',
'⏰ 闹钟/铃声 → 暂停音乐',
'🔔 通知提示 → 短暂降低音量',
'🗺️ 导航提示 → 降低音乐音量',
'🎵 媒体音乐 → 可被以上所有打断',
], (text: string) => {
Text(text)
.fontSize(11)
.fontColor('#888')
})
}
.width('90%')
.padding(12)
.borderRadius(8)
.backgroundColor('#252540')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(20)
.backgroundColor('#1a1a2e')
}
}
四、踩坑与注意事项
4.1 不注册 audioInterrupt 监听
问题:创建了 AudioRenderer 但没有注册 audioInterrupt 监听,结果通话来了音乐不停。
原因:系统虽然会发送中断事件,但如果应用没有监听,就不会做出响应。系统不会自动帮你暂停播放。
解决方案:必须注册 audioInterrupt 监听,并在回调中正确处理。
// ❌ 错误写法:没有注册监听
const renderer = await audio.createAudioRenderer(options);
renderer.start(); // 通话来了也不会暂停!
// ✅ 正确写法:注册监听并处理
const renderer = await audio.createAudioRenderer(options);
renderer.on('audioInterrupt', (event) => {
if (event.eventType === audio.InterruptType.INTERRUPT_TYPE_BEGIN) {
if (event.hintType === audio.InterruptHint.INTERRUPT_HINT_PAUSE) {
renderer.pause(); // 主动暂停!
}
}
});
renderer.start();
4.2 中断恢复时自动播放
问题:收到 INTERRUPT_HINT_RESUME 后无条件恢复播放,但用户之前是手动暂停的,结果音乐突然响起来了。
原因:没有区分"因焦点中断而暂停"和"用户手动暂停"。
解决方案:用一个标志位记录暂停原因,只在因焦点中断而暂停时才自动恢复。
private pausedByInterrupt: boolean = false;
// 焦点中断暂停
onInterruptPause() {
this.pausedByInterrupt = true;
this.renderer.pause();
}
// 用户手动暂停
onUserPause() {
this.pausedByInterrupt = false; // 标记为手动暂停
this.renderer.pause();
}
// 焦点恢复
onInterruptResume() {
if (this.pausedByInterrupt) { // 只有因焦点中断暂停的才自动恢复
this.renderer.start();
this.pausedByInterrupt = false;
}
}
4.3 Duck 后忘记恢复音量
问题:收到 INTERRUPT_HINT_DUCK 降低音量后,没有收到 INTERRUPT_HINT_UNDUCK 就结束了,音量一直很低。
原因:某些边界情况下(如导航 App 崩溃),UNDUCK 事件可能丢失。
解决方案:设置一个超时机制,如果一定时间内没有收到 UNDUCK,主动恢复音量。
private duckTimeout: number = -1;
onDuck() {
this.savedVolume = this.currentVolume;
this.renderer.setVolume(this.savedVolume * 0.2);
// 设置超时保护(10秒后自动恢复)
this.duckTimeout = setTimeout(() => {
console.warn('[焦点] Duck 超时,主动恢复音量');
this.renderer.setVolume(this.savedVolume);
}, 10000) as unknown as number;
}
onUnduck() {
// 清除超时
if (this.duckTimeout !== -1) {
clearTimeout(this.duckTimeout);
this.duckTimeout = -1;
}
this.renderer.setVolume(this.savedVolume);
}
4.4 焦点模式选择错误
问题:音乐播放器用了 INDEPENDENT_MODE,结果打开另一个音乐 App 时,自己的音乐被强制暂停了。
原因:INDEPENDENT_MODE 是独占模式,同类型互斥。音乐播放器应该用 SHARE_MODE。
选择建议:
- 音乐、视频、游戏 →
SHARE_MODE(可共存) - 语音通话、闹钟 →
INDEPENDENT_MODE(独占)
4.5 渲染器释放后仍收到中断回调
问题:调用了 renderer.release() 后,还收到 audioInterrupt 回调,访问已释放的渲染器导致崩溃。
解决方案:在释放前先取消监听,并在回调中检查渲染器是否还有效。
async releaseRenderer(): Promise<void> {
// 先取消监听
this.renderer?.off('audioInterrupt');
// 再释放
await this.renderer?.release();
this.renderer = null;
}
五、HarmonyOS 6 适配
5.1 API 变更
| 变更项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| 焦点监听 | renderer.on('audioInterrupt') |
保持一致,新增 AudioInterruptManager 统一管理 |
| 焦点请求 | 隐式(创建渲染器时自动请求) | 新增 requestInterrupt() 显式请求 |
| 焦点释放 | 隐式(释放渲染器时自动释放) | 新增 abandonInterrupt() 显式释放 |
| 焦点模式 | SHARE / INDEPENDENT | 新增 AUTO 模式(系统自动选择) |
| 多流焦点 | 每个渲染器独立 | 新增 InterruptGroup 组级焦点管理 |
5.2 迁移指南
// HarmonyOS 5 写法:隐式焦点管理
const renderer = await audio.createAudioRenderer(options);
renderer.on('audioInterrupt', (event) => { ... });
// 焦点随渲染器生命周期自动管理
// HarmonyOS 6 写法:显式焦点管理
const interruptManager = audio.createAudioInterruptManager();
await interruptManager.requestInterrupt({
streamUsage: audio.StreamUsage.STREAM_USAGE_MUSIC,
contentType: audio.ContentType.CONTENT_TYPE_MUSIC,
interruptMode: audio.InterruptMode.SHARE_MODE
});
interruptManager.on('audioInterrupt', (event) => { ... });
// 不再依赖渲染器生命周期
5.3 新特性
- 显式焦点管理:不再绑定到渲染器,可以更灵活地控制焦点生命周期
- 焦点组:多个音频流可以组成一个焦点组,统一管理
- AUTO 模式:系统根据音频内容自动选择最优的焦点模式
- 焦点优先级自定义:应用可以声明自己的焦点优先级,实现更精细的协调
六、总结
mindmap
root((音频焦点))
焦点机制
请求-授权-监听
系统统一仲裁
基于流类型优先级
焦点优先级
语音通话 最高
语音助手
闹钟/铃声
通知提示
导航提示
媒体音乐 最低
中断类型
INTERRUPT_TYPE_BEGIN
PAUSE 暂停
STOP 停止
DUCK 降低音量
INTERRUPT_TYPE_END
RESUME 恢复播放
UNDUCK 恢复音量
焦点模式
SHARE_MODE 共享
可与其他应用共存
适用于音乐/导航
INDEPENDENT_MODE 独占
抢占其他应用焦点
适用于通话/闹钟
正确处理
必须注册audioInterrupt
区分中断暂停/手动暂停
Duck超时保护
释放前取消监听
选择正确的焦点模式
多应用协调
通话→暂停音乐
导航→降低音乐音量
通知→短暂降低
通话结束→恢复音乐
核心要点回顾:
- 必须注册 audioInterrupt:系统不会自动帮你暂停,必须监听中断事件并主动响应
- 区分暂停原因:用标志位区分"因焦点中断暂停"和"用户手动暂停",恢复时只自动恢复前者
- Duck/Unduck 成对处理:降低音量后要等恢复音量事件,设置超时保护防止事件丢失
- 选择正确的焦点模式:音乐用 SHARE_MODE,通话用 INDEPENDENT_MODE,不要搞反
- 资源释放要干净:释放渲染器前先取消焦点监听,避免回调访问已释放的资源
- 点赞
- 收藏
- 关注作者

评论(0)