HarmonyOS APP中的媒体控制

举报
Jack20 发表于 2026/06/21 11:40:59 2026/06/21
【摘要】 核心要点:AVSessionController 是媒体控制的「遥控器」,它让控制端(锁屏、通知栏、蓝牙设备、其他应用)能够远程操控媒体应用的播放行为。掌握 Controller 的创建、命令发送、进度同步和音量控制,是实现跨组件媒体控制的关键。项目说明核心模块@ohos.multimedia.avsession 一、背景与动机上一篇我们讲了 AVSession——媒体应用的「身份证」。但有...

核心要点:AVSessionController 是媒体控制的「遥控器」,它让控制端(锁屏、通知栏、蓝牙设备、其他应用)能够远程操控媒体应用的播放行为。掌握 Controller 的创建、命令发送、进度同步和音量控制,是实现跨组件媒体控制的关键。

项目 说明
核心模块 @ohos.multimedia.avsession

一、背景与动机

上一篇我们讲了 AVSession——媒体应用的「身份证」。但有了身份证还不够,系统还需要一个「遥控器」来操控这个会话。

你想想看,当你坐在沙发上,手机连着蓝牙音箱,你想切歌,你是按音箱上的按钮,还是跑过去划手机?当然是按音箱。那音箱怎么告诉手机「我要切歌」?手机又怎么知道当前播放到哪一分哪一秒?

这就是 AVSessionController 的用武之地。

Controller 是 AVSession 的「另一半」。Session 负责发布信息,Controller 负责消费信息和发送命令。它们就像对讲机的两端——一个说,一个听,但反过来也行。

典型的使用场景:

  1. 锁屏媒体控件——系统锁屏上的播放/暂停/切歌按钮
  2. 通知栏媒体控件——下拉通知栏里的迷你播放器
  3. 蓝牙设备控制——耳机/音箱上的物理按键
  4. 跨应用控制——一个 App 控制另一个 App 的播放
  5. 智能家居联动——语音助手控制播放

二、核心原理

2.1 Session 与 Controller 的关系

flowchart LR
    classDef primary fill:#1890ff,stroke:#096dd9,color:#fff
    classDef warning fill:#fa8c16,stroke:#d48806,color:#fff
    classDef error fill:#f5222d,stroke:#cf1322,color:#fff
    classDef info fill:#13c2c2,stroke:#006d75,color:#fff
    classDef purple fill:#722ed1,stroke:#531dab,color:#fff

    subgraph 媒体应用
        A[AVSession]:::primary
    end

    subgraph AVSessionService
        B[会话管理中心]:::purple
    end

    subgraph 控制端
        C[AVSessionController 1]:::warning
        D[AVSessionController 2]:::warning
        E[AVSessionController N]:::warning
    end

    A -->|注册会话| B
    B -->|分发会话信息| C
    B -->|分发会话信息| D
    B -->|分发会话信息| E
    C -->|发送控制命令| B
    D -->|发送控制命令| B
    E -->|发送控制命令| B
    B -->|转发命令| A

核心关系

  • 一个 AVSession 可以有多个 AVSessionController
  • Controller 通过 sessionId 与 Session 绑定
  • Controller 发送的命令,最终由 Session 侧的 on('play') 等回调接收

2.2 控制命令体系

flowchart TB
    classDef primary fill:#1890ff,stroke:#096dd9,color:#fff
    classDef warning fill:#fa8c16,stroke:#d48806,color:#fff
    classDef error fill:#f5222d,stroke:#cf1322,color:#fff
    classDef info fill:#13c2c2,stroke:#006d75,color:#fff
    classDef purple fill:#722ed1,stroke:#531dab,color:#fff

    Root[AVSessionController 控制命令]:::primary

    Root --> A[播放控制]:::warning
    Root --> B[进度控制]:::warning
    Root --> C[音量控制]:::info
    Root --> D[模式控制]:::info

    A --> A1[sendPlayCommand]
    A --> A2[sendPauseCommand]
    A --> A3[sendStopCommand]
    A --> A4[sendPlayNextCommand]
    A --> A5[sendPlayPreviousCommand]
    A --> A6[sendFastForwardCommand]
    A --> A7[sendRewindCommand]

    B --> B1[sendSeekCommand]

    C --> C1[sendVolumeKeyEvent]

    D --> D1[sendSetSpeedCommand]
    D --> D2[sendSetLoopModeCommand]
    D --> D3[sendToggleFavoriteCommand]

2.3 Controller 生命周期

阶段 方法 说明
创建 avsession.createController(sessionId) 通过 sessionId 创建控制器
活跃 监听会话变化、发送命令 正常工作状态
销毁 controller.destroy() 释放资源

三、代码实战

3.1 发现并创建控制器

要控制一个媒体会话,首先得找到它。系统提供了 avsession.getAllSessionDescriptors() 来获取当前所有活跃会话。

import { avsession } from '@kit.MultimediaKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 发现并创建媒体会话控制器
 * 典型场景:系统媒体控制面板需要发现所有正在播放的媒体应用
 */
class MediaControllerManager {
  private controllers: Map<string, avsession.AVSessionController> = new Map();

  /**
   * 发现所有活跃的媒体会话,并为每个会话创建控制器
   */
  async discoverAndCreateControllers(): Promise<void> {
    try {
      // 第一步:获取所有活跃会话描述符
      const descriptors = await avsession.getAllSessionDescriptors();
      console.info(`[Controller] 发现 ${descriptors.length} 个活跃会话`);

      for (const descriptor of descriptors) {
        const sessionId = descriptor.sessionId;
        if (!sessionId) continue;

        // 跳过已创建的控制器
        if (this.controllers.has(sessionId)) continue;

        // 第二步:根据 sessionId 创建控制器
        const controller = await avsession.createController(sessionId);
        this.controllers.set(sessionId, controller);

        console.info(`[Controller] 创建控制器成功: sessionId=${sessionId}, tag=${descriptor.sessionTag}`);

        // 第三步:注册控制器监听
        this.setupControllerListeners(controller);
      }
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Controller] 发现会话失败: ${error.message}`);
    }
  }

  /**
   * 注册控制器的各种监听
   */
  private setupControllerListeners(controller: avsession.AVSessionController): void {
    // 监听会话元数据变化(歌曲切换时触发)
    controller.on('metadataChange', (metadata: avsession.AVMetadata) => {
      console.info(`[Controller] 元数据变化: title=${metadata.title}, artist=${metadata.artist}`);
      // 更新 UI 显示
    });

    // 监听播放状态变化(播放/暂停/进度变化时触发)
    controller.on('playbackStateChange', (state: avsession.AVPlaybackState) => {
      console.info(`[Controller] 播放状态变化: state=${state.state}, position=${state.position?.elapsedTime}`);
      // 更新播放按钮状态、进度条
    });

    // 监听会话销毁事件
    controller.on('sessionDestroy', () => {
      const sessionId = controller.sessionId;
      console.info(`[Controller] 会话已销毁: ${sessionId}`);
      this.controllers.delete(sessionId);
      controller.destroy();
    });

    // 监听活跃状态变化
    controller.on('activeChange', (isActive: boolean) => {
      console.info(`[Controller] 活跃状态变化: ${isActive}`);
    });
  }

  /**
   * 获取指定会话的控制器
   */
  getController(sessionId: string): avsession.AVSessionController | undefined {
    return this.controllers.get(sessionId);
  }

  /**
   * 销毁所有控制器
   */
  async destroyAll(): Promise<void> {
    for (const [sessionId, controller] of this.controllers) {
      try {
        controller.off('metadataChange');
        controller.off('playbackStateChange');
        controller.off('sessionDestroy');
        controller.off('activeChange');
        await controller.destroy();
      } catch (err) {
        // 忽略销毁错误
      }
    }
    this.controllers.clear();
    console.info('[Controller] 所有控制器已销毁');
  }
}

3.2 发送播放控制命令与进度同步

控制器最核心的功能就是发送控制命令。下面展示一个完整的远程控制面板实现。

import { avsession } from '@kit.MultimediaKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 远程媒体控制面板
 * 模拟锁屏/通知栏的媒体控制逻辑
 */
class RemoteMediaPanel {
  private controller: avsession.AVSessionController | null = null;

  constructor(controller: avsession.AVSessionController) {
    this.controller = controller;
  }

  // ========== 播放控制命令 ==========

  /** 发送播放命令 */
  async play(): Promise<void> {
    if (!this.controller) return;
    try {
      await this.controller.sendControlCommand({ command: 'play' });
      console.info('[RemotePanel] 发送播放命令');
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[RemotePanel] 播放命令失败: ${error.message}`);
    }
  }

  /** 发送暂停命令 */
  async pause(): Promise<void> {
    if (!this.controller) return;
    try {
      await this.controller.sendControlCommand({ command: 'pause' });
      console.info('[RemotePanel] 发送暂停命令');
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[RemotePanel] 暂停命令失败: ${error.message}`);
    }
  }

  /** 发送停止命令 */
  async stop(): Promise<void> {
    if (!this.controller) return;
    try {
      await this.controller.sendControlCommand({ command: 'stop' });
      console.info('[RemotePanel] 发送停止命令');
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[RemotePanel] 停止命令失败: ${error.message}`);
    }
  }

  /** 发送下一曲命令 */
  async playNext(): Promise<void> {
    if (!this.controller) return;
    try {
      await this.controller.sendControlCommand({ command: 'playNext' });
      console.info('[RemotePanel] 发送下一曲命令');
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[RemotePanel] 下一曲命令失败: ${error.message}`);
    }
  }

  /** 发送上一曲命令 */
  async playPrevious(): Promise<void> {
    if (!this.controller) return;
    try {
      await this.controller.sendControlCommand({ command: 'playPrevious' });
      console.info('[RemotePanel] 发送上一曲命令');
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[RemotePanel] 上一曲命令失败: ${error.message}`);
    }
  }

  // ========== 进度同步 ==========

  /** 发送进度跳转命令 */
  async seekTo(positionMs: number): Promise<void> {
    if (!this.controller) return;
    try {
      await this.controller.sendControlCommand({
        command: 'seek',
        parameter: positionMs,  // 目标位置(毫秒)
      });
      console.info(`[RemotePanel] 发送跳转命令: ${positionMs}ms`);
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[RemotePanel] 跳转命令失败: ${error.message}`);
    }
  }

  /** 获取当前播放进度(从缓存的播放状态中读取) */
  async getCurrentProgress(): Promise<{ position: number; duration: number } | null> {
    if (!this.controller) return null;
    try {
      const playbackState = await this.controller.getAVPlaybackState();
      const position = playbackState.position?.elapsedTime ?? 0;
      // 时长需要从元数据获取
      const metadata = await this.controller.getAVMetadata();
      const duration = metadata.duration ?? 0;
      return { position, duration };
    } catch (err) {
      console.error('[RemotePanel] 获取进度失败');
      return null;
    }
  }

  // ========== 模式控制 ==========

  /** 设置播放速度 */
  async setSpeed(speed: number): Promise<void> {
    if (!this.controller) return;
    try {
      await this.controller.sendControlCommand({
        command: 'setSpeed',
        parameter: speed,
      });
      console.info(`[RemotePanel] 设置播放速度: ${speed}x`);
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[RemotePanel] 设置速度失败: ${error.message}`);
    }
  }

  /** 设置循环模式 */
  async setLoopMode(mode: avsession.LoopMode): Promise<void> {
    if (!this.controller) return;
    try {
      await this.controller.sendControlCommand({
        command: 'setLoopMode',
        parameter: mode,
      });
      console.info(`[RemotePanel] 设置循环模式: ${mode}`);
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[RemotePanel] 设置循环模式失败: ${error.message}`);
    }
  }

  /** 切换收藏状态 */
  async toggleFavorite(assetId: string): Promise<void> {
    if (!this.controller) return;
    try {
      await this.controller.sendControlCommand({
        command: 'toggleFavorite',
        parameter: assetId,
      });
      console.info(`[RemotePanel] 切换收藏: ${assetId}`);
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[RemotePanel] 切换收藏失败: ${error.message}`);
    }
  }
}

3.3 音量控制与锁屏/通知栏集成

音量控制是媒体控制中特殊的一环——它不经过 AVSession 的命令通道,而是直接与系统音频服务交互。同时,我们来看看如何监听系统的会话变化,实现类似锁屏媒体控件的完整功能。

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

/**
 * 完整的媒体控制面板
 * 集成音量控制、锁屏/通知栏控制、会话变化监听
 */
@Entry
@Component
struct MediaControlPanel {
  // 当前活跃的控制器
  @State currentController: avsession.AVSessionController | null = null;
  // 当前歌曲信息
  @State songTitle: string = '未知歌曲';
  @State songArtist: string = '未知艺术家';
  @State songAlbum: string = '';
  // 播放状态
  @State isPlaying: boolean = false;
  @State currentPosition: number = 0;  // 当前位置(毫秒)
  @State totalDuration: number = 0;    // 总时长(毫秒)
  // 音量
  @State currentVolume: number = 50;
  @State maxVolume: number = 100;

  // 音频管理器
  private audioManager: audio.AudioVolumeManager | null = null;
  // 会话变化监听器
  private sessionListener: avsession.SessionListener | null = null;

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

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

  /**
   * 初始化媒体控制
   */
  async initMediaControl(): Promise<void> {
    // 第一步:初始化音量管理
    this.audioManager = audio.getAudioManager().getVolumeManager();
    this.initVolumeControl();

    // 第二步:监听系统会话变化
    this.setupSessionListener();

    // 第三步:发现并连接当前活跃会话
    await this.connectToActiveSession();
  }

  /**
   * 初始化音量控制
   */
  private initVolumeControl(): void {
    if (!this.audioManager) return;

    // 获取媒体流音量
    this.audioManager.getVolume(audio.AudioVolumeType.MEDIA, (err: BusinessError, volume: number) => {
      if (err) {
        console.error(`[Volume] 获取音量失败: ${err.message}`);
        return;
      }
      this.currentVolume = volume;
    });

    // 监听音量变化
    this.audioManager.on('volumeChange', (volumeEvent: audio.VolumeEvent) => {
      if (volumeEvent.volumeType === audio.AudioVolumeType.MEDIA) {
        this.currentVolume = volumeEvent.volume;
        this.maxVolume = volumeEvent.maxVolume;
        console.info(`[Volume] 音量变化: ${volumeEvent.volume}/${volumeEvent.maxVolume}`);
      }
    });
  }

  /**
   * 设置系统会话变化监听
   * 当有新的媒体会话创建或销毁时,系统会通知我们
   */
  private setupSessionListener(): void {
    this.sessionListener = {
      // 新会话创建时回调
      onCreate: (session: avsession.AVSessionDescriptor) => {
        console.info(`[SessionListener] 新会话创建: ${session.sessionId}`);
        this.connectToActiveSession();
      },
      // 会话销毁时回调
      onDestroy: (session: avsession.AVSessionDescriptor) => {
        console.info(`[SessionListener] 会话销毁: ${session.sessionId}`);
        // 如果销毁的是当前控制的会话,清空 UI
        if (this.currentController && this.currentController.sessionId === session.sessionId) {
          this.currentController = null;
          this.resetUI();
        }
      },
      // 顶级会话变化时回调(系统焦点切换)
      onTopSessionChange: (session: avsession.AVSessionDescriptor) => {
        console.info(`[SessionListener] 顶级会话变化: ${session.sessionId}`);
        this.connectToActiveSession();
      },
    };

    avsession.on('sessionCreate', this.sessionListener.onCreate);
    avsession.on('sessionDestroy', this.sessionListener.onDestroy);
    avsession.on('topSessionChange', this.sessionListener.onTopSessionChange);
  }

  /**
   * 连接到当前活跃会话
   */
  private async connectToActiveSession(): Promise<void> {
    try {
      const descriptors = await avsession.getAllSessionDescriptors();
      if (descriptors.length === 0) {
        console.info('[Panel] 没有活跃的媒体会话');
        this.resetUI();
        return;
      }

      // 选择第一个活跃会话(实际应用中可以根据类型、优先级等筛选)
      const targetSession = descriptors[0];
      const sessionId = targetSession.sessionId;
      if (!sessionId) return;

      // 如果已有控制器且 sessionId 相同,不需要重新创建
      if (this.currentController && this.currentController.sessionId === sessionId) {
        return;
      }

      // 销毁旧控制器
      if (this.currentController) {
        this.currentController.off('metadataChange');
        this.currentController.off('playbackStateChange');
        await this.currentController.destroy();
      }

      // 创建新控制器
      this.currentController = await avsession.createController(sessionId);
      this.setupControllerListeners(this.currentController);

      // 读取当前元数据和播放状态
      const metadata = await this.currentController.getAVMetadata();
      this.updateMetadataUI(metadata);

      const playbackState = await this.currentController.getAVPlaybackState();
      this.updatePlaybackStateUI(playbackState);

      console.info(`[Panel] 已连接到会话: ${sessionId}`);
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Panel] 连接会话失败: ${error.message}`);
    }
  }

  /**
   * 注册控制器监听(实时同步 UI)
   */
  private setupControllerListeners(controller: avsession.AVSessionController): void {
    // 元数据变化 → 更新歌曲信息
    controller.on('metadataChange', (metadata: avsession.AVMetadata) => {
      this.updateMetadataUI(metadata);
    });

    // 播放状态变化 → 更新播放按钮和进度条
    controller.on('playbackStateChange', (state: avsession.AVPlaybackState) => {
      this.updatePlaybackStateUI(state);
    });
  }

  // ========== UI 更新方法 ==========

  private updateMetadataUI(metadata: avsession.AVMetadata): void {
    this.songTitle = metadata.title ?? '未知歌曲';
    this.songArtist = metadata.artist ?? '未知艺术家';
    this.songAlbum = metadata.album ?? '';
    this.totalDuration = metadata.duration ?? 0;
  }

  private updatePlaybackStateUI(state: avsession.AVPlaybackState): void {
    this.isPlaying = state.state === avsession.PlaybackState.PLAYBACK_STATE_PLAY;
    this.currentPosition = state.position?.elapsedTime ?? 0;
  }

  private resetUI(): void {
    this.songTitle = '未知歌曲';
    this.songArtist = '未知艺术家';
    this.isPlaying = false;
    this.currentPosition = 0;
    this.totalDuration = 0;
  }

  // ========== 控制操作 ==========

  /** 播放/暂停切换 */
  async togglePlayPause(): Promise<void> {
    if (!this.currentController) return;
    const command = this.isPlaying ? 'pause' : 'play';
    await this.currentController.sendControlCommand({ command });
  }

  /** 下一曲 */
  async nextTrack(): Promise<void> {
    if (!this.currentController) return;
    await this.currentController.sendControlCommand({ command: 'playNext' });
  }

  /** 上一曲 */
  async prevTrack(): Promise<void> {
    if (!this.currentController) return;
    await this.currentController.sendControlCommand({ command: 'playPrevious' });
  }

  /** 进度跳转 */
  async seekTo(positionMs: number): Promise<void> {
    if (!this.currentController) return;
    await this.currentController.sendControlCommand({ command: 'seek', parameter: positionMs });
  }

  /** 调节音量 */
  async adjustVolume(delta: number): Promise<void> {
    if (!this.audioManager) return;
    const newVolume = Math.max(0, Math.min(this.maxVolume, this.currentVolume + delta));
    try {
      await this.audioManager.setVolume(audio.AudioVolumeType.MEDIA, newVolume);
    } catch (err) {
      console.error('[Volume] 调节音量失败');
    }
  }

  /**
   * 清理资源
   */
  private async cleanup(): Promise<void> {
    // 移除会话监听
    avsession.off('sessionCreate');
    avsession.off('sessionDestroy');
    avsession.off('topSessionChange');

    // 销毁控制器
    if (this.currentController) {
      this.currentController.off('metadataChange');
      this.currentController.off('playbackStateChange');
      await this.currentController.destroy();
    }

    // 移除音量监听
    if (this.audioManager) {
      this.audioManager.off('volumeChange');
    }
  }

  build() {
    Column() {
      // 歌曲信息
      Text(this.songTitle).fontSize(20).fontWeight(FontWeight.Bold)
      Text(this.songArtist).fontSize(14).fontColor('#999999')

      // 进度条
      Slider({
        value: this.currentPosition,
        min: 0,
        max: this.totalDuration || 1,
      })
        .onChange((value: number) => {
          this.seekTo(value);
        })

      // 控制按钮
      Row() {
        Button('上一曲').onClick(() => this.prevTrack())
        Button(this.isPlaying ? '暂停' : '播放').onClick(() => this.togglePlayPause())
        Button('下一曲').onClick(() => this.nextTrack())
      }

      // 音量控制
      Row() {
        Button('音量-').onClick(() => this.adjustVolume(-1))
        Text(`${this.currentVolume}/${this.maxVolume}`)
        Button('音量+').onClick(() => this.adjustVolume(1))
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

四、踩坑与注意事项

4.1 Controller 创建时机

Controller 只能控制已激活的会话。如果会话还没激活就创建 Controller,getAVMetadata() 等方法可能返回空数据。

// ❌ 错误:会话还没激活就创建控制器
const descriptors = await avsession.getAllSessionDescriptors();
// 此时拿到的可能是未激活的会话

// ✅ 正确:检查会话是否活跃
const descriptors = await avsession.getAllSessionDescriptors();
for (const desc of descriptors) {
  // 只有活跃会话才创建控制器
  if (desc.isActive) {
    const controller = await avsession.createController(desc.sessionId!);
  }
}

4.2 命令发送失败的常见原因

sendControlCommand 可能失败,常见原因:

错误码 原因 解决方案
6600103 会话未激活 先激活会话再发命令
6600105 命令无效 检查命令名称是否正确
6600101 会话不存在 会话可能已被销毁,重新获取
401 参数错误 检查 parameter 类型是否匹配
// 健壮的命令发送
async function sendCommandSafely(
  controller: avsession.AVSessionController,
  command: avsession.AVControlCommand
): Promise<boolean> {
  try {
    await controller.sendControlCommand(command);
    return true;
  } catch (err) {
    const error = err as BusinessError;
    switch (error.code) {
      case 6600103:
        console.warn('[Command] 会话未激活,尝试重新连接');
        break;
      case 6600101:
        console.warn('[Command] 会话不存在,需要重新发现');
        break;
      default:
        console.error(`[Command] 命令发送失败: ${error.message}`);
    }
    return false;
  }
}

4.3 进度同步的精度问题

playbackStateChange 回调的触发频率取决于 Session 侧的更新频率。如果 Session 侧每秒更新一次播放位置,Controller 侧的进度条就会有 1 秒的延迟。

解决方案:在 Controller 侧做插值估算。

// 进度插值估算器
class ProgressEstimator {
  private lastPosition: number = 0;
  private lastUpdateTime: number = 0;
  private isPlaying: boolean = false;

  /** 接收来自 Controller 的播放状态更新 */
  update(position: number, updateTime: number, isPlaying: boolean): void {
    this.lastPosition = position;
    this.lastUpdateTime = updateTime;
    this.isPlaying = isPlaying;
  }

  /** 获取估算的当前进度 */
  getEstimatedPosition(): number {
    if (!this.isPlaying) return this.lastPosition;
    const elapsed = Date.now() - this.lastUpdateTime;
    return this.lastPosition + elapsed;
  }
}

4.4 音量控制的特殊性

音量控制不经过 AVSession 的命令通道,而是直接与系统音频服务交互。这意味着:

  1. sendControlCommand 中没有音量命令
  2. 音量调节需要使用 audio.AudioVolumeManager
  3. 音量变化需要单独监听
// ❌ 错误:AVSession 没有音量命令
await controller.sendControlCommand({ command: 'setVolume' }); // 不存在!

// ✅ 正确:使用 AudioVolumeManager
const volumeManager = audio.getAudioManager().getVolumeManager();
await volumeManager.setVolume(audio.AudioVolumeType.MEDIA, 10);

4.5 Controller 的内存泄漏

每个 Controller 都是系统资源,用完必须销毁。特别是在会话频繁创建/销毁的场景(如用户频繁切换 App),不及时销毁 Controller 会导致内存泄漏。

// 监听会话销毁事件,自动清理对应的 Controller
controller.on('sessionDestroy', () => {
  controller.off('metadataChange');
  controller.off('playbackStateChange');
  controller.destroy();
  this.controllers.delete(sessionId);
});

五、HarmonyOS 6 适配

5.1 API 变更

变更项 HarmonyOS 5 HarmonyOS 6
命令发送 sendControlCommand() 新增 sendControlCommandAsync() 支持异步回调结果
进度同步 被动监听 playbackStateChange 新增 subscribePlaybackPosition() 主动订阅高频进度
音量控制 需要单独使用 AudioVolumeManager 新增 setVolume() 集成到 Controller
分布式控制 仅本地控制 新增远程设备发现与控制

5.2 高频进度订阅

HarmonyOS 6 新增了主动进度订阅能力,可以获取更精确的播放位置:

// HarmonyOS 6 新增:高频进度订阅
controller.subscribePlaybackPosition(100, (position: avsession.AVPlaybackPosition) => {
  // 每 100ms 回调一次,精度大幅提升
  console.info(`[Progress] position=${position.elapsedTime}`);
});

// 取消订阅
controller.unsubscribePlaybackPosition();

5.3 迁移要点

  1. sendControlCommand 替换为 sendControlCommandAsync 以获取命令执行结果
  2. 使用 subscribePlaybackPosition 替代手动插值估算
  3. 分布式控制场景需要声明 ohos.permission.DISTRIBUTED_DATASYNC 权限

六、总结

知识点 核心内容
AVSessionController 本质 AVSession 的「遥控器」,一个 Session 可以有多个 Controller
创建方式 avsession.createController(sessionId) 绑定到指定会话
发现会话 getAllSessionDescriptors() 获取所有活跃会话
播放控制 sendControlCommand({ command }) 发送 play/pause/stop 等命令
进度同步 监听 playbackStateChange 获取播放位置,或使用插值估算
音量控制 不经过 AVSession,使用 audio.AudioVolumeManager 直接控制
元数据监听 metadataChange 回调实时获取歌曲信息变化
会话变化 sessionCreate / sessionDestroy / topSessionChange 三大系统回调
资源释放 Controller 必须在会话销毁时同步 destroy(),避免内存泄漏
HarmonyOS 6 新增高频进度订阅、异步命令回调、分布式控制等能力

💡 一句话总结:AVSessionController 是媒体控制的「遥控器端」,通过发现会话→创建控制器→监听状态变化→发送控制命令的完整链路,实现跨组件、跨应用的媒体控制。记住:命令走 AVSession 通道,音量走 Audio 服务,两者不可混淆。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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