## 概述

对于音频播放类应用，除了歌曲播控的基础能力外，各种交互场景的设计也对用户体验有着重要的影响。本文以音乐播放器应用为例，从应用与用户、播放设备以及其他应用的交互三方面入手，分别对典型使用场景给出示例方案，为应用带来灵活多样、符合用户直觉的交互体验。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/7e/v3/sB9JRuFUSfiTxgMhAyb8Aw/zh-cn_image_0000002229451481.gif?HW-CC-KV=V1&HW-CC-Date=20260414T060137Z&HW-CC-Expire=86400&HW-CC-Sign=F551D80F1137387C8859171AE42646C78CB477BBB5247314FE5B3BBCE0AB90BE "点击放大")

## 场景分析

根据当前HarmonyOS APP开发过程中遇到的实际音频类应用业务场景，总结提炼出如下典型场景，设计其交互功能与方案以供参考：

展开

| 场景分类 | 典型场景 | 场景描述 |
| --- | --- | --- |
| 与用户交互 | 播控中心操控 | 无需进入应用直接通过播控中心操控歌曲状态 |
| 后台播放音乐 | 应用位于后台长时间持续播放音乐 |
| 与音频播放设备交互 | 播放设备的状态发生改变 | 新设备可用、旧设备不可用、用户切换设备场景的适配方案 |
| 响应播放设备的指令 | 蓝牙耳机控制应用播放状态 |
| 用户主动切换播放设备 | 通过投播组件选择播放设备 |
| 与其他应用交互 | 与其他应用音频冲突 | 被其他音频打断、与其他音频并发 |

## 与用户交互

### 播控中心控制音乐状态

在应用接入播控中心后，用户可以直接通过播控中心修改音频播放状态，如暂停/播放、下一首、切换播放模式、拖动进度条等，应用内的音频信息和状态也会同步显示在播控中心界面上，使得用户与应用的交互更加灵活易用。同时与播控中心交互使用的AVSession还管控着应用的后台播放能力，因此对于音乐类应用一般推荐接入播控中心，具体适用场景可以参考[应用接入AVSession场景介绍](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/avsession-access-scene)。

应用与播控中心交互的过程如下图所示：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/e3/v3/92A63VTiR3mha8H8Gc_rlw/zh-cn_image_0000002229336989.png?HW-CC-KV=V1&HW-CC-Date=20260414T060137Z&HW-CC-Expire=86400&HW-CC-Sign=71F5CF7FE26F6AA1E96D5F4310307780AA45EF6B424FF8160A7796F7C3887D6B "点击放大")

由图可见，应用的接入流程可以主要分为三个部分：创建AVSession、监听播控中心通知以及向播控中心上报应用状态，以下分别给出具体实现：

1. 应用创建媒体会话

   在应用的音频渲染器初始化的时候创建并激活AVSession，将此会话用作应用与播控中心交互的媒介，音乐类应用选择创建audio类型的AVSession，不同类型的AVSession对应不同的播控中心样式，详细指导参考[媒体会话提供方](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/using-avsession-developer)。

   收起

   自动换行

   深色代码主题

   复制

   ```
   1. import { avSession } from '@kit.AVSessionKit';
   2. // ...
   3. export class AVSessionController {
   4. private context: common.UIAbilityContext | undefined = undefined;
   5. private AVSession: avSession.AVSession | undefined = undefined;
   6. private songList: SongItem[] = [];
   7. private musicIndex: number | undefined = undefined;
   8. private audioRendererController: AudioRendererController | undefined = undefined;
   9. // ...
   10. private async initAVSession() {
   11. this.context = AppStorage.get('context');
   12. if (!this.context) {
   13. Logger.info(TAG, `session create failed : conext is undefined`);
   14. return;
   15. }
   16. this.audioRendererController = AppStorage.get('audioRendererController');
   17. if (!this.audioRendererController) {
   18. Logger.info(TAG, `session create failed : audioRendererController is undefined`);
   19. return;
   20. }
   21. this.AVSession = await avSession.createAVSession(this.context, "PLAY_AUDIO", 'audio');
   22. await this.AVSession.activate();
   23. // ...
   24. }
   25. // ...
   26. }
   ```

   [AVSessionController.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/AVSessionController.ets#L18-L383)
2. 注册播控中心通知回调

   当用户在锁屏或者其他界面时希望通过播控中心控制应用的播放状态，比如播放、暂停、下一首等，应用需要设置监听回调来对播控中心的通知做出对应的调整，只有设置了回调，播控中心侧的按钮才会亮起来，否则按钮将会置灰。

   收起

   自动换行

   深色代码主题

   复制

   ```
   1. async setListenerForMesFromController() {
   2. if (!this.AVSession) {
   3. return;
   4. }
   5. this.AVSession.on('play', this.onPlay);
   6. this.AVSession.on('pause', this.onPause);
   7. this.AVSession.on('playNext', this.onPlayNext);
   8. this.AVSession.on('playPrevious', this.onPlayPrevious);
   9. this.AVSession.on('seek', this.onSeek);
   10. this.AVSession.on('setLoopMode', this.onSetLoopMode);
   11. this.AVSession.on('toggleFavorite', this.onToggleFavorite);
   12. }
   ```

   [AVSessionController.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/AVSessionController.ets#L157-L168)

   适配时需要注意播控中心点击循环模式图标后，应用在回调中只会收到当前播控中心的图标状态，因此开发者需要在业务代码中自己管理状态切换顺序（如：单曲循环->顺序播放->随机播放->单曲循环），根据收到的当前状态指定要切换到的下一个状态。

   收起

   自动换行

   深色代码主题

   复制

   ```
   1. private onSetLoopMode: (loopMode: avSession.LoopMode) => void = (loopMode: avSession.LoopMode) => {
   2. Logger.info(TAG, `on setLoopMode , do loopMode task`);
   3. if (!this.audioRendererController) {
   4. Logger.error(TAG, 'audioRendererController is undefined in onSetLoopMode');
   5. return;
   6. }
   7. switch (loopMode) {
   8. case avSession.LoopMode.LOOP_MODE_SINGLE:
   9. this.audioRendererController.setPlayModel(MusicPlayMode.ORDER);
   10. break;
   11. case avSession.LoopMode.LOOP_MODE_SEQUENCE:
   12. this.audioRendererController.setPlayModel(MusicPlayMode.RANDOM);
   13. break;
   14. case avSession.LoopMode.LOOP_MODE_SHUFFLE:
   15. this.audioRendererController.setPlayModel(MusicPlayMode.SINGLE_CYCLE);
   16. break;
   17. default:
   18. break;
   19. }
   20. this.setPlayModeToAVSession();
   21. this.setPlayModeToControlArea();
   22. }
   ```

   [AVSessionController.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/AVSessionController.ets#L219-L240)

   应用如果已切换到后台，用户点击播控中心，将由播控中心负责拉起应用。应用需要配置参数来选择拉起的应用和页面，并通过setLaunchAbility()方法配置给媒体会话。

   收起

   自动换行

   深色代码主题

   复制

   ```
   1. private setLaunchAbility() {
   2. if (!this.context) {
   3. return;
   4. }
   5. let wantAgentInfo: wantAgent.WantAgentInfo = {
   6. wants: [
   7. {
   8. bundleName: this.context.abilityInfo.bundleName,
   9. abilityName: this.context.abilityInfo.name
   10. }
   11. ],
   12. operationType: wantAgent.OperationType.START_ABILITIES,
   13. requestCode: 0,
   14. wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
   15. };
   16. wantAgent.getWantAgent(wantAgentInfo).then((agent) => {
   17. if (this.AVSession) {
   18. this.AVSession.setLaunchAbility(agent);
   19. }
   20. });
   21. }
   ```

   [AVSessionController.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/AVSessionController.ets#L81-L101)
3. 应用状态上报播控中心

   当应用内音频信息发生改变时，如用户手动在应用内操作或应用响应播控中心通知进行调整后，需要向播控中心上报应用中的音频状态数据，播控中心修改界面显示来达到播控中心与应用的状态同步，通过setAVPlaybackState()方法可以上报媒体播放状态，如暂停、播放、进度调整、循环模式、收藏状态等。

   收起

   自动换行

   深色代码主题

   复制

   ```
   1. private setFavoriteState(isFavorite: boolean) {
   2. if (this.AVSession) {
   3. this.AVSession.setAVPlaybackState({ isFavorite }, (err: BusinessError) => {
   4. if (err) {
   5. Logger.error(TAG, `SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`);
   6. } else {
   7. Logger.info(TAG, 'SetAVPlaybackState successfully');
   8. }
   9. });
   10. }
   11. }

   13. public setProgressState(ms: number) {
   14. if (this.AVSession) {
   15. this.AVSession.setAVPlaybackState({
   16. position: {
   17. elapsedTime: ms,
   18. updateTime: new Date().getTime()
   19. }
   20. }, (err: BusinessError) => {
   21. if (err) {
   22. Logger.error(TAG, `SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`);
   23. } else {
   24. Logger.info(TAG, 'SetAVPlaybackState successfully');
   25. }
   26. });
   27. }
   28. }

   30. public setPlayState(isPlay: boolean) {
   31. if (this.AVSession) {
   32. this.AVSession.setAVPlaybackState({
   33. state: isPlay ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
   34. }, (err: BusinessError) => {
   35. if (err) {
   36. Logger.error(TAG, `SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`);
   37. } else {
   38. Logger.info(TAG, 'SetAVPlaybackState successfully');
   39. }
   40. });
   41. }
   42. }

   44. // ...
   45. public async setLoopModeState(AVSessionLoopMode: avSession.LoopMode) {
   46. if (this.AVSession) {
   47. this.AVSession.setAVPlaybackState({ loopMode: AVSessionLoopMode }, (err: BusinessError) => {
   48. if (err) {
   49. Logger.error(TAG, `SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`);
   50. } else {
   51. Logger.info(TAG, 'SetAVPlaybackState successfully');
   52. }
   53. });
   54. }
   55. }
   ```

   [AVSessionController.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/AVSessionController.ets#L281-L368)

   通过setAVMetadata()方法可以上报媒体会话元数据如标题、歌手、歌词等。

   收起

   自动换行

   深色代码主题

   复制

   ```
   1. public async setAVMetadata() {
   2. this.musicIndex = AppStorage.get('selectIndex');
   3. Logger.info(TAG, 'current musicIndex is:' + this.musicIndex);
   4. if (this.musicIndex === undefined) {
   5. this.musicIndex = 0;
   6. }
   7. try {
   8. if (this.context) {
   9. let mediaImage = await MediaTools.getPixelMapFromResource(this.context,
   10. this.songList[this.musicIndex].label as resourceManager.Resource);
   11. Logger.info(TAG, 'getPixelMapFromResource success' + JSON.stringify(mediaImage));
   12. let metadata: avSession.AVMetadata = {
   13. assetId: `${this.musicIndex}`,
   14. title: this.songList[this.musicIndex].title,
   15. artist: this.songList[this.musicIndex].singer,
   16. mediaImage: mediaImage,
   17. duration: this.getDuration(),
   18. };
   19. let lrc = await MediaTools.getLrcFromRawFile(this.context, this.songList[this.musicIndex].lyric);
   20. if (lrc) {
   21. metadata.lyric = lrc;
   22. }
   23. if (this.AVSession) {
   24. this.AVSession.setAVMetadata(metadata).then(() => {
   25. Logger.info(TAG, 'SetAVMetadata successfully');
   26. }).catch((err: BusinessError) => {
   27. Logger.error(TAG, `SetAVMetadata BusinessError: code: ${err.code}, message: ${err.message}`);
   28. });
   29. }
   30. }
   31. } catch (error) {
   32. Logger.error(TAG, `SetAVMetadata try: code: ${(error as BusinessError).code}`);
   33. }
   34. }
   ```

   [AVSessionController.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/AVSessionController.ets#L120-L153)

   注意此处上报歌词信息需要获取歌词lrc文件数据，解码成字符串后上传，具体处理如下：

   收起

   自动换行

   深色代码主题

   复制

   ```
   1. import { util } from '@kit.ArkTS';
   2. // ...
   3. export class MediaTools {
   4. // ...
   5. static async getLrcFromRawFile(context: common.UIAbilityContext, filename: string): Promise<string | undefined> {
   6. if (!filename) {
   7. return undefined;
   8. }
   9. const lyricUint8Array: Uint8Array = await context.resourceManager.getRawFileContent(filename);
   10. let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true });
   11. let resStr: string = textDecoder.decodeToString(lyricUint8Array, { stream: false });
   12. return resStr;
   13. }
   14. }
   ```

   [MediaTools.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/MediaTools.ets#L20-L79)

   适配后效果如图，应用内的歌词、播放进度、收藏状态、循环模式、歌曲信息与播控中心展示状态一致：

   ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/e8/v3/jeUmqP3ESwKcDckTvNnufg/zh-cn_image_0000002229336997.png?HW-CC-KV=V1&HW-CC-Date=20260414T060137Z&HW-CC-Expire=86400&HW-CC-Sign=BBE7D34AFE183A3440055FE357E20511EA09A8E788F443DAB4A1C2084558E529 "点击放大")

### 后台播放音乐

实现播控中心的接入后，解除了必须进入应用才能调整播放状态的限制，但应用退到后台状态后很快就会被挂起，导致歌曲中断。此时在接入AVSession的基础上还需要申请长时任务，保证歌曲能在后台一直保持播放状态，达到连贯的听歌体验，具体实现方式如下：

1. 创建后台管理类，使用BackgroundTasksKit的startBackgroundRunning()/stopBackgroundRunning()方法分别申请和取消后台运行任务，长时任务类型选择AUDIO\_PLAYBACK，表示音视频后台播放。

   收起

   自动换行

   深色代码主题

   复制

   ```
   1. import { wantAgent, common } from '@kit.AbilityKit';
   2. import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
   3. import { BusinessError } from '@kit.BasicServicesKit';
   4. import { Logger } from './Logger';

   6. const TAG = 'BackgroundUtil';

   8. export class BackgroundUtil {
   9. /**
   10. * Start background task.
   11. *
   12. * @param context
   13. */
   14. public static startContinuousTask(context?: common.UIAbilityContext): void {
   15. if (!context) {
   16. Logger.error(TAG, 'startContinuousTask failed', `context undefined`);
   17. return;
   18. }
   19. let wantAgentInfo: wantAgent.WantAgentInfo = {
   20. wants: [
   21. {
   22. bundleName: context.abilityInfo.bundleName,
   23. abilityName: context.abilityInfo.name
   24. }
   25. ],
   26. operationType: wantAgent.OperationType.START_ABILITY,
   27. requestCode: 0,
   28. wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
   29. };

   31. wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: Object) => {
   32. try {
   33. backgroundTaskManager.startBackgroundRunning(context,
   34. backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj).then(() => {
   35. Logger.info('this audioRenderer: ', 'startBackgroundRunning succeeded');
   36. }).catch((error: BusinessError) => {
   37. Logger.error('this audioRenderer: ', `startBackgroundRunning failed Cause: code ${error.code}`);
   38. });
   39. } catch (error) {
   40. Logger.error(TAG, `startBackgroundRunning failed.message ${(error as BusinessError).message}`);
   41. }
   42. });
   43. }

   45. /**
   46. * Stop background task.
   47. *
   48. * @param context
   49. */
   50. public static stopContinuousTask(context: common.UIAbilityContext): void {
   51. try {
   52. backgroundTaskManager.stopBackgroundRunning(context).then(() => {
   53. Logger.info('this audioRenderer: ', 'stopBackgroundRunning succeeded');
   54. }).catch((error: BusinessError) => {
   55. Logger.error('this audioRenderer: ', `stopBackgroundRunning failed Cause: code ${error.code}`);
   56. });
   57. } catch (error) {
   58. Logger.error(TAG, `stopBackgroundRunning failed. message ${(error as BusinessError).message}`);
   59. }
   60. }
   61. }
   ```

   [BackgroundUtil.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/BackgroundUtil.ets#L16-L76)
2. 在音乐播放时申请长时任务，达到默认支持后台播放的效果。

   收起

   自动换行

   深色代码主题

   复制

   ```
   1. /**
   2. * Play music by index.
   3. *
   4. * @param musicIndex
   5. */
   6. async play(musicIndex: number = this.musicIndex) {
   7. if (!this.audioRenderer) {
   8. return;
   9. }
   10. if (musicIndex >= this.songList.length) {
   11. Logger.error(TAG, `current musicIndex ${musicIndex}`);
   12. return;
   13. }
   14. BackgroundUtil.startContinuousTask(this.context);
   15. this.updateMusicIndex(musicIndex);
   16. if (this.isFirst) {
   17. this.isFirst = false;
   18. await this.loadSongAssent();
   19. await this.stop();
   20. await this.start();
   21. } else {
   22. await this.stop();
   23. this.updateIsPlay(false);
   24. await this.reset();
   25. }
   26. }

   28. // ...
   29. /**
   30. * start music.
   31. */
   32. public async start() {
   33. if (this.audioRenderer) {
   34. try {
   35. await this.audioRenderer.start().catch((err: BusinessError) => {
   36. Logger.error(TAG, `start failed,code is ${err.code},message is ${err.message}`);
   37. })
   38. this.updateIsPlay(true);
   39. BackgroundUtil.startContinuousTask(this.context);
   40. Logger.info(TAG, 'start success');
   41. } catch (e) {
   42. Logger.error(TAG, `start failed,audioRenderer is undefined`);
   43. }
   44. }
   45. }
   ```

   [AudioRendererController.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/AudioRendererController.ets#L266-L376)
3. 在音频渲染器释放的时候取消长时任务。

   收起

   自动换行

   深色代码主题

   复制

   ```
   1. public async release() {
   2. if (this.audioRenderer && this.context) {
   3. try {
   4. await this.audioRenderer.release().catch((err: BusinessError) => {
   5. Logger.error(TAG, `release failed,code is ${err.code},message is ${err.message}`);
   6. })
   7. this.avSessionController?.unregisterSessionListener();
   8. BackgroundUtil.stopContinuousTask(this.context);
   9. Logger.info(TAG, 'release success');
   10. } catch (e) {
   11. Logger.error(TAG, `release failed,audioRenderer is undefined`);
   12. }
   13. }
   14. }
   ```

   [AudioRendererController.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/AudioRendererController.ets#L523-L537)

## 与播放设备交互

### 播放设备状态发生改变

当应用连接的播放设备状态发生改变时，需要及时做出对应的处理，从音频流输出目标的切换、音频播放状态的改变到应用界面的更新，都应该与系统行为以及用户直觉相符，来保证交互体验的一致性，以下分别对各种变更场景给出适配方案：

1. 新设备可用

   新设备上线时，会触发播放设备自动切换，切换的优先级按下图所示，个人类设备高于公共类设备，同一优先级遵循后入优先原则。因此如果一开始通过扬声器播放，接入耳机后会自动切换至耳机播放，无需应用主动切换。

   ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/18/v3/xF_CcGShRJOuhGy6ZHxn8g/zh-cn_image_0000002229336993.png?HW-CC-KV=V1&HW-CC-Date=20260414T060137Z&HW-CC-Expire=86400&HW-CC-Sign=4393FE630FBE1C764FDE6B4BD3E43829406FA6832F8D2B7AAB350FDCB5A5E1FB "点击放大")

   应用可以通过监听outputDeviceChangeWithInfo事件来感知外放设备的切换，新设备上线对应的变更原因是AudioStreamDeviceChangeReason.REASON\_NEW\_DEVICE\_AVAILABLE，如上所述这种情况切换到新的设备播放的行为是系统行为，应用不需要主动改变输出设备；如果有特殊业务逻辑如接入新设备时暂停播放或者对新接入设备进行管理等，可在此类回调中进行处理。
2. 旧设备不可用

   正在发声的设备不可用后（有线耳机断开、蓝牙开关关闭、蓝牙耳机入盒、多连接耳机被抢占等），应用需要根据使用场景选择暂停播放或使用新设备继续播放。在此列出一些常用类型场景的处理建议，应用可以根据需要做出调整。

   展开

   | 类型 | 处理建议 |
   | --- | --- |
   | 游戏场景 | 不进行暂停 |
   | 音乐场景 | 根据回调进行暂停 |
   | 听书场景 | 根据回调进行暂停 |
   | 视频场景 | 根据回调进行暂停 |

   应用同样可以监听设备变更事件并对AudioStreamDeviceChangeReason.REASON\_OLD\_DEVICE\_UNAVAILABLE的场景进行处理，在回调中根据当前播放设备类型选择后续行为，例如为扬声器时暂停，为其他设备则继续播放。

   注意具有佩戴检测能力的蓝牙耳机入耳、摘下时会通过播控中心分别向应用发送播放和暂停的通知，应用可以通过监听播控中心指令而非设备变更事件来适配此类操作。
3. 用户强选

   当用户通过[投播组件](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ohos-multimedia-avcastpicker#avcastpicker)主动切换输出设备时，系统会自动切换到新选择的设备播放音频，应用可以对变更原因为AudioStreamDeviceChangeReason.REASON\_OVERRODE的事件处理自己的业务逻辑，接入投播组件可以参考[主动切换播放设备](/consumer/cn/doc/best-practices/bpta-audio-interaction-practice#section17613152517110)。

示例代码如下，分别对各种类型的设备状态改变事件做出对应的处理：

收起

自动换行

深色代码主题

复制

```
1. private setOutputDeviceChangeCallback() {
2. if (!this.audioRenderer) {
3. return;
4. }
5. this.audioRenderer.on('outputDeviceChangeWithInfo', this.outputDeviceChangeCallback);
6. }

8. private outputDeviceChangeCallback: (deviceChangeInfo: audio.AudioStreamDeviceChangeInfo) => void =
9. (deviceChangeInfo: audio.AudioStreamDeviceChangeInfo) => {
10. Logger.info(TAG, `DeviceInfo id: ${deviceChangeInfo.devices[0].id}`);
11. Logger.info(TAG, `DeviceInfo name: ${deviceChangeInfo.devices[0].name}`);
12. Logger.info(TAG, `DeviceInfo address: ${deviceChangeInfo.devices[0].address}`);
13. Logger.info(TAG, `Device change reason: ${deviceChangeInfo.changeReason}`);
14. if (deviceChangeInfo.changeReason === audio.AudioStreamDeviceChangeReason.REASON_OLD_DEVICE_UNAVAILABLE) {
15. Logger.info(TAG, `Device change reason: ${deviceChangeInfo.changeReason}`);
16. this.pause();
17. }
18. }
```

[AudioRendererController.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/AudioRendererController.ets#L189-L207)

### 响应播放设备指令

通过有线耳机、蓝牙耳机实现对音频的播放、暂停、上一首、下一首等基本操作，需要应用接入播控中心，并注册播控命令事件监听，以便响应播控中心下发的命令。应用通过蓝牙耳机发送指令与直接点击播控中心控制音频机制相同，因此接入播控中心监听播控指令即可，具体接入过程可参考[播控中心控制音乐状态](/consumer/cn/doc/best-practices/bpta-audio-interaction-practice#section1245419358228)。

### 用户主动切换播放设备

应用内可以提供主动切换播放设备的能力，来提升交互的灵活性，此时需要接入AVCastPicker组件，在用户界面点击此投播组件并在弹框中选择目标设备即可切换设备播放，接入指导可参考[投播组件开发指导](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/distributed-playback-guide)。

收起

自动换行

深色代码主题

复制

```
1. import { AVCastPicker } from '@kit.AVSessionKit';
2. import { SongItem } from '@ohos/MediaService';
3. import { StyleConstants } from '../common/constants/StyleConstants';
4. import { BreakpointType } from '../common/utils/BreakpointSystem';

6. @Component
7. export struct TopAreaComponent {
8. @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm';
9. @StorageLink('songList') songList: SongItem[] = [];
10. @StorageProp('selectIndex') @Watch('getIsDark') selectIndex: number = 0;
11. @State isDark: boolean = true;
12. @State color: Color = Color.White

14. getIsDark() {
15. this.isDark = this.songList[this.selectIndex].isDarkBackground;
16. this.color = this.isDark ? Color.White : Color.Black
17. }

19. build() {
20. Row() {
21. AVCastPicker({ normalColor: this.color, activeColor: this.color })
22. .width(new BreakpointType({
23. sm: $r('app.float.common_image'),
24. md: $r('app.float.common_image'),
25. lg: $r('app.float.control_image_lg')
26. }).getValue(this.currentBreakpoint))
27. .height(new BreakpointType({
28. sm: $r('app.float.common_image'),
29. md: $r('app.float.common_image'),
30. lg: $r('app.float.control_image_lg')
31. }).getValue(this.currentBreakpoint))
32. }
33. .height($r('app.float.info_margin_top_sm'))
34. .width(StyleConstants.FULL_WIDTH)
35. .justifyContent(FlexAlign.End)
36. }
37. }
```

[TopAreaComponent.ets](https://gitcode.com/harmonyos_samples/audio-interaction/blob/master/entry/src/main/ets/components/TopAreaComponent.ets#L17-L53)

将投播组件布局在页面的合理位置，点击选择音频输出设备，如图所示可以从本机扬声器切换到已连接的蓝牙耳机：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/ee/v3/dTZKWnnmThy9QtSm52l46w/zh-cn_image_0000002193851600.gif?HW-CC-KV=V1&HW-CC-Date=20260414T060137Z&HW-CC-Expire=86400&HW-CC-Sign=0C6F4E46C9110957FB0630E2CE792627CCA7DCBB3D1E14E3CAAB9F183300DDE2 "点击放大")

## 与其他应用交互

### 与其他应用音频冲突

当与其他应用的音频发生冲突，多个音频流同时播放时，系统预设了音频打断策略，对多音频的并发进行管控，只有持有音频焦点的音频流才可以正常播放，避免多个音频流无序并发播放的现象出现。为了维持应用和系统的状态一致性，保证符合用户直觉的交互体验，推荐应用[处理音频焦点变化](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/audio-playback-concurrency#处理音频焦点变化)，并在收到音频打断事件（[InterruptEvent](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-i#interruptevent9)）时做出相应处理。对于音乐类应用，被各种[处理音频焦点变化](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/audio-playback-concurrency#处理音频焦点变化)类型的其他应用音频打断场景和效果如下表所示：

展开

| 先播应用音频类型 | 后播应用音频类型 | 打断效果 |
| --- | --- | --- |
| 音乐 | 闹钟 | 后播应用播放时，先播应用暂停播放；后播应用停止播放后，先播应用恢复播放 |
| 电话 |
| 铃声 |
| VOIP 铃声(全屏呼叫/呼叫页面/横幅呼叫) |
| VOIP 通话 |
| VOIP MESSAGE(微信语音/畅联) |
| 导航 | 后播应用播放时，先播应用降低音量持续播放；后播应用停止播放后，先播应用恢复音量继续播放 |
| TextReader控件朗读语音 |
| 语音助手类短语音 |
| 音乐 | 后播应用播放时，先播应用停止播放；后播应用停止播放后，先播应用不再恢复播放 |
| 视频 |
| 游戏 | 先播、后播应用并发混音播放 |
| 系统音效(锁屏/按键) |

说明

移动端与电脑端的音频焦点在系统默认策略上存在差异。在移动端，默认策略为新应用播放时会中断或降低先前应用的音量，以避免同时大声播放。例如，用户在听音乐时打开短视频App，音乐将自动暂停或音量显著降低，确保短视频声音清晰。而在电脑端，默认策略允许多个应用的音频同时输出，音量叠加。例如，用户可以一边听音乐，一边观看游戏直播，通过调整不同窗口的音量来实现平衡。

如表所示，根据应用打断效果主要可以分为四个类型：暂停后恢复、降低音量后恢复、停止后不恢复和并发播放，以下分别对这四类场景给出适配方案：

1. 暂停后恢复场景

   后播音频类型为闹钟/电话/铃声/VOIP音频时，先播音频暂停播放；待后播音频播放完毕后，先播音频恢复播放。应用可以注册焦点事件监听，当此类打断事件发生时，系统会自动暂停先播音频，应用侧会接收到INTERRUPT\_HINT\_PAUSE事件，此时只需更新播放和UI界面状态为暂停态即可，不需要主动停止音频流。当后播音频结束后，应用接收到INTERRUPT\_HINT\_RESUME事件，此时系统不会主动继续播放先播音频，应用需主动调用播放方法续播音频流。
2. 降低音量后恢复场景

   当后播应用音频类型为导航/TextReader语音/语音助手类语音播报时，先播音频应降低音量持续播放；后播音频播报结束后，先播音频恢复音量继续播放。注意此类打断事件的音量降低/恢复行为是系统行为，应用无需主动调整音量。在降低音量和恢复时会分别收到INTERRUPT\_HINT\_DUCK和INTERRUPT\_HINT\_UNDUCK回调，应用可以在回调中按需更新页面状态或处理其他业务逻辑。
3. 停止后不恢复场景

   当后播应用音频类型为音乐/视频时，音乐终止播放；后播音频停止后，音乐不恢复播放。应用可以注册焦点事件监听，接收到INTERRUPT\_HINT\_STOP事件时，停止音乐播放，并更新UI界面。
4. 并发场景
   * 当后播应用音频类型为游戏或系统提示音（锁屏、按键）时，先播音频不停止，与后播音频混音并发播放。此行为是系统默认行为，应用侧不需要适配。
   * 如果是音乐音频在后台播放，前台音视频静音并发播放的场景，则需要前台应用使用[setSilentModeAndMixWithOthers()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-audiorenderer#setsilentmodeandmixwithothers12)接口开启静音并发模式，否则系统会根据音频流类型默认触发对应打断场景。当前台音视频取消静音播放时，setSilentModeAndMixWithOthers()接口关闭静音并发模式，同时使用[activateAudioSession()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-audiosessionmanager#activateaudiosession12)激活音频会话（AudioSession）并指定音频会话策略为暂停模式（CONCURRENCY\_PAUSE\_OTHERS），使后台音频处于暂停模式。当前台音频暂停后，再次开启静音并发模式时，可以使后台音频恢复播放。

综上四种情况，在audioRenderer.on('audioInterrupt')回调中分别对各种打断类型场景做出对应处理，并实现静音播放接口及设置音频会话策略为暂停模式，示例代码实现如下：

收起

自动换行

深色代码主题

复制

```
1. private setInterruptCallback() {
2. if (!this.audioRenderer) {
3. return;
4. }
5. this.audioRenderer.on('audioInterrupt', this.interruptCallback);
6. }

8. private interruptCallback: (interruptEvent: audio.InterruptEvent) => void =
9. (interruptEvent: audio.InterruptEvent) => {
10. if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_FORCE) {
11. switch (interruptEvent.hintType) {
12. case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
13. this.updateIsPlay(false);
14. break;
15. case audio.InterruptHint.INTERRUPT_HINT_STOP:
16. this.updateIsPlay(false);
17. this.pause();
18. break;
19. case audio.InterruptHint.INTERRUPT_HINT_DUCK:
20. break;
21. case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
22. break;
23. default:
24. break;
25. }
26. } else if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_SHARE) {
27. switch (interruptEvent.hintType) {
28. case audio.InterruptHint.INTERRUPT_HINT_RESUME:
29. this.start();
30. break;
31. default:
32. break;
33. }
34. }
35. }

37. // ...

39. private async setSilentModeAndMixWithOthersFromPrefer() {
40. if (!this.audioRenderer || !this.context) {
41. return;
42. }
43. let formIds: string[] = await PreferencesUtil.getInstance().getFormIds(this.context);
44. let isSupportSilent = false;
45. if (formIds.includes(SILENT_ID)) {
46. isSupportSilent = true;
47. } else {
48. isSupportSilent = false;
49. }
50. AppStorage.setOrCreate('isSilentMode', isSupportSilent);
51. this.audioRenderer.setSilentModeAndMixWithOthers(isSupportSilent);
52. }

54. public async setSilentModeAndMixWithOthers(isSupportSilent: boolean = false) {
55. if (!this.audioRenderer || !this.context) {
56. return;
57. }
58. let audioManger = audio.getAudioManager();
59. let audioSessionManager: audio.AudioSessionManager = audioManger.getSessionManager();
60. let strategy: audio.AudioSessionStrategy = {
61. concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_PAUSE_OTHERS
62. };

64. let formIds: string[] = await PreferencesUtil.getInstance().getFormIds(this.context);
65. if (isSupportSilent) {
66. if (!formIds.includes(SILENT_ID)) {
67. await PreferencesUtil.getInstance().addFormId(this.context, SILENT_ID);
68. }
69. this.audioRenderer.setSilentModeAndMixWithOthers(isSupportSilent);
70. // After using AudioSession, the system sends INTERRUPT_INT-REUME to the previously interrupted audio stream.
71. audioSessionManager.deactivateAudioSession().then(() => {
72. Logger.info(TAG, 'deactivateAudioSession SUCCESS');
73. }).catch((err: BusinessError) => {
74. Logger.error(TAG, `ERROR: ${err}`);
75. });
76. } else {
77. // AudioSession needs to be activated first, and then unmute.
78. audioSessionManager.activateAudioSession(strategy).then(() => {
79. Logger.info(TAG, 'activateAudioSession SUCCESS');
80. }).catch((err: BusinessError) => {
81. Logger.error(TAG, `ERROR: ${err}`);
82. });
83. if (formIds.includes(SILENT_ID)) {
84. await PreferencesUtil.getInstance().removeFormId(this.context, SILENT_ID);
85. }
86. this.audioRenderer.setSilentModeAndMixWithOthers(isSupportSilent);
87. }
88. AppStorage.setOrCreate('isSilentMode', isSupportSilent);
89. }
```

[AudioRendererController.ets](https://gitcode.com/HarmonyOS_Samples/audio-interaction/blob/master/MediaService/src/main/ets/utils/AudioRendererController.ets#L151-L262)

### 检测应用音频冲突适配完整性

[这里](https://gitcode.com/harmonyos_samples/HMMediaDiagnosticsTool)提供了一个音频检测应用，开发者可以用来检测自己开发的音频应用在不同场景下的功能完备度和准确性等，帮助开发者修复音频焦点类的应用功能问题。

## 示例代码

* [基于AudioRenderer的音频播控和多场景交互](https://gitcode.com/harmonyos_samples/audio-interaction)