HarmonyOS 6.0 音频焦点与多路播放协调
手机上同时跑两个App都会出声——抖音在播视频,微信来消息叮一声,两个声音撞在一起就变成了噪音。Android有AudioFocus机制协调,HarmonyOS也有类似方案。但音频焦点不只是系统的事——你的App内部也可能有多个音源同时播放(背景音乐+音效+语音播报),需要自己做协调。这篇把系统级音频焦点和应用内多路播放的协调方案都讲清楚。
AVSession音频会话

HarmonyOS用AVSession管理音频会话。每个播放器创建一个AVSession,系统根据会话类型和策略决定谁发声:
import { avSession } from '@kit.AVSessionKit';
let session: avSession.AVSession | null = null;
async createSession(context: Context): Promise<void> {
session = await avSession.createAVSession(context, 'MusicPlayer', 'audio');
await session.activate();
session.on('play', () => {
this.resumePlayback();
});
session.on('pause', () => {
this.pausePlayback();
});
session.on('stop', () => {
this.stopPlayback();
});
}
createAVSession的第三个参数是会话类型:‘audio’(音乐/音效)、‘video’(视频)。不同类型的焦点策略不同——'audio’类型的会话被电话打断时会暂停,'video’类型可能只降低音量。
AVSession必须activate才生效。 不activate的session不参与焦点管理。
设置会话元数据
告诉系统你在播什么——锁屏/控制中心会显示这些信息:
let metadata: avSession.AVMetadata = {
assetId: 'song_001',
title: '夜曲',
artist: '周杰伦',
mediaUri: 'https://example.com/music/night.mp3',
duration: 240000
};
await session.setAVMetadata(metadata);
锁屏界面的媒体控件会显示title和artist。duration是毫秒,240000=4分钟。
播放状态同步
告诉系统当前播放状态:
let playbackState: avSession.AVPlaybackState = {
state: avSession.PlaybackState.PLAYBACK_STATE_PLAYING,
speed: 1.0,
position: { elapsedTime: 30000, updateTime: Date.now() },
bufferedTime: 180000,
loopMode: avSession.LoopMode.LOOP_MODE_SINGLE,
isFavorite: false
};
await session.setAVPlaybackState(playbackState);
state是核心:PLAYING、PAUSED、STOPPED、BUFFERING等。系统根据state决定焦点行为——PLAYING的session持有一半焦点,PAUSED的session释放焦点。
position中的elapsedTime是已播放时间(毫秒),updateTime是更新时的时间戳。系统用两者计算当前播放位置。
焦点变化监听
当其他App获取焦点时,你的播放可能被暂停或降低音量:
session.on('interrupt', (event: avSession.InterruptEvent) => {
if (event.type === avSession.InterruptType.INTERRUPT_TYPE_BEGIN) {
// 焦点被抢占
if (event.hint === avSession.InterruptHint.INTERRUPT_HINT_PAUSE) {
this.pausePlayback();
this.isInterrupted = true;
} else if (event.hint === avSession.InterruptHint.INTERRUPT_HINT_DUCK) {
this.lowerVolume();
}
} else if (event.type === avSession.InterruptType.INTERRUPT_TYPE_END) {
// 焦点恢复
if (event.hint === avSession.InterruptHint.INTERRUPT_HINT_RESUME) {
if (this.isInterrupted) {
this.resumePlayback();
this.isInterrupted = false;
}
} else if (event.hint === avSession.InterruptHint.INTERRUPT_HINT_UNDUCK) {
this.restoreVolume();
}
}
});
两种打断类型:
- INTERRUPT_HINT_PAUSE:被更高优先级的音源(电话、语音助手)打断,必须暂停
- INTERRUPT_HINT_DUCK:被临时音源(通知提示音)打断,降低音量即可
关键:PAUSE后不一定要自动恢复。 INTERRUPT_HINT_RESUME提示可以恢复,但有些场景(用户主动暂停又被电话打断)不应该自动恢复。用isInterrupted标记区分。
应用内多路播放
App内部可能有多个音源同时播放。常见场景:
- 背景音乐 + 按钮音效
- 背景音乐 + 语音播报
- 视频播放 + 通知提示音
规则:同一时间只允许一个主音源播放。 主音源是音乐、视频、语音这类持续性音源。音效、提示音是瞬态音源,可以跟主音源叠加。
class AudioCoordinator {
private activePlayer: string = '';
private pausedPlayers: Set<string> = new Set();
private bgMusicVolume: number = 1.0;
requestPlay(playerId: string, priority: number): boolean {
if (this.activePlayer === playerId) {
return true;
}
if (this.activePlayer.length === 0) {
this.activePlayer = playerId;
return true;
}
if (priority >= this.getPriority(this.activePlayer)) {
this.pausedPlayers.add(this.activePlayer);
this.pausePlayer(this.activePlayer);
this.activePlayer = playerId;
return true;
}
return false;
}
releasePlay(playerId: string): void {
if (this.activePlayer === playerId) {
this.activePlayer = '';
// 恢复上一个被暂停的播放器
if (this.pausedPlayers.size > 0) {
let lastPaused = Array.from(this.pausedPlayers).pop();
if (lastPaused !== undefined) {
this.pausedPlayers.delete(lastPaused);
this.activePlayer = lastPaused;
this.resumePlayer(lastPaused);
}
}
} else {
this.pausedPlayers.delete(playerId);
}
}
}
优先级高的播放器抢占焦点,低的被暂停。释放焦点后恢复上一个。
背景音乐+音效
最常见的组合——背景音乐持续播放,音效叠加在上面:
private bgMediaPlayer: media.AVPlayer | null = null;
private effectPlayer: media.AVPlayer | null = null;
async playBgMusic(url: string): Promise<void> {
this.bgMediaPlayer = await media.createAVPlayer();
this.bgMediaPlayer.url = url;
this.bgMediaPlayer.looping = true;
this.bgMediaPlayer.volume = 0.3;
await this.bgMediaPlayer.prepare();
await this.bgMediaPlayer.play();
}
async playEffect(url: string): Promise<void> {
this.effectPlayer = await media.createAVPlayer();
this.effectPlayer.url = url;
this.effectPlayer.volume = 1.0;
this.effectPlayer.on('stateChange', (state: string) => {
if (state === 'completed' || state === 'stopped') {
this.effectPlayer?.release();
this.effectPlayer = null;
}
});
await this.effectPlayer.prepare();
await this.effectPlayer.play();
}
背景音乐volume=0.3(30%),音效volume=1.0(100%)。音效音量高于背景音乐,不会被盖住。背景音乐looping=true循环播放。
注意:音效播放器用完要release,否则会占住音频资源。 stateChange中检测播放完成/停止后释放。
语音播报暂停背景音乐
导航语音、朗读文本——这类语音播报需要独占音频:
async playVoiceAnnouncement(text: string): Promise<void> {
// 暂停背景音乐
if (this.bgMediaPlayer !== null && this.bgMediaPlayer.state === 'playing') {
await this.bgMediaPlayer.pause();
this.wasBgPlaying = true;
}
// 播放语音
let ttsPlayer = await media.createAVPlayer();
// 设置TTS音频源
ttsPlayer.on('stateChange', (state: string) => {
if (state === 'completed') {
ttsPlayer.release();
// 恢复背景音乐
if (this.wasBgPlaying && this.bgMediaPlayer !== null) {
this.bgMediaPlayer.play();
this.wasBgPlaying = false;
}
}
});
await ttsPlayer.prepare();
await ttsPlayer.play();
}
语音播报前暂停背景音乐,播报完成后恢复。wasBgPlaying标记记录背景音乐是否在播放——如果背景音乐本来就是暂停的,不应该自动恢复。
音量控制策略
不同音源的音量关系:
| 音源 | 建议音量 | 说明 |
|---|---|---|
| 背景音乐 | 0.2~0.4 | 氛围,不能盖住其他声音 |
| 语音播报 | 0.8~1.0 | 需要听清楚 |
| 音效 | 0.6~1.0 | 短促有力 |
| 视频播放 | 0.8~1.0 | 主内容 |
| 通知提示 | 0.5~0.7 | 短暂打断 |
Duck模式下背景音乐降低到0.1~0.2,提示音结束后恢复。
会话释放
退出播放时必须释放AVSession:
async releaseSession(): Promise<void> {
if (session !== null) {
await session.deactivate();
session.destroy();
session = null;
}
}
aboutToDisappear(): void {
this.releaseSession();
if (this.bgMediaPlayer !== null) {
this.bgMediaPlayer.release();
}
if (this.effectPlayer !== null) {
this.effectPlayer.release();
}
}
deactivate停用会话,destroy销毁会话。不释放的话锁屏控件会一直显示,系统也认为你的App还在占用音频资源。
远程控制
耳机按钮、锁屏控件、通知栏控件都通过AVSession控制播放:
session.on('play', () => {
this.resumePlayback();
});
session.on('pause', () => {
this.pausePlayback();
});
session.on('seek', (time: number) => {
this.seekTo(time);
});
session.on('playNext', () => {
this.playNext();
});
session.on('playPrevious', () => {
this.playPrevious();
});
注册这些回调后,用户按耳机播放键、锁屏按暂停、通知栏切歌,都能正确响应。
注意:AVSession的回调是系统触发的,不是用户点击UI触发的。 你需要同时处理UI按钮和AVSession回调两条路径,确保两者行为一致。
踩坑清单
| 问题 | 原因 | 解决 |
|---|---|---|
| 电话挂断音乐不恢复 | 没监听interrupt恢复 | 处理INTERRUPT_HINT_RESUME |
| 背景音乐盖住语音 | 音量没区分 | 背景音乐0.20.4,语音0.81.0 |
| 音效播放后没释放 | 没在completed回调中release | stateChange检测completed |
| 锁屏不显示播放控件 | AVSession没设metadata | setAVMetadata设置title/artist |
| 耳机按键没反应 | 没注册play/pause回调 | session.on(‘play’/‘pause’) |
| 两个音源同时出声 | 没做应用内焦点协调 | AudioCoordinator统一管理 |
| 退出后锁屏还在 | AVSession没deactivate | aboutToDisappear中release |
| 视频被通知打断 | 通知音与视频同时播放 | 对视频用DUCK策略 |
| AVPlayer创建失败 | 没有prepare直接play | 先prepare再play |
| Duck后音量不恢复 | 没处理UNDUCK | INTERRUPT_HINT_UNDUCK恢复音量 |
音频焦点管理的核心是"一次一个主音源"——系统级靠AVSession的interrupt机制,应用内靠AudioCoordinator协调。背景音乐低音量,音效短促叠加,语音独占暂停其余。记住了这个原则,多路音频就不会打架。
更多推荐

所有评论(0)