HarmonyOS开发音频路由:设备管理、输出切换与蓝牙音频策略全解

举报
Jack20 发表于 2026/06/20 20:35:50 2026/06/20
【摘要】 HarmonyOS开发中的音频路由:设备管理、输出切换与蓝牙音频策略全解📌 核心要点:掌握 HarmonyOS 音频路由机制——从音频设备发现与监听,到输出设备动态切换,再到蓝牙音频路由策略,实现多设备间的无缝音频流转 一、背景与动机想象这样一个场景:你正在用手机外放听歌,突然戴上蓝牙耳机,音乐自动从耳机里流出来;摘下耳机,音乐又自动切回扬声器。再比如,你正在用蓝牙音箱放音乐,来了个电话...

HarmonyOS开发中的音频路由:设备管理、输出切换与蓝牙音频策略全解

📌 核心要点:掌握 HarmonyOS 音频路由机制——从音频设备发现与监听,到输出设备动态切换,再到蓝牙音频路由策略,实现多设备间的无缝音频流转


一、背景与动机

想象这样一个场景:你正在用手机外放听歌,突然戴上蓝牙耳机,音乐自动从耳机里流出来;摘下耳机,音乐又自动切回扬声器。再比如,你正在用蓝牙音箱放音乐,来了个电话,通话音频自动切到手机听筒。这些"音频自动流转"的体验,背后就是音频路由在起作用。

音频路由,简单来说就是决定"声音从哪里出来"。手机有扬声器、听筒、有线耳机、蓝牙耳机、蓝牙音箱……多个输出设备并存时,系统需要一套规则来决定音频走哪条路。

为什么音频路由如此重要?

  • 用户体验:插上耳机声音就自动从耳机出来,这是最基本的期望
  • 蓝牙场景:蓝牙设备的连接、断开、切换,都需要正确的路由策略
  • 多设备协同:HarmonyOS 的分布式特性,音频可能在多个设备间流转
  • 通话场景:来电时音频路由需要快速、准确地切换到听筒或蓝牙
  • 应用适配:应用需要感知当前输出设备,做出相应的 UI 和功能调整

二、核心原理

2.1 音频路由架构

HarmonyOS 的音频路由由 AudioRoutingManager 统一管理,它负责设备发现、状态监听和路由决策:

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[应用层] --> B[AudioRoutingManager]
    B --> C[设备发现]
    B --> D[设备监听]
    B --> E[路由决策]
    B --> F[设备切换]

    C --> C1[getDevices]
    C --> C2[设备类型识别]

    D --> D1[on'deviceChange']
    D --> D2[连接/断开事件]

    E --> E1[音频流类型]
    E --> E2[设备优先级]
    E --> E3[系统策略]

    F --> F1[selectOutputDevice]
    F --> F2[蓝牙A2DP切换]
    F --> F3[分布式流转]

    G[输出设备] --> H[扬声器 SPEAKER]
    G --> I[有线耳机 WIRED_HEADSET]
    G --> J[蓝牙A2DP BLUETOOTH_A2DP]
    G --> K[蓝牙SCO BLUETOOTH_SCO]
    G --> L[听筒 EARPIECE]
    G --> M[分布式设备 DISTRIBUTED]

    class A primary
    class B info
    class C primary
    class D warning
    class E purple
    class F error
    class G primary

2.2 音频设备类型与优先级

HarmonyOS 定义了多种音频设备类型,系统有一套默认的优先级规则:

优先级 设备类型 枚举值 说明
1(最高) 蓝牙 SCO BLUETOOTH_SCO 通话专用蓝牙通道
2 有线耳机 WIRED_HEADSET 3.5mm 或 USB-C 耳机
3 蓝牙 A2DP BLUETOOTH_A2DP 蓝牙耳机/音箱(媒体音频)
4 听筒 EARPIECE 通话听筒
5(最低) 扬声器 SPEAKER 外放扬声器

默认路由策略:系统会自动选择当前已连接的优先级最高的设备。比如同时插着有线耳机和连着蓝牙耳机,声音会从有线耳机出来。

2.3 蓝牙音频路由

蓝牙音频路由是最复杂的场景,因为蓝牙有两种音频协议:

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[蓝牙设备] --> B{协议类型}
    B -->|A2DP| C[高级音频分发协议]
    B -->|SCO| D[同步面向连接协议]

    C --> C1[音乐播放]
    C --> C2[视频音频]
    C --> C3[游戏音效]
    C --> C4[高质量立体声]

    D --> D1[语音通话]
    D --> D2[语音识别]
    D --> D3[低延迟单声道]

    class A primary
    class B warning
    class C info
    class D error

A2DP vs SCO

特性 A2DP SCO
用途 媒体音频(音乐、视频) 语音通话
音质 高(AAC/SBC 编码) 低(CVSD/msBC 编码)
延迟 较高
声道 立体声 单声道
方向 单向(播放) 双向(播放+录音)

三、代码实战

3.1 音频设备发现与监听

实现一个完整的音频设备管理面板,实时显示已连接设备,监听设备变化:

import { audio } from '@kit.AudioKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 音频设备信息模型
interface AudioDeviceInfo {
  id: number;
  deviceType: audio.DeviceType;
  deviceName: string;
  address: string;
  isConnected: boolean;
  icon: string;
}

@Entry
@Component
struct AudioDeviceManager {
  // 路由管理器
  private routingManager: audio.AudioRoutingManager | null = null;
  // 音频管理器
  private audioManager: audio.AudioManager | null = null;

  // 设备列表
  @State outputDevices: AudioDeviceInfo[] = [];
  @State activeDevice: AudioDeviceInfo | null = null;
  @State statusText: string = '未初始化';

  aboutToAppear(): void {
    this.initAudioManager();
  }

  aboutToDisappear(): void {
    this.removeDeviceListener();
  }

  /**
   * 初始化音频管理器
   */
  async initAudioManager(): Promise<void> {
    try {
      // 获取音频管理器
      this.audioManager = audio.getAudioManager();

      // 获取路由管理器
      this.routingManager = this.audioManager.getRoutingManager();

      // 加载当前设备列表
      await this.loadDevices();

      // 注册设备变化监听
      this.registerDeviceListener();

      this.statusText = '设备监听已启动';
      console.info('[路由] 初始化成功');

    } catch (err) {
      const error = err as BusinessError;
      console.error(`[路由] 初始化失败: ${error.message}`);
      this.statusText = '初始化失败';
    }
  }

  /**
   * 加载当前已连接的输出设备
   */
  async loadDevices(): Promise<void> {
    if (!this.routingManager) return;

    try {
      // 获取所有输出设备
      const devices = this.routingManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG);
      const deviceList: AudioDeviceInfo[] = [];

      for (const device of devices) {
        deviceList.push({
          id: device.id,
          deviceType: device.deviceType,
          deviceName: this.getDeviceTypeName(device.deviceType),
          address: device.address || '',
          isConnected: true, // getDevices 返回的都是已连接的
          icon: this.getDeviceIcon(device.deviceType)
        });
      }

      this.outputDevices = deviceList;

      // 获取当前活跃设备
      const activeDevices = this.routingManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG);
      if (activeDevices.length > 0) {
        this.activeDevice = {
          id: activeDevices[0].id,
          deviceType: activeDevices[0].deviceType,
          deviceName: this.getDeviceTypeName(activeDevices[0].deviceType),
          address: activeDevices[0].address || '',
          isConnected: true,
          icon: this.getDeviceIcon(activeDevices[0].deviceType)
        };
      }

    } catch (err) {
      const error = err as BusinessError;
      console.error(`[路由] 加载设备失败: ${error.message}`);
    }
  }

  /**
   * 注册设备变化监听
   */
  registerDeviceListener(): void {
    if (!this.routingManager) return;

    this.routingManager.on('deviceChange', audio.DeviceFlag.OUTPUT_DEVICES_FLAG, (deviceChanged: audio.DeviceChangeAction) => {
      const type = deviceChanged.type;  // 0=连接, 1=断开
      const device = deviceChanged.device;

      if (type === audio.DeviceChangeType.CONNECT) {
        console.info(`[路由] 设备连接: ${this.getDeviceTypeName(device.deviceType)}`);
        promptAction.showToast({
          message: `${this.getDeviceIcon(device.deviceType)} ${this.getDeviceTypeName(device.deviceType)} 已连接`
        });
      } else if (type === audio.DeviceChangeType.DISCONNECT) {
        console.info(`[路由] 设备断开: ${this.getDeviceTypeName(device.deviceType)}`);
        promptAction.showToast({
          message: `${this.getDeviceIcon(device.deviceType)} ${this.getDeviceTypeName(device.deviceType)} 已断开`
        });
      }

      // 刷新设备列表
      this.loadDevices();
    });
  }

  /**
   * 移除设备监听
   */
  removeDeviceListener(): void {
    if (!this.routingManager) return;

    this.routingManager.off('deviceChange', audio.DeviceFlag.OUTPUT_DEVICES_FLAG);
  }

  /**
   * 获取设备类型名称
   */
  getDeviceTypeName(type: audio.DeviceType): string {
    const names: Record<number, string> = {
      [audio.DeviceType.SPEAKER]: '扬声器',
      [audio.DeviceType.WIRED_HEADSET]: '有线耳机',
      [audio.DeviceType.WIRED_HEADPHONES]: '有线耳机(无麦克风)',
      [audio.DeviceType.BLUETOOTH_SCO]: '蓝牙通话',
      [audio.DeviceType.BLUETOOTH_A2DP]: '蓝牙音频',
      [audio.DeviceType.EARPIECE]: '听筒',
      [audio.DeviceType.USB_HEADSET]: 'USB 耳机',
      [audio.DeviceType.DISTRIBUTED]: '分布式设备',
    };
    return names[type] || `未知设备(${type})`;
  }

  /**
   * 获取设备图标
   */
  getDeviceIcon(type: audio.DeviceType): string {
    const icons: Record<number, string> = {
      [audio.DeviceType.SPEAKER]: '🔊',
      [audio.DeviceType.WIRED_HEADSET]: '🎧',
      [audio.DeviceType.WIRED_HEADPHONES]: '🎧',
      [audio.DeviceType.BLUETOOTH_SCO]: '📞',
      [audio.DeviceType.BLUETOOTH_A2DP]: '📶',
      [audio.DeviceType.EARPIECE]: '📱',
      [audio.DeviceType.USB_HEADSET]: '🔌',
      [audio.DeviceType.DISTRIBUTED]: '🌐',
    };
    return icons[type] || '❓';
  }

  build() {
    Column({ space: 16 }) {
      // 标题
      Text('🔌 音频设备管理')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      Text(this.statusText)
        .fontSize(14)
        .fontColor('#888')

      // 当前活跃设备
      if (this.activeDevice) {
        Column({ space: 8 }) {
          Text('当前输出')
            .fontSize(14)
            .fontColor('#888')
            .width('100%')

          Row({ space: 12 }) {
            Text(this.activeDevice.icon)
              .fontSize(32)

            Column({ space: 4 }) {
              Text(this.activeDevice.deviceName)
                .fontSize(18)
                .fontWeight(FontWeight.Bold)

              if (this.activeDevice.address) {
                Text(this.activeDevice.address)
                  .fontSize(12)
                  .fontColor('#888')
              }
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
          }
          .width('100%')
          .padding(16)
          .borderRadius(12)
          .backgroundColor('#252540')
        }
        .width('90%')
      }

      // 所有已连接设备
      Text('已连接设备')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .width('90%')

      if (this.outputDevices.length === 0) {
        Text('暂无已连接设备')
          .fontSize(14)
          .fontColor('#888')
      } else {
        List({ space: 8 }) {
          ForEach(this.outputDevices, (device: AudioDeviceInfo) => {
            ListItem() {
              Row({ space: 12 }) {
                Text(device.icon)
                  .fontSize(28)

                Column({ space: 4 }) {
                  Text(device.deviceName)
                    .fontSize(16)
                    .fontWeight(
                      this.activeDevice?.id === device.id ? FontWeight.Bold : FontWeight.Normal
                    )
                    .fontColor(
                      this.activeDevice?.id === device.id ? '#4CAF50' : '#fff'
                    )

                  Text(`ID: ${device.id}`)
                    .fontSize(12)
                    .fontColor('#888')
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)

                // 当前活跃标记
                if (this.activeDevice?.id === device.id) {
                  Text('✓ 活跃')
                    .fontSize(12)
                    .fontColor('#4CAF50')
                }
              }
              .width('100%')
              .padding(12)
              .borderRadius(8)
              .backgroundColor(
                this.activeDevice?.id === device.id ? 'rgba(76,175,80,0.1)' : '#252540'
              )
            }
          }, (device: AudioDeviceInfo) => `${device.id}`)
        }
        .width('90%')
        .height(300)
      }

      // 刷新按钮
      Button('🔄 刷新设备列表')
        .width(200)
        .backgroundColor('#2196F3')
        .onClick(() => this.loadDevices())
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .padding(20)
    .backgroundColor('#1a1a2e')
  }
}

3.2 输出设备动态切换

实现音频输出设备的手动切换功能,支持在扬声器、蓝牙、有线耳机之间自由切换:

import { audio } from '@kit.AudioKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct OutputDeviceSwitcher {
  // 路由管理器
  private routingManager: audio.AudioRoutingManager | null = null;
  // 音频渲染器
  private audioRenderer: audio.AudioRenderer | null = null;

  // 可用输出设备
  @State availableDevices: audio.AudioDeviceDescriptors = [];
  // 当前选中设备
  @State selectedDeviceId: number = -1;
  // 切换状态
  @State isSwitching: boolean = false;
  @State switchResult: string = '';

  aboutToAppear(): void {
    this.init();
  }

  aboutToDisappear(): void {
    this.release();
  }

  /**
   * 初始化
   */
  async init(): Promise<void> {
    try {
      const audioManager = audio.getAudioManager();
      this.routingManager = audioManager.getRoutingManager();

      // 创建音频渲染器(用于测试切换效果)
      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);
      await this.audioRenderer.start();

      // 加载设备列表
      this.refreshDevices();

      // 监听设备变化
      this.routingManager.on('deviceChange', audio.DeviceFlag.OUTPUT_DEVICES_FLAG, () => {
        this.refreshDevices();
      });

    } catch (err) {
      const error = err as BusinessError;
      console.error(`[切换] 初始化失败: ${error.message}`);
    }
  }

  /**
   * 刷新可用设备列表
   */
  refreshDevices(): void {
    if (!this.routingManager) return;

    try {
      this.availableDevices = this.routingManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG);

      // 默认选中第一个设备
      if (this.selectedDeviceId === -1 && this.availableDevices.length > 0) {
        this.selectedDeviceId = this.availableDevices[0].id;
      }
    } catch (err) {
      console.error('[切换] 刷新设备失败');
    }
  }

  /**
   * 切换输出设备
   */
  async switchOutputDevice(deviceId: number): Promise<void> {
    if (!this.routingManager || !this.audioRenderer) return;

    this.isSwitching = true;
    this.switchResult = '';

    try {
      // 找到目标设备
      const targetDevice = this.availableDevices.find(d => d.id === deviceId);
      if (!targetDevice) {
        this.switchResult = '❌ 设备未找到';
        return;
      }

      // 方式一:通过路由管理器全局切换
      await this.routingManager.selectOutputDevice(targetDevice);

      this.selectedDeviceId = deviceId;
      this.switchResult = `✅ 已切换到 ${this.getDeviceTypeName(targetDevice.deviceType)}`;
      console.info(`[切换] 输出设备: ${this.getDeviceTypeName(targetDevice.deviceType)}`);

    } catch (err) {
      const error = err as BusinessError;
      this.switchResult = `❌ 切换失败: ${error.message}`;
      console.error(`[切换] 失败: ${error.message}`);
    } finally {
      this.isSwitching = false;
    }
  }

  /**
   * 通过渲染器切换(仅影响当前渲染器的输出)
   */
  async switchRendererOutputDevice(deviceId: number): Promise<void> {
    if (!this.audioRenderer) return;

    this.isSwitching = true;

    try {
      const targetDevice = this.availableDevices.find(d => d.id === deviceId);
      if (!targetDevice) return;

      // 方式二:通过渲染器切换(只影响当前流)
      await this.audioRenderer.selectOutputDevice([targetDevice]);

      this.selectedDeviceId = deviceId;
      this.switchResult = `✅ 渲染器输出已切换`;
      console.info('[切换] 渲染器输出设备已切换');

    } catch (err) {
      const error = err as BusinessError;
      this.switchResult = `❌ 切换失败: ${error.message}`;
    } finally {
      this.isSwitching = false;
    }
  }

  /**
   * 获取设备类型名称
   */
  getDeviceTypeName(type: audio.DeviceType): string {
    const names: Record<number, string> = {
      [audio.DeviceType.SPEAKER]: '扬声器',
      [audio.DeviceType.WIRED_HEADSET]: '有线耳机',
      [audio.DeviceType.BLUETOOTH_A2DP]: '蓝牙音频',
      [audio.DeviceType.BLUETOOTH_SCO]: '蓝牙通话',
      [audio.DeviceType.EARPIECE]: '听筒',
    };
    return names[type] || '未知';
  }

  /**
   * 获取设备图标
   */
  getDeviceIcon(type: audio.DeviceType): string {
    const icons: Record<number, string> = {
      [audio.DeviceType.SPEAKER]: '🔊',
      [audio.DeviceType.WIRED_HEADSET]: '🎧',
      [audio.DeviceType.BLUETOOTH_A2DP]: '📶',
      [audio.DeviceType.BLUETOOTH_SCO]: '📞',
      [audio.DeviceType.EARPIECE]: '📱',
    };
    return icons[type] || '❓';
  }

  /**
   * 释放资源
   */
  async release(): Promise<void> {
    try {
      this.routingManager?.off('deviceChange', audio.DeviceFlag.OUTPUT_DEVICES_FLAG);
      await this.audioRenderer?.release();
    } catch (err) {
      console.error('[切换] 释放失败');
    }
  }

  build() {
    Column({ space: 16 }) {
      Text('🔀 输出设备切换')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      // 切换结果
      if (this.switchResult) {
        Text(this.switchResult)
          .fontSize(14)
          .fontColor(this.switchResult.startsWith('✅') ? '#4CAF50' : '#F44336')
          .padding(8)
          .borderRadius(8)
          .backgroundColor('#252540')
      }

      // 设备列表
      Text('选择输出设备')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .width('90%')

      List({ space: 8 }) {
        ForEach(this.availableDevices, (device: audio.AudioDeviceDescriptor) => {
          ListItem() {
            Row({ space: 12 }) {
              Text(this.getDeviceIcon(device.deviceType))
                .fontSize(28)

              Column({ space: 4 }) {
                Text(this.getDeviceTypeName(device.deviceType))
                  .fontSize(16)
                  .fontWeight(
                    this.selectedDeviceId === device.id ? FontWeight.Bold : FontWeight.Normal
                  )
                  .fontColor(
                    this.selectedDeviceId === device.id ? '#4CAF50' : '#fff'
                  )

                Text(`ID: ${device.id} | 地址: ${device.address || '内置'}`)
                  .fontSize(12)
                  .fontColor('#888')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)

              // 选中标记
              Radio({ value: `${device.id}`, group: 'outputDevice' })
                .checked(this.selectedDeviceId === device.id)
                .onChange((isChecked: boolean) => {
                  if (isChecked) {
                    this.switchOutputDevice(device.id);
                  }
                })
            }
            .width('100%')
            .padding(12)
            .borderRadius(8)
            .backgroundColor(
              this.selectedDeviceId === device.id ? 'rgba(76,175,80,0.1)' : '#252540'
            )
          }
        }, (device: audio.AudioDeviceDescriptor) => `${device.id}`)
      }
      .width('90%')
      .layoutWeight(1)

      // 刷新按钮
      Button('🔄 刷新')
        .width(120)
        .onClick(() => this.refreshDevices())
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .padding(20)
    .backgroundColor('#1a1a2e')
  }
}

3.3 蓝牙音频路由策略管理

针对蓝牙场景的复杂路由需求,实现一个策略化的蓝牙音频路由管理器:

import { audio } from '@kit.AudioKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 蓝牙音频路由策略
enum BluetoothRouteStrategy {
  AUTO = 'auto',           // 自动(系统默认)
  A2DP_ONLY = 'a2dp',      // 仅 A2DP(强制媒体音频走蓝牙)
  SCO_ONLY = 'sco',        // 仅 SCO(强制通话音频走蓝牙)
  SPEAKER_FALLBACK = 'spk' // 蓝牙不可用时回退到扬声器
}

@Entry
@Component
struct BluetoothRouteManager {
  // 路由管理器
  private routingManager: audio.AudioRoutingManager | null = null;

  // 蓝牙设备列表
  @State bluetoothDevices: audio.AudioDeviceDescriptors = [];
  // 当前策略
  @State currentStrategy: BluetoothRouteStrategy = BluetoothRouteStrategy.AUTO;
  // 蓝牙连接状态
  @State isBluetoothConnected: boolean = false;
  @State connectedDeviceName: string = '';
  // 当前活跃的蓝牙设备类型
  @State activeBtType: string = '无';
  // 状态日志
  @State routeLogs: string[] = [];

  aboutToAppear(): void {
    this.initRouteManager();
  }

  aboutToDisappear(): void {
    this.cleanup();
  }

  /**
   * 初始化路由管理器
   */
  async initRouteManager(): Promise<void> {
    try {
      const audioManager = audio.getAudioManager();
      this.routingManager = audioManager.getRoutingManager();

      // 初始加载蓝牙设备
      this.refreshBluetoothDevices();

      // 监听设备变化
      this.routingManager.on('deviceChange', audio.DeviceFlag.OUTPUT_DEVICES_FLAG, (action: audio.DeviceChangeAction) => {
        const device = action.device;
        const isBt = device.deviceType === audio.DeviceType.BLUETOOTH_A2DP ||
                     device.deviceType === audio.DeviceType.BLUETOOTH_SCO;

        if (isBt) {
          if (action.type === audio.DeviceChangeType.CONNECT) {
            this.addLog(`🔵 蓝牙设备已连接: ${this.getDeviceTypeName(device.deviceType)}`);
            this.isBluetoothConnected = true;
            this.applyStrategy(this.currentStrategy);
          } else {
            this.addLog(`⚪ 蓝牙设备已断开: ${this.getDeviceTypeName(device.deviceType)}`);
            this.isBluetoothConnected = false;
            this.handleBluetoothDisconnect();
          }
          this.refreshBluetoothDevices();
        }
      });

      this.addLog('✅ 蓝牙路由管理器已启动');

    } catch (err) {
      const error = err as BusinessError;
      console.error(`[蓝牙路由] 初始化失败: ${error.message}`);
    }
  }

  /**
   * 刷新蓝牙设备列表
   */
  refreshBluetoothDevices(): void {
    if (!this.routingManager) return;

    const allDevices = this.routingManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG);
    this.bluetoothDevices = allDevices.filter(d =>
      d.deviceType === audio.DeviceType.BLUETOOTH_A2DP ||
      d.deviceType === audio.DeviceType.BLUETOOTH_SCO
    );

    this.isBluetoothConnected = this.bluetoothDevices.length > 0;
    if (this.bluetoothDevices.length > 0) {
      this.connectedDeviceName = this.getDeviceTypeName(this.bluetoothDevices[0].deviceType);
      this.activeBtType = this.bluetoothDevices[0].deviceType === audio.DeviceType.BLUETOOTH_A2DP ? 'A2DP' : 'SCO';
    } else {
      this.connectedDeviceName = '';
      this.activeBtType = '无';
    }
  }

  /**
   * 应用蓝牙路由策略
   */
  async applyStrategy(strategy: BluetoothRouteStrategy): Promise<void> {
    if (!this.routingManager) return;

    this.currentStrategy = strategy;

    try {
      switch (strategy) {
        case BluetoothRouteStrategy.AUTO:
          // 自动模式:让系统决定路由
          this.addLog('📋 策略: 自动(系统默认)');
          // 不做任何干预,系统自动选择最优设备
          break;

        case BluetoothRouteStrategy.A2DP_ONLY:
          // 强制 A2DP:找到 A2DP 设备并切换
          if (this.bluetoothDevices.length > 0) {
            const a2dpDevice = this.bluetoothDevices.find(d =>
              d.deviceType === audio.DeviceType.BLUETOOTH_A2DP
            );
            if (a2dpDevice) {
              await this.routingManager.selectOutputDevice(a2dpDevice);
              this.addLog('📋 策略: 强制A2DP,已切换到蓝牙音频');
            } else {
              this.addLog('⚠️ 无A2DP设备可用');
            }
          }
          break;

        case BluetoothRouteStrategy.SCO_ONLY:
          // 强制 SCO:找到 SCO 设备并切换
          if (this.bluetoothDevices.length > 0) {
            const scoDevice = this.bluetoothDevices.find(d =>
              d.deviceType === audio.DeviceType.BLUETOOTH_SCO
            );
            if (scoDevice) {
              await this.routingManager.selectOutputDevice(scoDevice);
              this.addLog('📋 策略: 强制SCO,已切换到蓝牙通话');
            } else {
              this.addLog('⚠️ 无SCO设备可用');
            }
          }
          break;

        case BluetoothRouteStrategy.SPEAKER_FALLBACK:
          // 蓝牙不可用时回退到扬声器
          if (!this.isBluetoothConnected) {
            const speaker = this.routingManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG)
              .find(d => d.deviceType === audio.DeviceType.SPEAKER);
            if (speaker) {
              await this.routingManager.selectOutputDevice(speaker);
              this.addLog('📋 策略: 回退到扬声器');
            }
          }
          break;
      }
    } catch (err) {
      const error = err as BusinessError;
      this.addLog(`❌ 策略应用失败: ${error.message}`);
    }
  }

  /**
   * 处理蓝牙断开
   */
  async handleBluetoothDisconnect(): Promise<void> {
    if (this.currentStrategy === BluetoothRouteStrategy.SPEAKER_FALLBACK) {
      await this.applyStrategy(BluetoothRouteStrategy.SPEAKER_FALLBACK);
    } else {
      this.addLog('ℹ️ 蓝牙已断开,系统将自动选择其他输出设备');
    }
  }

  /**
   * 获取设备类型名称
   */
  getDeviceTypeName(type: audio.DeviceType): string {
    const names: Record<number, string> = {
      [audio.DeviceType.BLUETOOTH_A2DP]: '蓝牙音频(A2DP)',
      [audio.DeviceType.BLUETOOTH_SCO]: '蓝牙通话(SCO)',
      [audio.DeviceType.SPEAKER]: '扬声器',
      [audio.DeviceType.WIRED_HEADSET]: '有线耳机',
    };
    return names[type] || '未知';
  }

  /**
   * 添加日志
   */
  addLog(message: string): void {
    const time = new Date().toLocaleTimeString();
    this.routeLogs.unshift(`[${time}] ${message}`);
    // 只保留最近 20 条
    if (this.routeLogs.length > 20) {
      this.routeLogs = this.routeLogs.slice(0, 20);
    }
  }

  /**
   * 清理
   */
  cleanup(): void {
    this.routingManager?.off('deviceChange', audio.DeviceFlag.OUTPUT_DEVICES_FLAG);
  }

  build() {
    Column({ space: 16 }) {
      // 标题
      Text('📶 蓝牙音频路由管理')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)

      // 蓝牙连接状态
      Row({ space: 12 }) {
        Text(this.isBluetoothConnected ? '🔵' : '⚪')
          .fontSize(24)

        Column({ space: 4 }) {
          Text(this.isBluetoothConnected ? '蓝牙已连接' : '蓝牙未连接')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .fontColor(this.isBluetoothConnected ? '#4CAF50' : '#F44336')

          if (this.isBluetoothConnected) {
            Text(`${this.connectedDeviceName} | 类型: ${this.activeBtType}`)
              .fontSize(12)
              .fontColor('#888')
          }
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .width('90%')
      .padding(16)
      .borderRadius(12)
      .backgroundColor('#252540')

      // 路由策略选择
      Column({ space: 12 }) {
        Text('路由策略')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .width('100%')

        ForEach([
          { strategy: BluetoothRouteStrategy.AUTO, name: '🔄 自动', desc: '系统自动选择最优设备' },
          { strategy: BluetoothRouteStrategy.A2DP_ONLY, name: '🎵 仅A2DP', desc: '强制媒体音频走蓝牙' },
          { strategy: BluetoothRouteStrategy.SCO_ONLY, name: '📞 仅SCO', desc: '强制通话音频走蓝牙' },
          { strategy: BluetoothRouteStrategy.SPEAKER_FALLBACK, name: '🔊 回退扬声器', desc: '蓝牙断开时自动切回扬声器' },
        ], (item: { strategy: BluetoothRouteStrategy; name: string; desc: string }) => {
          Row({ space: 12 }) {
            Radio({ value: item.strategy, group: 'btStrategy' })
              .checked(this.currentStrategy === item.strategy)
              .onChange((isChecked: boolean) => {
                if (isChecked) {
                  this.applyStrategy(item.strategy);
                }
              })

            Column({ space: 2 }) {
              Text(item.name)
                .fontSize(14)
                .fontWeight(FontWeight.Medium)
              Text(item.desc)
                .fontSize(12)
                .fontColor('#888')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
          }
          .width('100%')
          .padding(8)
          .borderRadius(8)
          .backgroundColor(this.currentStrategy === item.strategy ? 'rgba(76,175,80,0.1)' : 'transparent')
        })
      }
      .width('90%')
      .padding(16)
      .borderRadius(12)
      .backgroundColor('#252540')

      // 路由日志
      Column({ space: 8 }) {
        Text('📋 路由日志')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .width('100%')

        List({ space: 4 }) {
          ForEach(this.routeLogs, (log: string, index: number) => {
            ListItem() {
              Text(log)
                .fontSize(11)
                .fontColor('#aaa')
                .maxLines(2)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
            }
          }, (log: string, index: number) => `${index}`)
        }
        .width('100%')
        .height(150)
      }
      .width('90%')
      .padding(16)
      .borderRadius(12)
      .backgroundColor('#252540')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .padding(20)
    .backgroundColor('#1a1a2e')
  }
}

四、踩坑与注意事项

4.1 selectOutputDevice 权限问题

问题:调用 selectOutputDevice() 时抛出权限异常。

原因:切换输出设备需要 ohos.permission.MODIFY_AUDIO_SETTINGS 权限。

解决方案:在 module.json5 中声明权限:

"requestPermissions": [
  {
    "name": "ohos.permission.MODIFY_AUDIO_SETTINGS",
    "reason": "$string:audio_settings_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  }
]

4.2 蓝牙 SCO 和 A2DP 不能同时使用

问题:通话时想同时用 A2DP 播放背景音,结果通话音频也从 A2DP 出来了,音质很差。

原因:蓝牙同一时间只能使用一种协议。SCO 激活时,A2DP 会被暂停。

解决方案:通话场景必须使用 SCO,通话结束后再切回 A2DP。不要试图在通话中同时使用两种协议。

// 通话开始:切换到 SCO
this.routingManager.selectOutputDevice(scoDevice);

// 通话结束:切换回 A2DP
this.routingManager.selectOutputDevice(a2dpDevice);

4.3 设备变化回调的时序问题

问题:收到蓝牙连接回调后立即调用 selectOutputDevice(),但设备还没完全就绪,导致切换失败。

解决方案:收到连接回调后延迟一小段时间再切换,或检查设备状态后再操作。

this.routingManager.on('deviceChange', audio.DeviceFlag.OUTPUT_DEVICES_FLAG, async (action) => {
  if (action.type === audio.DeviceChangeType.CONNECT) {
    // 延迟 500ms 等待设备完全就绪
    setTimeout(async () => {
      try {
        await this.routingManager?.selectOutputDevice(action.device);
      } catch (err) {
        console.warn('[路由] 设备切换延迟后仍失败,可能设备未就绪');
      }
    }, 500);
  }
});

4.4 渲染器级切换 vs 全局切换

问题:使用 audioRenderer.selectOutputDevice() 只切换了当前渲染器的输出,其他应用的音频没变。

原因AudioRenderer.selectOutputDevice() 只影响当前渲染器流,而 AudioRoutingManager.selectOutputDevice() 是全局切换。

选择建议

  • 音乐播放器等独立应用:使用渲染器级切换,不影响其他应用
  • 系统设置类应用:使用全局切换

4.5 分布式音频路由

问题:HarmonyOS 的分布式场景下,音频可能需要流转到其他设备,但路由切换不生效。

解决方案:分布式音频路由需要额外的权限和配置,确保设备在同一分布式网络中,且应用声明了分布式权限。


五、HarmonyOS 6 适配

5.1 API 变更

变更项 HarmonyOS 5 HarmonyOS 6
设备切换 selectOutputDevice(device) 新增 selectOutputDeviceByStrategy(strategy)
蓝牙协议 A2DP / SCO 新增 LE Audio(低功耗音频)支持
设备监听 on('deviceChange') 新增 on('preferredDeviceChange') 首选设备变化
分布式路由 手动切换 新增 selectDistributedDevice() 自动发现分布式设备
音频策略 新增 AudioPolicyManager 统一策略管理

5.2 迁移指南

// HarmonyOS 5 写法
await this.routingManager.selectOutputDevice(targetDevice);

// HarmonyOS 6 写法(支持策略化切换)
await this.routingManager.selectOutputDeviceByStrategy({
  strategy: audio.OutputStrategy.PREFER_BLUETOOTH,
  fallbackDevice: audio.DeviceType.SPEAKER
});

5.3 新特性

  • LE Audio:新一代蓝牙音频协议,更低延迟、更高音质、更低功耗
  • 策略化路由:通过策略描述路由偏好,系统自动选择最优设备
  • 分布式音频流转:一键将音频流转到同一网络下的其他 HarmonyOS 设备
  • 多流路由:不同音频流可以同时输出到不同设备(如音乐走蓝牙,通知走扬声器)

六、总结

mindmap
  root((音频路由))
    设备管理
      设备发现
        getDevices
        设备类型识别
      设备监听
        on deviceChange
        连接/断开事件
      设备信息
        id / deviceType
        address / name
    输出切换
      全局切换
        RoutingManager.selectOutputDevice
        影响所有音频流
      渲染器切换
        AudioRenderer.selectOutputDevice
        仅影响当前流
      切换权限
        MODIFY_AUDIO_SETTINGS
    蓝牙路由
      A2DP协议
        高质量媒体音频
        立体声输出
      SCO协议
        通话语音
        低延迟双向
      策略管理
        自动/强制A2DP
        强制SCO/回退扬声器
    设备优先级
      蓝牙SCO 最高
      有线耳机
      蓝牙A2DP
      听筒
      扬声器 最低
    注意事项
      SCO/A2DP互斥
      设备就绪延迟
      权限声明
      分布式场景

核心要点回顾

  1. AudioRoutingManager 是核心:所有设备发现、监听、切换都通过它完成
  2. 设备优先级有规则:蓝牙 SCO > 有线耳机 > 蓝牙 A2DP > 听筒 > 扬声器,系统默认选最高优先级的已连接设备
  3. 蓝牙双协议互斥:A2DP 和 SCO 不能同时使用,通话必须用 SCO,媒体用 A2DP
  4. 两种切换粒度:全局切换影响所有流,渲染器切换只影响当前流,按需选择
  5. 设备变化有延迟:收到连接回调后设备可能还没完全就绪,切换前适当等待
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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