HarmonyOS 6实战(源码教学篇)— AVSession Kit 新特性【仿某云音乐实现媒体会话和后台播放管理】【API20】

HarmonyOS 6实战(源码教学篇)--- AVSession Kit 新特性【仿某云音乐实现媒体会话和后台播放管理】【API20】

    • 教程概述
    • [一、AVSession 媒体会话概述](#一、AVSession 媒体会话概述)
      • [1.1 什么是 AVSession?](#1.1 什么是 AVSession?)
      • [1.2 AVSession 架构](#1.2 AVSession 架构)
      • [1.3 核心概念](#1.3 核心概念)
    • [二、AVSession 初始化](#二、AVSession 初始化)
      • [2.1 AVSessionController 类设计](#2.1 AVSessionController 类设计)
      • [2.2 创建和激活会话](#2.2 创建和激活会话)
    • 三、媒体元数据管理
      • [3.1 设置媒体元数据](#3.1 设置媒体元数据)
      • [3.2 图片资源转换](#3.2 图片资源转换)
    • 四、设置启动能力
      • [4.1 LaunchAbility 作用](#4.1 LaunchAbility 作用)
    • 五、监听控制命令
      • [5.1 注册命令监听器](#5.1 注册命令监听器)
      • [5.2 播放/暂停命令处理](#5.2 播放/暂停命令处理)
      • [5.3 上一首/下一首命令处理](#5.3 上一首/下一首命令处理)
      • [5.4 进度跳转命令处理](#5.4 进度跳转命令处理)
    • 六、循环模式管理
      • [6.1 循环模式映射](#6.1 循环模式映射)
      • [6.2 处理循环模式切换命令](#6.2 处理循环模式切换命令)
      • [6.3 同步循环模式到 AVSession](#6.3 同步循环模式到 AVSession)
    • 七、播放状态同步
      • [7.1 同步播放/暂停状态](#7.1 同步播放/暂停状态)
      • [7.2 同步播放进度](#7.2 同步播放进度)
      • [7.3 在 AudioRendererController 中调用](#7.3 在 AudioRendererController 中调用)
    • 八、收藏功能实现
      • [8.1 收藏状态管理](#8.1 收藏状态管理)
      • [8.2 同步收藏状态](#8.2 同步收藏状态)
      • [8.3 Preferences 工具类](#8.3 Preferences 工具类)
    • 九、后台播放管理
      • [9.1 后台播放原理](#9.1 后台播放原理)
      • [9.2 BackgroundUtil 工具类](#9.2 BackgroundUtil 工具类)
      • [9.3 在播放控制中集成](#9.3 在播放控制中集成)
      • [9.4 权限配置](#9.4 权限配置)
    • 十、静音模式与混音
      • [10.1 静音模式](#10.1 静音模式)
    • 十一、完整集成流程
      • [11.1 应用启动流程](#11.1 应用启动流程)
      • [11.2 页面初始化流程](#11.2 页面初始化流程)
      • [11.3 AudioRendererController 初始化](#11.3 AudioRendererController 初始化)
      • [11.4 AVSessionController 初始化](#11.4 AVSessionController 初始化)
    • 十二、状态同步机制
      • [12.1 状态流转图](#12.1 状态流转图)
      • [12.2 关键状态同步点](#12.2 关键状态同步点)
      • [12.3 状态同步代码示例](#12.3 状态同步代码示例)
    • 十三、注销监听器
      • [13.1 为什么要注销?](#13.1 为什么要注销?)
      • [13.2 注销 AVSession 监听器](#13.2 注销 AVSession 监听器)
      • [13.3 在释放资源时调用](#13.3 在释放资源时调用)
    • 十六、常见问题与解决方案
      • [Q1: 控制中心不显示播放信息?](#Q1: 控制中心不显示播放信息?)
      • [Q2: 点击控制中心无响应?](#Q2: 点击控制中心无响应?)
      • [Q3: 后台播放被暂停?](#Q3: 后台播放被暂停?)
      • [Q4: 状态不同步?](#Q4: 状态不同步?)
      • [Q5: 切歌后封面不更新?](#Q5: 切歌后封面不更新?)
    • 总结

大家好!我是你们的老朋友木斯佳, 是华为云 HDE 认证专家和OpenTiny 开源社区的布道师,熟悉我们的小伙伴们已经实现了音频播放的小案例,今天我们继续分享HarmonyOS 6 基础能力 AVSession Kit 新特性。

AVSession Kit 提供音视频统一管控能力,音视频类应用接入AVSession后,可以发送应用的数据(比如正在播放的歌曲、歌曲的播放状态等),用户可以通过系统播控中心、语音助手等应用切换多个应用、多个设备播放音频接入AVSession后,可以进行后台音频播放。


教程概述

本教程分为上下两篇:

在上篇中,我们学习了如何使用 AudioRenderer 实现音频播放的核心功能。本篇将带你深入理解 AVSession 媒体会话机制,实现与系统媒体控制中心的交互,以及后台播放管理。

学习目标

  • 理解 AVSession 媒体会话的作用和原理
  • 实现与系统媒体控制中心的双向交互
  • 掌握媒体元数据的设置和更新
  • 实现后台播放和长时任务管理
  • 同步应用内外的播放状态
  • 处理收藏、循环模式等高级功能

技术栈

  • 核心 API: AVSession、BackgroundTaskManager、WantAgent
  • 权限: ohos.permission.KEEP_BACKGROUND_RUNNING

一、AVSession 媒体会话概述

1.1 什么是 AVSession?

AVSession(Audio Video Session)是 HarmonyOS 提供的媒体会话管理框架,用于:

  • 在系统媒体控制中心显示播放信息
  • 接收来自控制中心的播放控制命令
  • 同步播放状态到系统
  • 支持锁屏、通知栏、控制中心的统一控制

1.2 AVSession 架构

复制代码
┌─────────────────────────────────────────┐
│        系统媒体控制中心                    │
│  (锁屏、通知栏、控制中心)                  │
└──────────────┬──────────────────────────┘
               │ 命令 (play/pause/next...)
               ↓
┌─────────────────────────────────────────┐
│         AVSession 会话                   │
│  - 接收控制命令                           │
│  - 发送媒体元数据                         │
│  - 同步播放状态                           │
└──────────────┬──────────────────────────┘
               │ 调用播放器方法
               ↓
┌─────────────────────────────────────────┐
│    AudioRendererController              │
│    (音频播放控制器)                       │
└─────────────────────────────────────────┘

1.3 核心概念

概念 说明
AVMetadata 媒体元数据(歌曲名、歌手、封面等)
AVPlaybackState 播放状态(播放/暂停、进度、循环模式等)
AVSessionController 会话控制器(接收系统命令)
LaunchAbility 启动能力(点击控制中心时打开应用)

二、AVSession 初始化

2.1 AVSessionController 类设计

typescript 复制代码
export class AVSessionController {
  private context: common.UIAbilityContext;
  private AVSession?: avSession.AVSession;
  private songList: SongItem[] = [];
  private musicIndex: number = 0;
  private audioRendererController?: AudioRendererController;

  constructor() {
    let list = AppStorage.get<SongItem[]>('songList');
    if (list) {
      this.songList = list;
    }
    this.initAVSession();
  }

  /**
   * 获取单例实例
   */
  public static getInstance(): AVSessionController {
    let avSessionController = AppStorage.get<AVSessionController>('AVSessionController');
    
    if (!avSessionController) {
      avSessionController = new AVSessionController();
      AppStorage.setOrCreate('AVSessionController', avSessionController);
    }
    
    return avSessionController;
  }
}

2.2 创建和激活会话

typescript 复制代码
private async initAVSession() {
  // 1. 获取上下文
  this.context = AppStorage.get('context');
  if (!this.context) {
    Logger.error(TAG, '上下文获取失败');
    return;
  }

  // 2. 获取音频控制器引用
  this.audioRendererController = AppStorage.get('audioRendererController');
  if (!this.audioRendererController) {
    Logger.error(TAG, '音频控制器获取失败');
    return;
  }

  // 3. 创建 AVSession
  this.AVSession = await avSession.createAVSession(
    this.context,
    "PLAY_AUDIO",  // 会话标签(自定义)
    'audio'        // 会话类型:audio(音频) / video(视频)
  );

  // 4. 激活会话
  await this.AVSession.activate();
  Logger.info(TAG, `会话创建成功,ID: ${this.AVSession.sessionId}`);

  // 5. 初始化配置
  await this.setAVMetadata();           // 设置媒体元数据
  this.setLaunchAbility();              // 设置启动能力
  this.setListenerForMesFromController(); // 监听控制命令
}

关键参数说明

  • 会话标签: 自定义字符串,用于标识会话
  • 会话类型 : audio 用于音频播放,video 用于视频播放
  • 激活: 只有激活的会话才能在控制中心显示

三、媒体元数据管理

3.1 设置媒体元数据

媒体元数据会显示在系统媒体控制中心,包括歌曲名、歌手、封面等。

typescript 复制代码
public async setAVMetadata() {
  // 1. 获取当前歌曲索引
  this.musicIndex = AppStorage.get('selectIndex') ?? 0;
  
  try {
    // 2. 加载封面图片
    let mediaImage = await MediaTools.getPixelMapFromResource(
      this.context,
      this.songList[this.musicIndex].label
    );

    // 3. 构建元数据对象
    let metadata: avSession.AVMetadata = {
      assetId: `${this.musicIndex}`,                    // 资源ID(用于标识)
      title: this.songList[this.musicIndex].title,      // 歌曲名
      artist: this.songList[this.musicIndex].singer,    // 歌手名
      mediaImage: mediaImage,                           // 封面图(PixelMap)
      duration: this.getDuration(),                     // 总时长(毫秒)
    };

    // 4. 加载歌词(可选)
    let lrc = await MediaTools.getLrcFromRawFile(
      this.context,
      this.songList[this.musicIndex].lyric
    );
    if (lrc) {
      metadata.lyric = lrc;  // 歌词内容
    }

    // 5. 设置到 AVSession
    if (this.AVSession) {
      await this.AVSession.setAVMetadata(metadata);
      Logger.info(TAG, '元数据设置成功');
    }
  } catch (error) {
    Logger.error(TAG, `元数据设置失败: ${error.message}`);
  }
}

AVMetadata 完整字段

字段 类型 说明 是否必填
assetId string 资源唯一标识
title string 歌曲标题
artist string 歌手名称
author string 作者
album string 专辑名称
mediaImage PixelMap 封面图片
duration number 总时长(毫秒)
lyric string 歌词内容

3.2 图片资源转换

typescript 复制代码
export class MediaTools {
  /**
   * 将 Resource 资源转换为 PixelMap
   */
  static async getPixelMapFromResource(
    context: common.UIAbilityContext,
    resource: resourceManager.Resource
  ): Promise<PixelMap> {
    let resourceMgr = context.resourceManager;
    
    // 1. 读取图片资源为 ArrayBuffer
    let imageBuffer = await resourceMgr.getMediaContent(resource);
    
    // 2. 创建图片源
    let imageSource = image.createImageSource(imageBuffer);
    
    // 3. 创建 PixelMap
    let pixelMap = await imageSource.createPixelMap();
    
    return pixelMap;
  }

  /**
   * 从 rawfile 读取歌词文件
   */
  static async getLrcFromRawFile(
    context: common.UIAbilityContext,
    lyricPath: string
  ): Promise<string> {
    let resourceMgr = context.resourceManager;
    
    // 读取文本文件
    let lrcBuffer = await resourceMgr.getRawFileContent(lyricPath);
    
    // 转换为字符串
    let decoder = util.TextDecoder.create('utf-8');
    let lrcContent = decoder.decodeWithStream(lrcBuffer);
    
    return lrcContent;
  }
}

四、设置启动能力

4.1 LaunchAbility 作用

当用户点击系统媒体控制中心时,可以启动或切换到应用。

typescript 复制代码
private setLaunchAbility() {
  if (!this.context) return;

  // 1. 配置 WantAgent 信息
  let wantAgentInfo: wantAgent.WantAgentInfo = {
    wants: [{
      bundleName: this.context.abilityInfo.bundleName,  // 应用包名
      abilityName: this.context.abilityInfo.name        // Ability 名称
    }],
    operationType: wantAgent.OperationType.START_ABILITIES,  // 操作类型
    requestCode: 0,
    wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]  // 更新标志
  };

  // 2. 获取 WantAgent
  wantAgent.getWantAgent(wantAgentInfo).then((agent) => {
    if (this.AVSession) {
      // 3. 设置到 AVSession
      this.AVSession.setLaunchAbility(agent);
      Logger.info(TAG, '启动能力设置成功');
    }
  });
}

WantAgent 操作类型

  • START_ABILITIES: 启动 Ability
  • START_ABILITY: 启动单个 Ability
  • SEND_COMMON_EVENT: 发送公共事件

五、监听控制命令

5.1 注册命令监听器

系统媒体控制中心会发送各种控制命令,应用需要监听并响应。

typescript 复制代码
async setListenerForMesFromController() {
  if (!this.AVSession) return;

  // 注册各种命令监听器
  this.AVSession.on('play', this.onPlay);
  this.AVSession.on('pause', this.onPause);
  this.AVSession.on('playNext', this.onPlayNext);
  this.AVSession.on('playPrevious', this.onPlayPrevious);
  this.AVSession.on('seek', this.onSeek);
  this.AVSession.on('setLoopMode', this.onSetLoopMode);
  this.AVSession.on('toggleFavorite', this.onToggleFavorite);
  
  Logger.info(TAG, '命令监听器注册成功');
}

5.2 播放/暂停命令处理

typescript 复制代码
/**
 * 处理播放命令
 */
private onPlay: () => void = () => {
  Logger.info(TAG, '收到播放命令');
  
  if (!this.audioRendererController) {
    Logger.error(TAG, '音频控制器未初始化');
    return;
  }

  // 判断是首次播放还是恢复播放
  if (this.audioRendererController.getFirst()) {
    this.audioRendererController.play();  // 首次播放
  } else {
    this.audioRendererController.start(); // 恢复播放
  }
}

/**
 * 处理暂停命令
 */
private onPause: () => void = () => {
  Logger.info(TAG, '收到暂停命令');
  
  if (!this.audioRendererController) {
    Logger.error(TAG, '音频控制器未初始化');
    return;
  }

  this.audioRendererController.pause();
}

5.3 上一首/下一首命令处理

typescript 复制代码
/**
 * 处理下一首命令
 */
private onPlayNext: () => void = () => {
  Logger.info(TAG, '收到下一首命令');
  
  if (!this.audioRendererController) {
    Logger.error(TAG, '音频控制器未初始化');
    return;
  }

  this.audioRendererController.playNext();
}

/**
 * 处理上一首命令
 */
private onPlayPrevious: () => void = () => {
  Logger.info(TAG, '收到上一首命令');
  
  if (!this.audioRendererController) {
    Logger.error(TAG, '音频控制器未初始化');
    return;
  }

  this.audioRendererController.playPrevious();
}

5.4 进度跳转命令处理

typescript 复制代码
/**
 * 处理进度跳转命令
 * @param curMs 目标时间(毫秒)
 */
private onSeek: (curMs: number) => void = (curMs: number) => {
  Logger.info(TAG, `收到跳转命令: ${curMs}ms`);
  
  if (!this.audioRendererController) {
    Logger.error(TAG, '音频控制器未初始化');
    return;
  }

  this.audioRendererController.seek(curMs);
}

六、循环模式管理

6.1 循环模式映射

AVSession 和应用内部使用不同的循环模式枚举,需要进行映射。

typescript 复制代码
// 应用内部枚举
enum MusicPlayMode {
  SINGLE_CYCLE = 0,  // 单曲循环
  ORDER = 1,         // 顺序播放
  RANDOM = 2         // 随机播放
}

// AVSession 枚举
enum avSession.LoopMode {
  LOOP_MODE_SINGLE,    // 单曲循环
  LOOP_MODE_SEQUENCE,  // 顺序播放
  LOOP_MODE_SHUFFLE    // 随机播放
}

6.2 处理循环模式切换命令

typescript 复制代码
/**
 * 处理循环模式切换命令
 */
private onSetLoopMode: (loopMode: avSession.LoopMode) => void = (loopMode) => {
  Logger.info(TAG, `收到循环模式切换命令: ${loopMode}`);
  
  if (!this.audioRendererController) {
    Logger.error(TAG, '音频控制器未初始化');
    return;
  }

  // AVSession 模式 → 应用内部模式
  switch (loopMode) {
    case avSession.LoopMode.LOOP_MODE_SINGLE:
      this.audioRendererController.setPlayModel(MusicPlayMode.ORDER);
      break;
    case avSession.LoopMode.LOOP_MODE_SEQUENCE:
      this.audioRendererController.setPlayModel(MusicPlayMode.RANDOM);
      break;
    case avSession.LoopMode.LOOP_MODE_SHUFFLE:
      this.audioRendererController.setPlayModel(MusicPlayMode.SINGLE_CYCLE);
      break;
  }

  // 同步状态
  this.setPlayModeToAVSession();
  this.setPlayModeToControlArea();
}

6.3 同步循环模式到 AVSession

typescript 复制代码
/**
 * 将应用内部的循环模式同步到 AVSession
 */
public setPlayModeToAVSession() {
  if (!this.audioRendererController) return;

  let currentPlayMode = this.audioRendererController.getPlayMode();
  let AVSessionLoopMode: avSession.LoopMode;

  // 应用内部模式 → AVSession 模式
  switch (currentPlayMode) {
    case MusicPlayMode.SINGLE_CYCLE:
      AVSessionLoopMode = avSession.LoopMode.LOOP_MODE_SINGLE;
      break;
    case MusicPlayMode.ORDER:
      AVSessionLoopMode = avSession.LoopMode.LOOP_MODE_SEQUENCE;
      break;
    case MusicPlayMode.RANDOM:
      AVSessionLoopMode = avSession.LoopMode.LOOP_MODE_SHUFFLE;
      break;
  }

  this.setLoopModeState(AVSessionLoopMode);
}

/**
 * 设置循环模式状态
 */
public setLoopModeState(loopMode: avSession.LoopMode) {
  if (this.AVSession) {
    this.AVSession.setAVPlaybackState({ loopMode }, (err) => {
      if (err) {
        Logger.error(TAG, `设置循环模式失败: ${err.message}`);
      } else {
        Logger.info(TAG, '循环模式设置成功');
      }
    });
  }
}

七、播放状态同步

7.1 同步播放/暂停状态

typescript 复制代码
/**
 * 同步播放状态到 AVSession
 * @param isPlay true-播放中,false-暂停
 */
public setPlayState(isPlay: boolean) {
  if (!this.AVSession) return;

  this.AVSession.setAVPlaybackState({
    state: isPlay ? 
      avSession.PlaybackState.PLAYBACK_STATE_PLAY : 
      avSession.PlaybackState.PLAYBACK_STATE_PAUSE
  }, (err) => {
    if (err) {
      Logger.error(TAG, `设置播放状态失败: ${err.message}`);
    } else {
      Logger.info(TAG, `播放状态设置为: ${isPlay ? '播放' : '暂停'}`);
    }
  });
}

7.2 同步播放进度

typescript 复制代码
/**
 * 同步播放进度到 AVSession
 * @param ms 当前播放位置(毫秒)
 */
public setProgressState(ms: number) {
  if (!this.AVSession) return;

  this.AVSession.setAVPlaybackState({
    position: {
      elapsedTime: ms,                  // 已播放时间
      updateTime: new Date().getTime()  // 更新时间戳
    }
  }, (err) => {
    if (err) {
      Logger.error(TAG, `设置进度失败: ${err.message}`);
    }
  });
}

注意事项

  • elapsedTime: 当前播放位置(毫秒)
  • updateTime: 状态更新的时间戳,系统用于计算实时进度
  • 建议每秒更新一次,避免频繁调用

7.3 在 AudioRendererController 中调用

typescript 复制代码
// 在 AudioRendererController 中
private updateIsPlay(isPlay: boolean) {
  AppStorage.setOrCreate<boolean>('isPlay', isPlay);
  
  // 同步到 AVSession
  if (this.avSessionController) {
    this.avSessionController.setPlayState(isPlay);
  }
}

public seek(ms: number) {
  this.curMs = ms;
  AppStorage.setOrCreate('progress', this.curMs);
  AppStorage.setOrCreate('currentTime', MediaTools.msToCountdownTime(this.curMs));
  
  // 同步到 AVSession
  if (this.avSessionController) {
    this.avSessionController.setProgressState(ms);
  }
  
  this.currentOffset = this.initOffset + MediaTools.getOffsetFromTime(this.curMs);
}

八、收藏功能实现

8.1 收藏状态管理

使用 Preferences 持久化存储收藏列表。

typescript 复制代码
/**
 * 处理收藏切换命令
 */
private onToggleFavorite: (assetId: string) => void = (assetId: string) => {
  Logger.info(TAG, `收到收藏切换命令: ${assetId}`);
  this.updateFavoriteState(assetId);
}

/**
 * 更新收藏状态
 */
public async updateFavoriteState(assetId: string, isFavorite: boolean = false) {
  if (!this.context) return;

  // 1. 获取收藏列表
  let favoriteIds = await PreferencesUtil.getInstance().getFormIds(this.context);

  // 2. 切换收藏状态
  if (favoriteIds.includes(assetId)) {
    // 取消收藏
    await PreferencesUtil.getInstance().removeFormId(this.context, assetId);
    isFavorite = false;
  } else {
    // 添加收藏
    await PreferencesUtil.getInstance().addFormId(this.context, assetId);
    isFavorite = true;
  }

  // 3. 更新UI和AVSession
  AppStorage.setOrCreate('isFavorite', isFavorite);
  this.setFavoriteState(isFavorite);
}

8.2 同步收藏状态

typescript 复制代码
/**
 * 设置收藏状态到 AVSession
 */
private setFavoriteState(isFavorite: boolean) {
  if (!this.AVSession) return;

  this.AVSession.setAVPlaybackState({ isFavorite }, (err) => {
    if (err) {
      Logger.error(TAG, `设置收藏状态失败: ${err.message}`);
    } else {
      Logger.info(TAG, `收藏状态设置为: ${isFavorite}`);
    }
  });
}

/**
 * 获取并更新收藏状态(切歌时调用)
 */
public async getAndUpdateFavoriteState(assetId: string) {
  if (!this.context) return;

  let favoriteIds = await PreferencesUtil.getInstance().getFormIds(this.context);
  let isFavorite = favoriteIds.includes(assetId);

  AppStorage.setOrCreate('isFavorite', isFavorite);
  this.setFavoriteState(isFavorite);
}

8.3 Preferences 工具类

typescript 复制代码
export class PreferencesUtil {
  private static instance: PreferencesUtil;
  private preferences?: dataPreferences.Preferences;

  public static getInstance(): PreferencesUtil {
    if (!PreferencesUtil.instance) {
      PreferencesUtil.instance = new PreferencesUtil();
    }
    return PreferencesUtil.instance;
  }

  /**
   * 获取收藏列表
   */
  async getFormIds(context: common.UIAbilityContext): Promise<string[]> {
    this.preferences = await dataPreferences.getPreferences(context, 'favoriteStore');
    let ids = await this.preferences.get('favoriteIds', '[]');
    return JSON.parse(ids as string);
  }

  /**
   * 添加收藏
   */
  async addFormId(context: common.UIAbilityContext, id: string): Promise<void> {
    let ids = await this.getFormIds(context);
    if (!ids.includes(id)) {
      ids.push(id);
      await this.preferences?.put('favoriteIds', JSON.stringify(ids));
      await this.preferences?.flush();
    }
  }

  /**
   * 取消收藏
   */
  async removeFormId(context: common.UIAbilityContext, id: string): Promise<void> {
    let ids = await this.getFormIds(context);
    let index = ids.indexOf(id);
    if (index > -1) {
      ids.splice(index, 1);
      await this.preferences?.put('favoriteIds', JSON.stringify(ids));
      await this.preferences?.flush();
    }
  }
}

九、后台播放管理

9.1 后台播放原理

HarmonyOS 默认会在应用退到后台后暂停音频播放,需要申请长时任务(Continuous Task)才能实现后台播放。

长时任务类型

  • AUDIO_PLAYBACK: 音频播放
  • AUDIO_RECORDING: 音频录制
  • LOCATION: 定位导航
  • BLUETOOTH_INTERACTION: 蓝牙交互
  • MULTI_DEVICE_CONNECTION: 多设备连接
  • WIFI_INTERACTION: WIFI 交互
  • VOIP: 音视频通话
  • TASK_KEEPING: 计算任务

9.2 BackgroundUtil 工具类

typescript 复制代码
export class BackgroundUtil {
  /**
   * 启动后台任务
   */
  public static startContinuousTask(context?: common.UIAbilityContext): void {
    if (!context) {
      Logger.error(TAG, '上下文未定义,无法启动后台任务');
      return;
    }

    // 1. 配置 WantAgent(用于通知栏点击)
    let wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [{
        bundleName: context.abilityInfo.bundleName,
        abilityName: context.abilityInfo.name
      }],
      operationType: wantAgent.OperationType.START_ABILITY,
      requestCode: 0,
      wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };

    // 2. 获取 WantAgent
    wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj) => {
      try {
        // 3. 启动后台任务
        backgroundTaskManager.startBackgroundRunning(
          context,
          backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,  // 音频播放模式
          wantAgentObj
        ).then(() => {
          Logger.info(TAG, '后台任务启动成功');
        }).catch((error: BusinessError) => {
          Logger.error(TAG, `后台任务启动失败: ${error.code}`);
        });
      } catch (error) {
        Logger.error(TAG, `后台任务启动异常: ${error.message}`);
      }
    });
  }

  /**
   * 停止后台任务
   */
  public static stopContinuousTask(context: common.UIAbilityContext): void {
    try {
      backgroundTaskManager.stopBackgroundRunning(context).then(() => {
        Logger.info(TAG, '后台任务停止成功');
      }).catch((error: BusinessError) => {
        Logger.error(TAG, `后台任务停止失败: ${error.code}`);
      });
    } catch (error) {
      Logger.error(TAG, `后台任务停止异常: ${error.message}`);
    }
  }
}

9.3 在播放控制中集成

typescript 复制代码
// 在 AudioRendererController 中

/**
 * 开始播放
 */
async play(musicIndex: number) {
  // 启动后台任务
  BackgroundUtil.startContinuousTask(this.context);
  
  this.updateMusicIndex(musicIndex);
  
  if (this.isFirst) {
    await this.loadSongAssent();
    await this.stop();
    await this.start();
  } else {
    await this.stop();
    await this.reset();
  }
}

/**
 * 开始播放
 */
async start() {
  if (!this.audioRenderer) return;
  
  await this.audioRenderer.start();
  this.updateIsPlay(true);
  
  // 确保后台任务已启动
  BackgroundUtil.startContinuousTask(this.context);
}

/**
 * 释放资源
 */
async release() {
  if (!this.audioRenderer || !this.context) return;
  
  await this.audioRenderer.release();
  
  // 停止后台任务
  BackgroundUtil.stopContinuousTask(this.context);
  
  // 注销 AVSession 监听器
  this.avSessionController?.unregisterSessionListener();
}

9.4 权限配置

module.json5 中声明后台任务权限:

json 复制代码
{
  "module": {
    "name": "entry",
    "type": "entry",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "backgroundModes": ["audioPlayback"]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
        "reason": "$string:keep_background_running_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

string.json 中添加权限说明:

json 复制代码
{
  "string": [
    {
      "name": "keep_background_running_reason",
      "value": "用于后台播放音乐"
    }
  ]
}

十、静音模式与混音

10.1 静音模式

静音模式允许应用在不打断其他音频的情况下播放音频。

typescript 复制代码
/**
 * 设置静音模式和混音
 * @param isSupportSilent true-启用静音模式,false-禁用
 */
public async setSilentModeAndMixWithOthers(isSupportSilent: boolean = false) {
  if (!this.audioRenderer || !this.context) return;

  let audioManger = audio.getAudioManager();
  let audioSessionManager = audioManger.getSessionManager();
  
  // 配置音频会话策略
  let strategy: audio.AudioSessionStrategy = {
    concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_PAUSE_OTHERS
  };

  // 获取静音模式配置
  let formIds = await PreferencesUtil.getInstance().getFormIds(this.context);

  if (isSupportSilent) {
    // 启用静音模式
    if (!formIds.includes(SILENT_ID)) {
      await PreferencesUtil.getInstance().addFormId(this.context, SILENT_ID);
    }
    
    this.audioRenderer.setSilentModeAndMixWithOthers(isSupportSilent);
    
    // 停用音频会话,允许其他音频恢复
    audioSessionManager.deactivateAudioSession().then(() => {
      Logger.info(TAG, '音频会话停用成功');
    }).catch((err: BusinessError) => {
      Logger.error(TAG, `音频会话停用失败: ${err}`);
    });
  } else {
    // 禁用静音模式
    // 先激活音频会话
    audioSessionManager.activateAudioSession(strategy).then(() => {
      Logger.info(TAG, '音频会话激活成功');
    }).catch((err: BusinessError) => {
      Logger.error(TAG, `音频会话激活失败: ${err}`);
    });
    
    if (formIds.includes(SILENT_ID)) {
      await PreferencesUtil.getInstance().removeFormId(this.context, SILENT_ID);
    }
    
    this.audioRenderer.setSilentModeAndMixWithOthers(isSupportSilent);
  }

  AppStorage.setOrCreate('isSilentMode', isSupportSilent);
}

使用场景

  • 启用静音模式:应用音频不会打断其他应用(如导航、通话)
  • 禁用静音模式:应用音频会暂停其他应用的音频

十一、完整集成流程

11.1 应用启动流程

typescript 复制代码
// EntryAbility.ets
export default class EntryAbility extends UIAbility {
  onCreate() {
    // 保存 context 到全局存储
    AppStorage.setOrCreate('context', this.context);
  }

  onWindowStageCreate(windowStage: window.WindowStage) {
    windowStage.loadContent('pages/Root', (err) => {
      // 加载主页面
    });
  }
}

11.2 页面初始化流程

typescript 复制代码
// PlayerPage.ets
@Component
struct PlayerPage {
  aboutToAppear(): void {
    // 1. 初始化歌曲列表
    AppStorage.setOrCreate('songList', songList);
    
    // 2. 创建音频控制器(会自动创建 AVSession)
    AudioRendererController.getInstance();
  }

  aboutToDisappear(): void {
    // 释放资源
    AudioRendererController.getInstance().release();
  }

  build() {
    NavDestination() {
      PlayerInfoComponent()
    }
  }
}

11.3 AudioRendererController 初始化

typescript 复制代码
export class AudioRendererController {
  constructor() {
    // 1. 初始化 AudioRenderer
    this.initAudioRenderer();
  }

  private initAudioRenderer() {
    audio.createAudioRenderer(options, (err, data) => {
      this.audioRenderer = data;
      
      // 2. 设置回调
      this.setAudioRendererCallbacks();
    });
  }

  private setAudioRendererCallbacks() {
    // 3. 创建 AVSession
    this.avSessionController = AVSessionController.getInstance();
    
    // 4. 设置其他回调
    this.setWriteDataCallback();
    this.setInterruptCallback();
    this.setOutputDeviceChangeCallback();
  }
}

11.4 AVSessionController 初始化

typescript 复制代码
export class AVSessionController {
  constructor() {
    this.initAVSession();
  }

  private async initAVSession() {
    // 1. 创建并激活会话
    this.AVSession = await avSession.createAVSession(this.context, "PLAY_AUDIO", 'audio');
    await this.AVSession.activate();
    
    // 2. 设置元数据
    await this.setAVMetadata();
    
    // 3. 设置启动能力
    this.setLaunchAbility();
    
    // 4. 监听控制命令
    this.setListenerForMesFromController();
  }
}

十二、状态同步机制

12.1 状态流转图

复制代码
用户操作 → AudioRendererController → AppStorage → UI 更新
                    ↓
            AVSessionController
                    ↓
          系统媒体控制中心显示

系统控制中心操作 → AVSessionController → AudioRendererController
                                              ↓
                                        AppStorage → UI 更新

12.2 关键状态同步点

操作 同步内容 同步目标
播放/暂停 播放状态 AppStorage + AVSession
切歌 歌曲信息、元数据 AppStorage + AVSession
进度更新 播放进度 AppStorage + AVSession
模式切换 循环模式 AppStorage + AVSession
收藏切换 收藏状态 AppStorage + AVSession + Preferences

12.3 状态同步代码示例

typescript 复制代码
// 在 AudioRendererController 中更新歌曲
private updateMusicIndex(musicIndex: number) {
  // 1. 更新本地状态
  this.musicIndex = musicIndex;
  
  // 2. 同步到 AppStorage(UI 会自动更新)
  AppStorage.setOrCreate('selectIndex', musicIndex);
  
  // 3. 同步到 AVSession
  if (this.avSessionController) {
    this.avSessionController.setAVMetadata();  // 更新元数据
    this.avSessionController.getAndUpdateFavoriteState(musicIndex.toString());  // 更新收藏状态
  }
}

十三、注销监听器

13.1 为什么要注销?

  • 避免内存泄漏
  • 防止重复监听
  • 确保资源正确释放

13.2 注销 AVSession 监听器

typescript 复制代码
/**
 * 注销所有 AVSession 监听器
 */
async unregisterSessionListener() {
  if (!this.AVSession) return;

  this.AVSession.off('play');
  this.AVSession.off('pause');
  this.AVSession.off('playNext');
  this.AVSession.off('playPrevious');
  this.AVSession.off('setLoopMode');
  this.AVSession.off('seek');
  this.AVSession.off('toggleFavorite');
  
  Logger.info(TAG, '监听器注销成功');
}

13.3 在释放资源时调用

typescript 复制代码
// 在 AudioRendererController 中
async release() {
  if (!this.audioRenderer || !this.context) return;

  try {
    // 1. 释放 AudioRenderer
    await this.audioRenderer.release();
    
    // 2. 注销 AVSession 监听器
    this.avSessionController?.unregisterSessionListener();
    
    // 3. 停止后台任务
    BackgroundUtil.stopContinuousTask(this.context);
    
    Logger.info(TAG, '资源释放成功');
  } catch (e) {
    Logger.error(TAG, '资源释放失败');
  }
}

十六、常见问题与解决方案

Q1: 控制中心不显示播放信息?

原因 : AVSession 未激活或元数据未设置
解决:

typescript 复制代码
await this.AVSession.activate();
await this.setAVMetadata();

Q2: 点击控制中心无响应?

原因 : 未注册命令监听器
解决:

typescript 复制代码
this.setListenerForMesFromController();

Q3: 后台播放被暂停?

原因 : 未申请后台任务或权限未配置
解决:

  • 配置 KEEP_BACKGROUND_RUNNING 权限
  • 调用 BackgroundUtil.startContinuousTask()

Q4: 状态不同步?

原因 : 未在状态变化时同步到 AVSession
解决:

typescript 复制代码
this.avSessionController.setPlayState(isPlay);
this.avSessionController.setProgressState(ms);

Q5: 切歌后封面不更新?

原因 : 未重新设置元数据
解决:

typescript 复制代码
this.avSessionController.setAVMetadata();

总结

核心技术回顾

  1. AVSession 媒体会话

    • 创建和激活会话
    • 设置媒体元数据
    • 监听控制命令
    • 同步播放状态
  2. 后台播放管理

    • 申请后台任务权限
    • 启动/停止长时任务
    • 配置 WantAgent
  3. 状态同步机制

    • AppStorage 全局状态
    • AVSession 状态同步
    • Preferences 持久化

开发要点

  • AVSession 必须激活才能显示
  • 状态变化及时同步到 AVSession
  • 后台播放需要申请权限和长时任务
  • 资源使用完毕及时释放
  • 监听器使用完毕及时注销

参考资源


至此,HarmonyOS 音频播放器开发教程(上下篇)全部完成。结合两篇教程,你已经掌握了从底层音频播放到系统集成的完整技术栈,可以开发功能完善的音频播放应用。

相关推荐
Miguo94well2 小时前
Flutter框架跨平台鸿蒙开发——失物招领APP的开发流程
flutter·华为·harmonyos
实时云渲染dlxyz66882 小时前
鸿蒙系统下,点盾云播放器使用一段时间后忽然读取不到视频解决方法
音视频·harmonyos·点盾云播放·纯鸿蒙系统播放·应用权限授权
小风呼呼吹儿2 小时前
Flutter 框架跨平台鸿蒙开发 - 全国公积金查询:智能公积金管理助手
flutter·华为·harmonyos
大雷神2 小时前
HarmonyOS智慧农业管理应用开发教程--高高种地--第11篇:任务管理与提醒系统
harmonyos
翰德恩咨询2 小时前
DSTE咨询洞见:华为战略管理体系的进化之路
华为·华为战略·dste
zilikew2 小时前
Flutter框架跨平台鸿蒙开发——小说人物生成APP开发流程
flutter·华为·harmonyos·鸿蒙
ShuiShenHuoLe2 小时前
记录一次在使用beta版DevecoStudio打包的问题
harmonyos
zilikew2 小时前
Flutter框架跨平台鸿蒙开发——图书馆座位预约APP开发流程
flutter·华为·harmonyos·鸿蒙
Easonmax2 小时前
基础入门 React Native 鸿蒙跨平台开发:积分商城页面实现(积分商品+兑换+记录)
react native·react.js·harmonyos