AVSession封面

做了个音乐播放器,用户反馈说锁屏的时候看不到歌曲信息,也不能在控制中心切歌,每次都要解锁打开 App 才能操作。这个体验确实差,系统播放器都支持锁屏控制,我们的播放器也得支持。

HarmonyOS 7 的 AVSession 就是干这个的——把应用的播放状态和媒体元数据暴露给系统,锁屏界面、控制中心、蓝牙耳机都能控制播放。这篇就讲讲怎么接入 AVSession,让播放器变成系统级的媒体应用。

一、真实开发中遇到的问题

最开始以为就是个通知,把歌曲信息放上去就行。真做了才发现不是那么回事:

第一,锁屏界面显示什么?歌名、歌手、专辑封面,这些元数据怎么传给系统?什么时候更新?

第二,控制中心的播放暂停按钮怎么联动?用户在控制中心点了暂停,我们的应用怎么收到这个事件?点了上一首下一首呢?

第三,蓝牙耳机按键怎么响应?用户按耳机上的播放键,应用怎么收到?

第四,播放状态同步。应用里点了暂停,锁屏上的按钮状态也要跟着变。状态不一致怎么办?

二、这个能力怎么接入

AVSession 的接入思路:

1. 创建 AVSession:调用 AVSessionManager.createAVSession() 创建一个媒体会话,指定会话类型(音频/视频)。

2. 设置媒体元数据:歌名、歌手、专辑名、封面图,通过 setAVMetadata() 传给系统。

3. 设置播放状态:当前播放状态(播放中/暂停/停止)、播放进度,通过 setAVPlaybackState() 更新。

4. 注册事件监听:监听系统发来的控制事件——播放、暂停、上一首、下一首、快进快退。

5. 销毁会话:应用退出或者播放器销毁的时候,release() 释放会话。

整个过程就是:应用告诉系统"我在放这首歌",系统把信息显示在锁屏和控制中心;用户在系统侧操作,系统把事件发给应用,应用执行对应的操作。

AVSession架构交互图

三、关键代码怎么写

下面是封装的 AVSession 管理器,文件位置在 entry/src/main/ets/utils/AVSessionManager.ets

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

export class AVSessionManager {
  private session: avSession.AVSession | null = null;
  private controller: avSession.AVSessionController | null = null;

  // 初始化媒体会话
  async initSession(): Promise<void> {
    // 创建会话
    this.session = await avSession.createAVSession(
      getContext(this).resourceManager,
      'MusicPlayer_' + Date.now().toString(),
      'audio'
    );

    // 获取控制器
    this.controller = this.session.getController();

    // 注册控制事件监听
    this.session.on('play', () => {
      console.info('System command: play');
      // 调用播放接口
      this.onPlayCallback?.();
    });

    this.session.on('pause', () => {
      console.info('System command: pause');
      this.onPauseCallback?.();
    });

    this.session.on('playNext', () => {
      console.info('System command: next');
      this.onNextCallback?.();
    });

    this.session.on('playPrevious', () => {
      console.info('System command: previous');
      this.onPrevCallback?.();
    });

    // 激活会话
    await this.session.activate();
  }

  // 更新媒体元数据(切歌时调用)
  async updateMetadata(track: TrackInfo): Promise<void> {
    if (!this.session) return;

    const metadata: avSession.AVMetadata = {
      title: track.title,
      artist: track.artist,
      album: track.album,
      duration: track.duration,
      mediaImage: track.coverUrl  // 封面图 URI
    };

    await this.session.setAVMetadata(metadata);
  }

  // 更新播放状态(播放/暂停/进度变化时调用)
  async updatePlaybackState(state: PlaybackState): Promise<void> {
    if (!this.session) return;

    const playbackState: avSession.AVPlaybackState = {
      state: state.isPlaying 
        ? avSession.PlaybackState.PLAYBACK_STATE_PLAY 
        : avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
      time: {
        elapsedTime: state.currentPosition,
        updateTime: Date.now()
      },
      speed: 1.0
    };

    await this.session.setAVPlaybackState(playbackState);
  }

  // 注册回调
  onPlayCallback: (() => void) | null = null;
  onPauseCallback: (() => void) | null = null;
  onNextCallback: (() => void) | null = null;
  onPrevCallback: (() => void) | null = null;

  // 销毁会话
  async release(): Promise<void> {
    if (this.session) {
      await this.session.deactivate();
      await this.session.destroy();
      this.session = null;
    }
  }
}

// 歌曲信息
interface TrackInfo {
  title: string;
  artist: string;
  album: string;
  duration: number;
  coverUrl: string;
}

// 播放状态
interface PlaybackState {
  isPlaying: boolean;
  currentPosition: number;
}

这段代码的核心:createAVSession 创建会话,注册 play/pause/next/previous 事件监听。updateMetadata 在切歌的时候调用,把新歌的信息传给系统。updatePlaybackState 在播放状态变化的时候调用,同步给系统。

播放器页面怎么集成?文件位置 pages/PlayerPage.ets

@Entry
@Component
struct PlayerPage {
  @State currentTrack: TrackInfo = {} as TrackInfo;
  @State isPlaying: boolean = false;
  private avSession: AVSessionManager = new AVSessionManager();

  async aboutToAppear() {
    await this.avSession.initSession();

    // 绑定回调:系统发来的控制指令
    this.avSession.onPlayCallback = () => this.play();
    this.avSession.onPauseCallback = () => this.pause();
    this.avSession.onNextCallback = () => this.nextTrack();
    this.avSession.onPrevCallback = () => this.prevTrack();
  }

  // 播放
  async play() {
    // 实际播放逻辑...
    this.isPlaying = true;
    await this.avSession.updatePlaybackState({
      isPlaying: true,
      currentPosition: 0
    });
  }

  // 暂停
  async pause() {
    this.isPlaying = false;
    await this.avSession.updatePlaybackState({
      isPlaying: false,
      currentPosition: 30000  // 当前进度
    });
  }

  // 切歌
  async onTrackChanged(track: TrackInfo) {
    this.currentTrack = track;
    await this.avSession.updateMetadata(track);
  }

  async aboutToDisappear() {
    await this.avSession.release();
  }

  build() {
    Column() {
      // 专辑封面
      Image(this.currentTrack.coverUrl)
        .width(240)
        .height(240)
      
      // 歌名歌手
      Text(this.currentTrack.title)
        .fontSize(20)
      Text(this.currentTrack.artist)
        .fontSize(14)
      
      // 控制按钮
      Row({ space: 48 }) {
        Button('上一首').onClick(() => this.prevTrack())
        Button(this.isPlaying ? '暂停' : '播放')
          .onClick(() => {
            if (this.isPlaying) this.pause();
            else this.play();
          })
        Button('下一首').onClick(() => this.nextTrack())
      }
      .margin({ top: 32 })
    }
    .padding(24)
  }
}

控制中心播放卡片效果

四、运行过程中怎么处理异常

AVSession 的异常场景:

1. 会话创建失败:设备不支持 AVSession,或者系统资源不足。catch 住异常,降级成普通通知,不影响播放功能本身。

2. 激活失败:activate() 失败了,说明系统不认可这个会话。锁屏控制就用不了,但应用内播放正常。

3. 状态不同步:应用内暂停了,但锁屏上的状态没更新。要确保每次状态变化都调用 updatePlaybackState(),不要漏。

4. 多个会话冲突:如果有两个播放器同时激活 AVSession,系统会怎么处理?一般是后激活的顶掉前面的。要确保只有一个活跃会话。

五、实际开发中容易忽略的问题

1. 封面图要传 URI:mediaImage 是图片的 URI,不是 base64。本地封面图要用 file:// 协议或者资源 URI。

2. 进度更新不要太频繁:每播一秒就 updatePlaybackState 一次,锁屏上的进度条会刷新。太频繁了没必要,每秒一次够了。

3. 会话要及时销毁:播放器退出的时候一定要 release(),不然系统媒体列表里一直挂着你的应用,用户以为还在播放。

4. 蓝牙按键也要支持:AVSession 注册之后,蓝牙耳机的播放/暂停键自动就支持了,不用额外写代码。但要测试不同品牌的耳机兼容性。

5. 后台播放:AVSession 激活之后,应用切到后台还能继续播放。但要声明后台任务权限,不然系统会杀。

AVSession 是播放器应用的标配。接入之后,锁屏、控制中心、蓝牙耳机都能控制播放,体验一下子就专业了。代码量不大,但细节不少——元数据更新时机、状态同步、资源释放,每一步都要做到位。

Logo

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

更多推荐