手机上同时跑两个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协调。背景音乐低音量,音效短促叠加,语音独占暂停其余。记住了这个原则,多路音频就不会打架。

Logo

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

更多推荐