HarmonyOS 后台音频实战:AVSession 与后台播放保活

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1. 为什么需要后台音频

疗愈类 App 的播放场景很特殊:用户可能打开音频后锁屏,或者切到其他应用。如果一切到后台就停止,体验会非常割裂。

“律愈”项目通过三层能力支撑后台播放:

  • module.json5 声明后台音频模式和权限。
  • backgroundTaskManager 开启后台任务。
  • AVSession 把当前播放信息同步到系统媒体中心。

2. 配置后台能力

module.json5 中,项目声明:

{
  "requestPermissions": [
    {
      "name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
    }
  ],
  "abilities": [
    {
      "name": "EntryAbility",
      "backgroundModes": [
        "audioPlayback"
      ]
    }
  ]
}

这一步是基础。如果没有声明后台模式,后续服务代码写得再完整,也无法得到预期效果。

3. 后台音频服务类

项目将后台能力封装在 YulvBackgroundAudioService

export class YulvBackgroundAudioService {
  private readonly ctx: common.Context;
  private session: avSession.AVSession | null = null;
  private backgroundRunning: boolean = false;

  constructor(ctx: common.Context) {
    this.ctx = ctx;
  }
}

这个类不直接播放音频。真正播放由 ToneAudioPlayer 完成,它只负责系统层面的“播放状态声明”和“后台任务管理”。

4. 创建 AVSession

private async ensureSession(): Promise<avSession.AVSession | null> {
  if (this.session !== null) {
    return this.session;
  }
  try {
    this.session = await avSession.createAVSession(this.ctx, 'YulvToneAudio', 'audio');
    await this.session.activate();
    return this.session;
  } catch (_e) {
    this.session = null;
    return null;
  }
}

AVSession 的作用是告诉系统:当前应用正在进行媒体播放。它也方便系统媒体中心展示标题、艺术家、播放状态等信息。

5. 构建 WantAgent

后台任务需要一个 WantAgent,用于用户从系统入口重新回到应用:

private async buildWantAgent(): Promise<WantAgent> {
  const launchWant: Want = {
    bundleName: this.ctx.applicationInfo.name,
    abilityName: 'EntryAbility'
  };
  const info: YulvWantAgentInfo = {
    wants: [launchWant],
    actionType: abilityWantAgent.OperationType.START_ABILITY,
    requestCode: 20260611,
    actionFlags: [abilityWantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
  };
  return abilityWantAgent.getWantAgent(info);
}

这里的 requestCode 可以理解为这个后台任务入口的标识。

6. 启动后台运行

private async startBackgroundTask(): Promise<void> {
  if (this.backgroundRunning) {
    return;
  }
  try {
    const agent: WantAgent = await this.buildWantAgent();
    await backgroundTaskManager.startBackgroundRunning(this.ctx, ['audioPlayback'], agent);
    this.backgroundRunning = true;
  } catch (_e) {
    this.backgroundRunning = false;
  }
}

这里做了一个状态保护:如果已经在后台运行,就不重复开启。

7. 播放时更新媒体信息

当音频开始播放时,项目调用 markPlaying()

async markPlaying(tone: ToneKey): Promise<void> {
  const session: avSession.AVSession | null = await this.ensureSession();
  if (session !== null) {
    const meta = wuYinMetaOf(tone);
    const metadata: YulvAVMetadata = {
      assetId: `yulv_${tone}`,
      title: `${meta.name}疗愈`,
      artist: '律愈',
      album: `${meta.element} · ${meta.organ}`,
      subtitle: meta.description,
      duration: 6 * 1000
    };
    const state: YulvAVPlaybackState = {
      state: avSession.PlaybackState.PLAYBACK_STATE_PLAY,
      speed: 1
    };
    await session.setAVMetadata(metadata);
    await session.setAVPlaybackState(state);
    await session.activate();
  }
  await this.startBackgroundTask();
}

这里的设计很清晰:先更新系统媒体信息,再启动后台任务。

8. 暂停和释放

暂停时更新播放状态,并停止后台任务:

async markPaused(): Promise<void> {
  if (this.session !== null) {
    const state: YulvAVPlaybackState = {
      state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
      speed: 0
    };
    await this.session.setAVPlaybackState(state);
  }
  await this.stopBackgroundTask();
}

释放时销毁 AVSession:

async release(): Promise<void> {
  await this.stopBackgroundTask();
  if (this.session !== null) {
    await this.session.deactivate();
    await this.session.destroy();
    this.session = null;
  }
}

9. 与播放器的关系

ToneAudioPlayer 在真正播放前后调用后台服务:

if (this.loadedTone !== null) {
  await this.backgroundAudio.markPlaying(this.loadedTone);
}
await pl.play();

暂停时:

async pause(): Promise<void> {
  this.playAfterPrepared = false;
  if (this.avPlayer !== null && this.avPlayer.state === 'playing') {
    await this.avPlayer.pause();
  }
  await this.backgroundAudio.markPaused();
}

10. 流程图

用户点击播放

ToneAudioPlayer.playTone

AVPlayer prepared

YulvBackgroundAudioService.markPlaying

create or activate AVSession

set metadata and playback state

startBackgroundRunning audioPlayback

用户暂停

markPaused

set playback pause

stopBackgroundRunning

11. 小结

后台音频不是单一 API 能解决的问题。它需要配置、媒体会话、后台任务和播放器状态一起配合。

这个项目把后台播放封装为独立服务,是比较推荐的写法。页面只关心“播放/暂停”,播放器只关心“音频源和状态机”,后台服务只关心“系统媒体状态和后台运行”。边界清楚,后续维护会轻松很多。

9. 后台音频的三个层次

后台播放不是“加一个权限”就完成了。律愈项目里有三个层次:

ext 配置层:module.json5 声明权限和 backgroundModes 播放层:ToneAudioPlayer 真正播放音频 系统层:AVSession + backgroundTaskManager 告诉系统当前是音频任务

10. AVSession 元数据的意义

setAVMetadata() 不只是装饰信息,它让系统媒体中心知道当前播放的是什么:

s const metadata: YulvAVMetadata = { assetId: yulv_, title: ${meta.name}疗愈, artist: '律愈', album: ${meta.element} · , subtitle: meta.description, duration: 6 * 1000 };

对于音乐、冥想、课程类应用,媒体中心展示质量会直接影响用户体验。

11. WantAgent 为什么存在

后台任务需要一个可返回应用的入口:

s const launchWant: Want = { bundleName: this.ctx.applicationInfo.name, abilityName: 'EntryAbility' };

这说明后台音频不是孤立运行,它仍然和主 Ability 保持关联。用户从系统媒体入口回到应用时,需要知道启动哪个 Ability。

12. 播放和后台状态要同步

播放器播放成功后调用:

s await this.backgroundAudio.markPlaying(this.loadedTone); await pl.play();

暂停时调用:

s await this.backgroundAudio.markPaused();

这个同步很重要。如果音频暂停了但后台任务还在跑,系统层状态就不准确;如果音频播放了但 AVSession 没更新,系统媒体中心也无法正确展示。

13. 异常处理策略

项目中后台服务的异常大多被捕获并忽略,这是合理的:

s try { await session.setAVMetadata(metadata); await session.setAVPlaybackState(state); await session.activate(); } catch (_e) { // 不阻断实际播放 }

原因是 AVSession 失败不应该影响基本播放能力。文章中可以解释这种降级策略:后台能力是增强体验,主播放链路才是核心。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐