HarmonyOS开发中的音频焦点:多应用音频协调、焦点抢占与监听全解

举报
Jack20 发表于 2026/06/20 20:36:33 2026/06/20
【摘要】 HarmonyOS开发中的音频焦点:多应用音频协调、焦点抢占与监听全解📌 核心要点:掌握 HarmonyOS 音频焦点机制——从 AudioInterrupt 焦点请求与释放,到焦点抢占策略与多应用音频协调,实现多应用共存下的优雅音频体验 一、背景与动机你一定遇到过这样的场景:正在听音乐,突然来了个电话,音乐自动暂停,接完电话后音乐又自动恢复。或者正在看视频,打开另一个视频 App,前一...

HarmonyOS开发中的音频焦点:多应用音频协调、焦点抢占与监听全解

📌 核心要点:掌握 HarmonyOS 音频焦点机制——从 AudioInterrupt 焦点请求与释放,到焦点抢占策略与多应用音频协调,实现多应用共存下的优雅音频体验


一、背景与动机

你一定遇到过这样的场景:正在听音乐,突然来了个电话,音乐自动暂停,接完电话后音乐又自动恢复。或者正在看视频,打开另一个视频 App,前一个视频的声音自动变小或暂停。这就是音频焦点在起作用。

音频焦点,说白了就是"谁有资格发声"的协调机制。手机的音频输出通道是共享的,多个应用可能同时想播放声音,但物理上只能有一个声音(或有限的混合)输出。如果没有协调机制,多个应用同时播放,用户体验就是灾难。

为什么音频焦点如此重要?

  • 用户体验底线:通话时音乐不停,这是不可接受的
  • 系统级协调:操作系统需要一套统一的规则来仲裁音频冲突
  • 应用合规性:不正确处理音频焦点的应用,可能被系统强制静音甚至杀进程
  • 多应用共存:导航提示音和音乐如何共存?语音助手和播客如何协调?
  • HarmonyOS 生态:分布式场景下,焦点协调更加复杂

二、核心原理

2.1 音频焦点机制总览

音频焦点是一种请求-授权-监听的机制:

  1. 请求焦点:应用在播放音频前,向系统声明自己的音频类型和使用场景
  2. 系统仲裁:系统根据音频类型优先级,决定是否授予焦点,以及是否打断其他应用
  3. 焦点监听:应用监听焦点变化,当被抢占时主动降低音量或暂停
    图片.png
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超时保护
      释放前取消监听
      选择正确的焦点模式
    多应用协调
      通话→暂停音乐
      导航→降低音乐音量
      通知→短暂降低
      通话结束→恢复音乐

核心要点回顾

  1. 必须注册 audioInterrupt:系统不会自动帮你暂停,必须监听中断事件并主动响应
  2. 区分暂停原因:用标志位区分"因焦点中断暂停"和"用户手动暂停",恢复时只自动恢复前者
  3. Duck/Unduck 成对处理:降低音量后要等恢复音量事件,设置超时保护防止事件丢失
  4. 选择正确的焦点模式:音乐用 SHARE_MODE,通话用 INDEPENDENT_MODE,不要搞反
  5. 资源释放要干净:释放渲染器前先取消焦点监听,避免回调访问已释放的资源
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。