基于AVSession的媒体播放控制方案:设计与实现

108 阅读5分钟

前言

大家好,我是simple。我的理想是利用科技手段来解决生活中遇到的各种问题

在媒体类应用(如音乐、播客、视频播放器)中,用户常常需要通过多种方式控制播放——可能是点击应用内的按钮,可能是按耳机线控,也可能是滑动系统通知栏。这些分散的控制指令如果处理不当,很容易出现状态混乱(比如“同时收到播放和暂停指令”),或者设备间状态不同步(比如手机显示“播放中”,但手表显示“已暂停”)。

这套基于AVSession的实现方案,核心就是解决这些问题:通过统一的媒体会话管理,让所有控制指令走同一个“入口”,确保播放状态在任何设备上都一致,同时高效管理资源,避免内存浪费。

核心功能与实现架构

整个方案由两个核心类协作完成:

  • AVSessionPlayerManager:负责创建和管理全局唯一的AVSession实例(单例模式)。媒体会话就像一个“指挥中心”,全局只能有一个,否则会出现“多头指挥”的混乱,这个类就是保证这一点的。

  • AVSessionPlayerController:处理具体的“指挥”工作——注册控制指令的监听(比如“播放”“暂停”)、设置歌曲信息(如歌名、歌手)、更新播放状态(如“正在播放”“已暂停”),以及最后释放资源。

完整实现流程

1. 初始化会话:创建“指挥中心”

使用AVSessionPlayerManager的createSession方法初始化会话。因为用了单例模式,第一次调用会创建会话并激活,后续调用直接返回已有的实例,避免重复创建导致冲突。

// 首次调用创建会话,后续调用直接返回已有实例
const session = await AVSessionPlayerManager.createSession(context);

2. 注册事件监听:搭建“指令接收线”

会话创建后,通过AVSessionPlayerController的registerEventListeners方法注册所有支持的控制指令(播放、暂停、下一首等)。这里用了一个事件映射表,把指令名和对应的处理函数关联起来,既清晰又方便管理。

注册后,不管是来自耳机的“暂停”,还是通知栏的“下一首”,都会通过这个“接收线”进入统一处理流程。

3. 设置媒体信息:告诉“指挥中心”播放内容

通过setMediaInfo方法,把当前播放的媒体信息(如歌名、封面)、播放队列(比如“播放列表”)传给会话。同时会初始化播放器,从队列中取第一首开始播放,并同步更新播放状态(比如“正在播放”“已收藏”)。

这样一来,系统或其他设备就能通过AVSession获取到完整的媒体信息(比如通知栏显示当前歌曲名)。

4. 处理控制指令:统一响应操作

当收到控制指令(比如“播放”)时,Controller会调用对应的处理方法(handlePlay),操作播放器执行实际动作(如avplayer.play()),并同步更新会话状态。

比如用户按了耳机的“播放键”,指令会触发handlePlay,播放器开始播放,同时会话状态更新为“播放中”——手机、手表、通知栏都会同步显示这个状态。

5. 销毁资源:退出时“清场”

当应用退出或切换场景时,调用destroy方法释放资源:先移除所有事件监听(避免“已退出但还在接收指令”),再释放播放器,最后销毁会话,确保没有内存泄漏。

关键代码解析

单例模式:保证会话唯一

AVSessionPlayerManager的核心设计是单例模式,通过静态属性session存储唯一实例,避免重复创建:

// AVSessionPlayerManager中的单例实现
static async createSession(context: Context): Promise<avSession.AVSession> {
  // 已有会话直接返回,不重复创建
  if (AVSessionPlayerManager.session) {
    return AVSessionPlayerManager.session;
  }
  // 首次创建并激活会话
  AVSessionPlayerManager.session = await avSession.createAVSession(context, 'audio_test', 'audio');
  await AVSessionPlayerManager.session.activate();
  return AVSessionPlayerManager.session;
}

这种设计从根源上避免了“多个会话同时存在”导致的冲突。

事件注册:统一管理指令监听

Controller的registerEventListeners方法用“事件-处理函数”映射表管理所有指令,既减少重复代码,又方便后续维护:

// 事件与处理函数的映射表
const eventHandlers: Record<SupportedEvents, (...args: any[]) => void> = {
  play: () => this.handlePlay(),
  pause: () => this.handlePause(),
  stop: () => this.handleStop(),
  // 其他指令...
};

// 批量注册事件并记录,方便销毁时清理
Object.entries(eventHandlers).forEach(([event, handler]) => {
  this.session!.on(event as SupportedEvents, handler);
  this.registeredEvents.push(event as SupportedEvents);
});

后续如果要新增指令(比如“调整音量”),只需在映射表中加一行,无需修改注册逻辑。

资源销毁:避免内存泄漏

destroy方法系统性清理资源,是保证应用轻量运行的关键:

destroy(): void {
  // 移除所有事件监听
  this.registeredEvents.forEach(event => this.session?.off(event));
  // 释放播放器
  this.avplayer?.release();
  // 销毁会话
  this.session?.destroy((err) => {
    if (err) console.error('销毁会话失败:', err);
  });
}

这里的核心是“有注册就有注销”——通过registeredEvents记录所有注册过的事件,确保退出时一个不落全移除。

总结

这套方案的核心思路很简单:用单例会话做“唯一入口”,用控制器统一处理指令和状态,最后记得“用完就清”。无论是音乐APP还是视频播放器,只要涉及多渠道控制或跨设备同步,这套逻辑都能直接复用,既减少了状态混乱的风险,又降低了后期维护成本。