👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
   我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
  
  🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
  🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
  💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
  
   如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!🚀

前言

做一个音乐播放器时,页面里的播放、暂停按钮通常不难:业务层拿到播放器对象,调用对应接口即可。

真正容易被忽略的是另一个问题:用户离开播放页面以后,系统播控中心、锁屏界面或者外部控制入口,为什么还能知道当前播放的是哪首歌?又为什么能够继续向应用发送播放、暂停、上一首、下一首等命令?

答案并不在 AVPlayer 本身,而在 AVSession Kit。

HarmonyOS 官方把 AVSession Kit 定义为音视频播控服务。音视频应用以媒体会话提供方的身份接入以后,可以向系统提供媒体信息和播放状态,同时接收控制端发送的播放控制命令。换句话说,AVPlayer 负责“真正把媒体播出来”,AVSession 负责“让系统知道播放器现在在干什么,以及系统该怎么控制它”。

这次就围绕一个最小场景展开:音乐正在应用内播放,用户离开播放页面后,系统播控中心仍然能够显示歌曲标题、歌手、封面和播放状态,并能通过播放、暂停、上一首、下一首控制应用播放器。

一、AVPlayer 和 AVSession 解决的不是同一个问题

理解这个能力,先要把播放层和控制层分开。

Media Kit 提供的 AVPlayer 面向实际媒体播放;AVSession Kit 则提供媒体会话和播控能力。HarmonyOS 官方文档中心也将两者放在不同的 Kit 中:Media Kit 提供 AVPlayer、AVRecorder 等媒体能力,而 AVSession Kit 是系统提供的音视频管控服务。

可以把它们理解成这样:

能力主要职责
AVPlayer加载媒体、播放、暂停、跳转、停止等实际播放操作
AVSession向系统发布歌曲信息和播放状态,接收系统播控命令
系统播控中心作为控制端,根据 AVSession 提供的信息展示并下发控制命令

所以,一个应用即使已经能正常播放音乐,也不代表系统播控中心天然就知道它当前播放的是谁。

应用需要建立一个媒体 Session,把播放器状态主动同步出去。

而当用户在系统播控中心点击“暂停”时,也不是系统直接操作 AVPlayer,而是控制命令先进入 AVSession,应用收到事件以后,再调用自己的播放器暂停。

这层关系是理解后面所有代码的关键。

二、先确认 HarmonyOS 7 和 API 版本

截至当前官方 HarmonyOS 7 升级适配文档,HarmonyOS 7.0 对应 API 版本 26.0.0。华为同时说明,从 26.0.0 开始,HarmonyOS 开发套件 API 版本号改用 X.Y.Z 形式的语义化版本。

本文使用的核心 AVSession 能力并不是 HarmonyOS 7 才加入的新接口。

官方 AVSession Provider 文档中,下面这些核心接口均标记为 API version 10+:

  • createAVSession()
  • setAVMetadata()
  • setAVPlaybackState()
  • activate()
  • deactivate()
  • destroy()
  • getController()
  • setAVQueueItems()
  • setAVQueueTitle()

因此,在 HarmonyOS 7 / API 26.0.0 工程中可以继续使用这些能力。

AVSession 的核心系统能力为:

SystemCapability.Multimedia.AVSession.Core

官方当前 API 参考中也明确保留了这些媒体会话接口,并持续在 AVSession Kit 中增加新能力。

这里有一个容易误解的地方:

“HarmonyOS 7 支持”不等于“这个接口从 HarmonyOS 7 才支持”。

本文涉及的大多数基础媒体会话接口起始版本其实远早于 API 26。

三、搭一个最小实践场景

假设应用已经有自己的播放器控制层,可以完成下面四件事:

play()
pause()
playPrevious()
playNext()

这部分到底使用 AVPlayer、AudioRenderer,还是业务自己封装的播放器,不影响 AVSession 的基本思路。

我们只负责增加一层媒体会话:

播放器
   ↑
   │ 应用调用
   │
AVSession
   ↑ ↓
系统播控中心 / 其他控制端

实现目标有五个:

  1. 创建并激活一个音频 AVSession;
  2. 把当前歌曲信息同步给系统;
  3. 把播放器状态同步给系统;
  4. 接收系统发送的播放控制事件;
  5. 播放真正结束后释放 Session。

四、创建媒体 Session

ArkTS 侧通过 AVSession Kit 导入接口:

import { avSession } from '@kit.AVSessionKit';

建立媒体会话时,调用 createAVSession(),第三个参数使用 'audio' 表示音频会话。

import { avSession } from '@kit.AVSessionKit';

class MediaSessionController {
  private session?: avSession.AVSession;

  async init(context: Context): Promise<void> {
    this.session = await avSession.createAVSession(
      context,
      'MUSIC_SESSION',
      'audio'
    );

    await this.session.activate();
  }
}

官方示例同样采用:

avSession.createAVSession(context, 'SESSION_NAME', 'audio')

创建 Session 后再调用 activate() 激活媒体会话。官方当前的音频实践示例也是先创建 AVSession,随后立即激活。

真正需要关注的不是代码长度,而是两个动作:

createAVSession()
        ↓
activate()

创建只代表媒体会话对象已经存在。

激活后,Session 才进入正常提供媒体控制信息的工作状态。

另外,官方 Provider 文档说明,一个 UIAbility 只能创建一个 AVSession,因此实际项目里更适合把它放在播放器管理层,而不是每进入一次播放页面就创建一个新的 Session。

这也直接关系到“退出页面以后还能不能控制”。

如果 AVSession 生命周期完全绑在某个 ArkUI 页面上,页面一销毁就顺手把 Session 销毁,那么系统自然也失去了对应的媒体会话。

五、系统为什么知道当前播放的是哪首歌

创建 Session 以后,系统仍然不知道当前播放内容。

这一步需要 AVMetadata。

官方文档将 AVMetadata 定义为媒体相关属性集合,其中可以包含媒体资源标识、标题、歌手、专辑、时长等信息。应用通过 setAVMetadata() 将它同步到媒体会话。

例如:

async updateMetadata(
  assetId: string,
  title: string,
  artist: string,
  duration: number
): Promise<void> {
  if (!this.session) {
    return;
  }

  const metadata: avSession.AVMetadata = {
    assetId,
    title,
    artist,
    duration
  };

  await this.session.setAVMetadata(metadata);
}

如果已经获得用于展示的媒体图片,也可以向 AVMetadata 写入 mediaImage。HarmonyOS 官方音频播控实践正是通过 assetId、title、artist、mediaImage 和 duration 等字段,把当前音乐信息更新到播控中心。

因此,换歌以后除了让播放器加载下一首,还要同步更新 Metadata:

加载新歌曲
   ↓
开始播放
   ↓
setAVMetadata()
   ↓
系统播控中心刷新标题 / 歌手 / 封面

这也是很常见的状态不同步来源:

应用页面已经显示下一首了,但 AVSession 中仍然保留上一首的 Metadata。

系统播控中心并不会自动读取你的 ArkUI 页面状态,它看到的是应用通过 AVSession 上报的媒体信息。

六、播放状态也必须主动同步

媒体信息解决“现在播的是谁”,播放状态则解决“它现在是在播放还是暂停”。

AVSession 使用 AVPlaybackState 保存会话的播放状态、播放位置、速度等信息。官方文档明确说明,应用播放状态发生变化后,应通过 setAVPlaybackState() 向播控中心同步状态。

最基本的播放和暂停可以这样处理:

async reportPlaying(): Promise<void> {
  if (!this.session) {
    return;
  }

  await this.session.setAVPlaybackState({
    state: avSession.PlaybackState.PLAYBACK_STATE_PLAY
  });
}

async reportPaused(): Promise<void> {
  if (!this.session) {
    return;
  }

  await this.session.setAVPlaybackState({
    state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE
  });
}

如果还需要让系统播控中心知道当前播放进度,可以继续同步 position:

async reportPosition(positionMs: number): Promise<void> {
  if (!this.session) {
    return;
  }

  await this.session.setAVPlaybackState({
    position: {
      elapsedTime: positionMs,
      updateTime: new Date().getTime()
    }
  });
}

官方当前音频实践也采用同样结构:

position: {
  elapsedTime: ms,
  updateTime: new Date().getTime()
}

并使用:

avSession.PlaybackState.PLAYBACK_STATE_PLAY
avSession.PlaybackState.PLAYBACK_STATE_PAUSE

表示播放和暂停。

这里比较容易出现一个理解偏差:

AVSession 的状态不是播放器状态的自动镜像。

比如业务代码执行:

await player.pause();

并不会因为播放器已经暂停,就自动让你自己维护的 AVSession 状态也完成同步。

应用仍然需要在合适的位置调用:

await session.setAVPlaybackState(...)

所以更稳妥的结构是把两者放在同一条业务链路里:

async pauseMusic(): Promise<void> {
  await player.pause();
  await mediaSession.reportPaused();
}

这样应用页面、播放器和系统播控中心使用同一个状态源。

七、让系统的播放和暂停真正控制播放器

前面的 Metadata 和 PlaybackState 都是“应用 → 系统”。

控制事件则正好相反:

系统播控中心
      ↓
   AVSession
      ↓
应用播放器

AVSession 提供固定的播放控制事件。

官方 Provider 示例中可以直接监听:

session.on('play', ...)
session.on('pause', ...)
session.on('playPrevious', ...)
session.on('playNext', ...)

控制端只会把命令交给 Session,真正的播放器逻辑仍然由应用完成。

可以给 Session 注入播放器操作:

interface PlayerActions {
  play(): Promise<void>;
  pause(): Promise<void>;
  playPrevious(): Promise<void>;
  playNext(): Promise<void>;
}

class MediaSessionController {
  private session?: avSession.AVSession;

  async init(context: Context, player: PlayerActions): Promise<void> {
    this.session = await avSession.createAVSession(
      context,
      'MUSIC_SESSION',
      'audio'
    );

    await this.session.activate();

    this.session.on('play', async () => {
      await player.play();
      await this.reportPlaying();
    });

    this.session.on('pause', async () => {
      await player.pause();
      await this.reportPaused();
    });

    this.session.on('playPrevious', async () => {
      await player.playPrevious();
    });

    this.session.on('playNext', async () => {
      await player.playNext();
    });
  }

  async reportPlaying(): Promise<void> {
    if (!this.session) {
      return;
    }

    await this.session.setAVPlaybackState({
      state: avSession.PlaybackState.PLAYBACK_STATE_PLAY
    });
  }

  async reportPaused(): Promise<void> {
    if (!this.session) {
      return;
    }

    await this.session.setAVPlaybackState({
      state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE
    });
  }
}

真正需要关注的是回调里面做的事情。

例如系统下发:

session.on('playNext', ...)

应用并不是只把自己的数组索引加一。

完整业务通常应该包含:

切换业务歌曲
   ↓
播放器加载新媒体
   ↓
更新 AVMetadata
   ↓
更新 AVPlaybackState

否则就可能出现播放器已经切换成功,但控制中心仍然显示上一首歌的情况。

官方文档还特别说明:Session 注册了固定播控命令监听以后,这些命令会体现在控制端能够获取的有效命令集合中。因此,如果应用本身并不支持某个控制命令,就不应该注册一个空实现。

八、为什么退出播放页面以后 Session 不应该立即销毁

这正是文章主题里最核心的地方。

“退出播放页面”和“结束音乐播放”是两个完全不同的生命周期事件。

例如:

播放详情页关闭
        ↓
用户回到首页
        ↓
音乐仍然继续

此时播放器仍在工作,媒体会话也还有意义。

因此不能机械地在 ArkUI 页面的销毁生命周期里执行:

session.destroy();

否则页面虽然只是退出了,系统侧对应的媒体会话却已经被应用主动销毁。

更合理的做法是,让 AVSession 的生命周期跟随“播放器业务会话”,而不是跟随某个具体页面。

可以把它放进类似下面的层级:

UI 页面
   │
   ↓
PlayerManager
   ├── AVPlayer
   └── MediaSessionController

页面只负责显示和发出操作请求。

播放器与 AVSession 则由更长生命周期的业务管理对象维护。

但这里还要特别区分另一件事:

Session 没销毁,不等于应用已经具备后台持续播放能力。

九、页面退出、应用退后台和应用退出不是同一个概念

这是 AVSession 开发里最容易混淆的一组概念。

1. 只离开当前页面

例如从“歌曲详情页”返回应用首页。

如果播放器和 AVSession 没有跟着页面销毁,媒体会话可以继续存在。

2. 应用进入后台

例如用户回到桌面,但仍希望音乐持续播放。

这时仅仅创建 AVSession 并不够。

HarmonyOS 官方后台任务文档明确说明,应用进入后台后会受到系统后台管控;音频播放属于长时运行、用户可感知的业务场景,应使用长时任务机制。

华为当前音频播放实践进一步明确给出了后台音频方案:

先接入播控中心,然后申请后台运行权限、设置 audioPlayback 后台模式,并创建音频播放长时任务。

所以二者解决的问题分别是:

AVSession
解决:系统怎么认识、展示和控制播放器

Background Tasks
解决:应用进入后台后如何按系统规则继续执行音频播放任务

不能把它们当成同一个能力。

十、后台播放需要额外配置什么

如果业务明确需要音乐进入后台后继续播放,需要按照 Background Tasks Kit 的要求配置长时任务。

官方音频实践要求在 module.json5 中申请:

ohos.permission.KEEP_BACKGROUND_RUNNING

并将 Ability 的后台模式设置为:

audioPlayback

对应配置结构可以整理为:

{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        // 其他 Ability 配置……
        "backgroundModes": [
          "audioPlayback"
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
        "reason": "$string:reason_background",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"
        }
      }
    ]
  }
}

HarmonyOS 的长时任务接口 backgroundTaskManager.startBackgroundRunning() 明确要求 ohos.permission.KEEP_BACKGROUND_RUNNING,音视频播放对应 BackgroundMode.AUDIO_PLAYBACK。

也就是说,如果需求只是:

从播放页返回应用首页后,系统控制中心仍然可以控制音乐。

重点是播放器和 AVSession 生命周期设计。

如果需求已经变成:

应用退到后台甚至熄屏以后,音乐仍要持续播放。

就必须继续进入 Background Tasks Kit 的开发范围。

十一、系统控制中心为什么会出现上一首和下一首

系统不是看到你的歌单以后自动猜出应用支持哪些控制。

AVSession 的固定控制事件实际上构成了应用向控制端暴露的控制能力。

例如:

this.session.on('play', this.onPlay);
this.session.on('pause', this.onPause);
this.session.on('playNext', this.onPlayNext);
this.session.on('playPrevious', this.onPlayPrevious);

官方音频实践就是通过这些监听响应播控中心下发的命令。

因此不要为了让控制中心“看起来功能很多”,把自己不支持的事件全注册一遍。

比如应用根本没有上一首逻辑,就不要写:

session.on('playPrevious', () => {
  // 什么也不做
});

Provider 官方指南明确提示,已经注册的固定控制命令应该提供实际处理逻辑;如果命令暂时不需要,应取消监听。

十二、播放结束时怎么释放 Session

当用户只是退出页面但音乐仍然需要播放时,不应该释放媒体会话。

但如果业务已经真正结束:

用户停止播放
播放器不再保留当前媒体
后台播放任务结束

就应该清理监听并销毁 AVSession。

官方示例给出的取消方式包括:

session.off('play');
session.off('pause');
session.off('stop');
session.off('playNext');
session.off('playPrevious');

随后调用 destroy()。

可以把释放过程集中起来:

async release(): Promise<void> {
  if (!this.session) {
    return;
  }

  this.session.off('play');
  this.session.off('pause');
  this.session.off('playNext');
  this.session.off('playPrevious');

  await this.session.destroy();
  this.session = undefined;
}

这里同样建议把释放动作放进播放器业务层,而不是某个具体页面的 aboutToDisappear()。

一个简单判断标准是:

页面消失了,播放器是否还应该存在?

如果答案是“应该”,那么 AVSession 通常也不应该因为这个页面消失而直接销毁。

十三、一个更完整的最小结构

把前面的代码合起来,可以整理成一个比较清晰的 Session 管理层:

import { avSession } from '@kit.AVSessionKit';

interface PlayerActions {
  play(): Promise<void>;
  pause(): Promise<void>;
  playPrevious(): Promise<void>;
  playNext(): Promise<void>;
}

export class MediaSessionController {
  private session?: avSession.AVSession;

  async init(context: Context, player: PlayerActions): Promise<void> {
    if (this.session) {
      return;
    }

    this.session = await avSession.createAVSession(
      context,
      'MUSIC_SESSION',
      'audio'
    );

    await this.session.activate();

    this.session.on('play', async () => {
      await player.play();
      await this.reportPlaying();
    });

    this.session.on('pause', async () => {
      await player.pause();
      await this.reportPaused();
    });

    this.session.on('playPrevious', async () => {
      await player.playPrevious();
    });

    this.session.on('playNext', async () => {
      await player.playNext();
    });
  }

  async updateMetadata(
    assetId: string,
    title: string,
    artist: string,
    duration: number
  ): Promise<void> {
    if (!this.session) {
      return;
    }

    const metadata: avSession.AVMetadata = {
      assetId,
      title,
      artist,
      duration
    };

    await this.session.setAVMetadata(metadata);
  }

  async reportPlaying(): Promise<void> {
    if (!this.session) {
      return;
    }

    await this.session.setAVPlaybackState({
      state: avSession.PlaybackState.PLAYBACK_STATE_PLAY
    });
  }

  async reportPaused(): Promise<void> {
    if (!this.session) {
      return;
    }

    await this.session.setAVPlaybackState({
      state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE
    });
  }

  async reportPosition(positionMs: number): Promise<void> {
    if (!this.session) {
      return;
    }

    await this.session.setAVPlaybackState({
      position: {
        elapsedTime: positionMs,
        updateTime: new Date().getTime()
      }
    });
  }

  async release(): Promise<void> {
    if (!this.session) {
      return;
    }

    this.session.off('play');
    this.session.off('pause');
    this.session.off('playPrevious');
    this.session.off('playNext');

    await this.session.destroy();
    this.session = undefined;
  }
}

这段代码刻意没有塞进媒体 URL、AVPlayer 状态机、网络加载、歌词和歌单等业务。

它只解决一件事情:

把已经存在的播放器接入 HarmonyOS 媒体会话。

根据官方接口定义,这套结构可以作为 AVSession 接入时的最小骨架;真正项目中仍应结合实际播放器状态机,并在目标 HarmonyOS SDK 与设备环境中完成编译和真机验证。

十四、几个容易理解错的地方

1. AVSession 不是播放器

它不替代 AVPlayer。

收到:

session.on('pause', ...)

以后,应用仍然需要在回调中执行真正的播放器暂停逻辑。

2. 播放器状态变化不会自动同步到系统

播放器播放以后,需要上报:

setAVPlaybackState()

切换歌曲以后,需要更新:

setAVMetadata()

官方音频播控实践明确把这两步作为应用状态同步播控中心的核心操作。

3. 返回上一页不等于销毁 Session

如果音乐仍需继续播放,就不应该把 AVSession 生命周期绑死在播放页面生命周期上。

4. AVSession 不等于后台播放权限

接入 AVSession 解决播控问题。

真正要让应用进入后台后持续播放音频,还需要符合后台任务规范,申请 KEEP_BACKGROUND_RUNNING 并使用 audioPlayback 长时任务模式。

5. 不支持的控制命令不要注册空回调

控制端会根据 Session 暴露出的有效控制命令进行交互。官方文档明确建议,不支持的控制命令不注册,暂时不用的监听应取消。

十五、实际项目可以按这个顺序排查

当出现“应用里能播放,但系统控制中心不正常”的问题时,可以先按照下面这条链路检查。

第一步,看 Session 有没有创建并激活。

确认是否执行:

createAVSession()
activate()

第二步,看 Metadata 有没有上报。

重点检查:

assetId
title
artist
mediaImage
duration

如果应用已经切歌,还要确认是否再次调用了 setAVMetadata()。

第三步,看播放状态是否同步。

播放和暂停以后确认有没有调用:

setAVPlaybackState()

特别是业务自己的播放按钮和系统控制中心按钮,都应该最终走到同一套状态同步逻辑。

第四步,看控制命令监听。

确认需要的能力是否注册:

play
pause
playPrevious
playNext

同时确认回调里面确实调用了播放器,而不是只有日志。

第五步,看 Session 生命周期。

如果一返回页面控制中心就失效,优先检查有没有把:

destroy()

放进页面退出生命周期。

第六步,看后台任务。

如果前台正常,一进入后台或熄屏以后播放异常,就不要继续只排查 AVSession。

官方当前音频问题排查文档明确指出,前后台播放存在差异时,需要检查应用是否正确接入 AVSession,以及是否申请了长时任务。

开发经验总结

AVSession 的接口本身并不复杂,真正需要理清的是状态归属。

第一,播放器和媒体会话是两层能力。 AVPlayer 负责实际播放,AVSession 负责把播放器接入 HarmonyOS 系统播控体系。

第二,系统看到的是应用主动发布的状态。 当前歌曲发生变化,要更新 AVMetadata;播放、暂停和进度发生变化,要更新 AVPlaybackState。

第三,系统控制最终还是回到应用播放器。 on('play')、on('pause')、on('playPrevious')、on('playNext') 只是控制命令入口,真正的播放逻辑仍由应用完成。

第四,Session 生命周期不要简单绑定页面生命周期。 用户离开播放详情页,不代表媒体播放已经结束。

第五,媒体会话和后台播放要分开理解。 AVSession 让系统能够展示和控制播放器;应用真正进入后台后持续执行音频播放,还需要按照 Background Tasks Kit 的长时任务规范处理。

如果正在做自己的音乐播放器,可以重点检查一个问题:当前播放器的播放状态、页面状态和 AVSession 状态,到底是不是从同一个业务状态源同步出来的? 很多播控不同步的问题,最后并不出在控制中心,而是这三份状态已经各走各的了。

📝 写在最后

如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!

我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!

感谢你的阅读,我们下篇文章再见~👋

✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐