AVSession:媒体会话控制与锁屏播放(192)

📅 2026/7/17 11:22:37 👁️ 阅读次数 📝 编程学习
AVSession:媒体会话控制与锁屏播放(192)

在鸿蒙(HarmonyOS)应用开发中,AVSession Kit(音视频播控服务)是实现专业级音视频体验的核心框架。它不仅能让应用在锁屏状态下继续播放,还能将播放控制权无缝移交给系统播控中心、蓝牙耳机、智能手表等设备。

一、 核心机制与强制约束

  1. 后台播放的“通行证”
    鸿蒙系统对后台音视频播放有严格的约束。如果应用需要后台播放(如音乐、听书、长视频),必须同时接入AVSessionBackgroundTasks Kit(申请AUDIO_PLAYBACK长时任务)。
  2. 系统级管控
    若应用有后台播放需求但未接入AVSession,当应用退到后台或锁屏时,系统会强制暂停音频播放或静音通话声音,以防止恶意后台发声。
  3. 统一管控与解耦
    AVSession在应用与控制器(如播控中心、语音助手)之间建立了一个标准化的“会话(Session)”。应用只需向系统注册播放状态和控制命令,无需单独适配各种外设,即可实现一次开发、处处可控。
// BackgroundTaskManager.ets import { backgroundTaskManager } from '@kit.BackgroundTasksKit'; import { wantAgent } from '@kit.AbilityKit'; export class BackgroundTaskHelper { // 1. 在播放开始前申请 AUDIO_PLAYBACK 长时任务 public static async startAudioPlayback(context: Context) { try { let wantAgentObj = await wantAgent.getWantAgent(context, { wants: [{ bundleName: context.abilityInfo.bundleName, abilityName: context.abilityInfo.name }], operationType: wantAgent.OperationType.START_ABILITIES, requestCode: 0, wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG] }); await backgroundTaskManager.startBackgroundRunning( context, 'AudioPlaybackTask', wantAgentObj, backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK ); } catch (err) { console.error('Failed to start background running:', err); } } // 2. 播放结束或暂停时释放长时任务 public static async stopAudioPlayback(context: Context) { try { await backgroundTaskManager.stopBackgroundRunning(context, 'AudioPlaybackTask'); } catch (err) { console.error('Failed to stop background running:', err); } } }

二、 接入流程与状态同步

  1. 创建并激活会话
    根据业务场景选择正确的会话类型(如'audio'对应音乐,'video'对应视频,'voice_call'对应通话),这决定了播控中心展示的按钮样式。创建后,必须调用activate()激活会话。
  2. 设置元数据(Metadata)
    通过setAVMetadata上报当前媒体的标题、歌手、专辑封面、时长等信息,这些信息会实时展示在锁屏界面和播控中心。
  3. 注册控制命令与状态同步
    应用需注册播放、暂停、上一首、下一首等回调,以响应外部控制。同时,当应用内部状态改变时,必须通过setAVPlaybackState及时同步给系统,确保播控中心的进度条和播放状态与 App 完全一致。
// AVSessionManager.ets import { avSession } from '@kit.AVSessionKit'; export class AVSessionManager { private session: avSession.AVSession | null = null; // 严格遵循初始化顺序:创建 -> 设置元数据 -> 注册命令 -> 激活 public async initSession(context: Context) { // 1. 创建并激活会话 this.session = await avSession.createAVSession(context, 'MyMusicSession', 'audio'); // 2. 设置元数据(Metadata) const metadata: avSession.AVMetadata = { title: '鸿蒙星河交响曲', artist: 'Harmony Band', mediaImage: 'https://example.com/cover.jpg', duration: 240000 // 毫秒 }; await this.session.setAVMetadata(metadata); // 3. 注册控制命令回调 this.session.on('play', () => { console.info('Received play command'); // 调用内部 AVPlayer 的 play() 方法 }); this.session.on('pause', () => { console.info('Received pause command'); // 调用内部 AVPlayer 的 pause() 方法 }); this.session.on('playNext', () => { console.info('Received next track command'); }); // 4. 激活会话 this.session.activate(); } // 状态同步:当内部播放器状态改变时调用 public async syncPlaybackState(isPlaying: boolean, elapsedTime: number) { if (!this.session) return; const playbackState: avSession.AVPlaybackState = { state: isPlaying ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE, position: { elapsedTime: elapsedTime, updateTime: new Date().getTime() } }; await this.session.setAVPlaybackState(playbackState); } }

三、 性能优化

  1. 会话生命周期管理
    AVSession对象在后台播放期间必须保持存活。严禁将其定义在页面组件内部,否则页面销毁会导致 Session 释放,后台播放随即被系统掐断。应将其挂载到全局变量或单例中。
  2. 严格的初始化顺序
    接入流程必须遵循:设置元数据 -> 注册控制命令 -> 激活 Session。如果先激活再注册,可能会导致播控中心界面显示不全或按钮置灰。
  3. 配置防错
    module.json5中声明后台任务模式时,backgroundModes必须准确填写为"audio",写成"music""media"会导致后台播放静默失效,且不会报错。
// GlobalMediaService.ets import { Context } from '@kit.AbilityKit'; import { AVSessionManager } from './AVSessionManager'; import { BackgroundTaskHelper } from './BackgroundTaskManager'; // 核心避坑:严禁将 AVSession 定义在页面组件内部,必须挂载到全局单例 export class GlobalMediaService { private static instance: GlobalMediaService; public avSessionManager: AVSessionManager; private context: Context; private constructor(context: Context) { this.context = context; this.avSessionManager = new AVSessionManager(); } public static getInstance(context: Context): GlobalMediaService { if (!this.instance) { this.instance = new GlobalMediaService(context); } return this.instance; } // 会话销毁与资源释放规范 public async destroySession() { // 1. 取消长时任务 await BackgroundTaskHelper.stopAudioPlayback(this.context); // 2. 销毁 AVSession if (this.avSessionManager) { // 内部需调用 session.off() 取消所有监听,然后 session.destroy() await this.avSessionManager.destroy(); } } }

四、 多模设备交互与外设按键适配

AVSession 的核心价值之一在于屏蔽底层硬件差异,让应用能够无缝响应各种外设的控制指令。

  1. 蓝牙耳机与有线耳机适配
    应用无需单独监听蓝牙或有线耳机的媒体按键事件。只要正确接入了 AVSession 并注册了相应的控制指令,系统会自动将耳机的按键事件(如播放/暂停、上下一首、佩戴检测等)转换为 AVSession 的控制指令发送给应用。
  2. 进度条拖拽控制
    对于支持进度调整的场景,应用需注册seek控制命令。当用户在锁屏界面或播控中心拖动进度条时,应用会收到目标时间戳的回调。此时建议先将播放状态上报为BUFFERING(缓冲中),待应用内播放器完成 Seek 操作后,再同步更新真实的播放状态与进度。
  3. 循环模式与收藏状态同步
    播控中心支持展示循环模式和收藏状态。应用需在收到setLoopMode等指令后,更新内部播放器状态,并立即通过setAVPlaybackState将最新的loopModeisFavorite状态上报给系统,确保 UI 表现一致。
// AVSessionControlHandler.ets import { avSession } from '@kit.AVSessionKit'; export class AVSessionControlHandler { private session: avSession.AVSession | null = null; constructor(session: avSession.AVSession) { this.session = session; this.registerCommands(); } private registerCommands() { if (!this.session) return; // 1. 进度条拖拽控制 (Seek) this.session.on('seek', async (position: number) => { console.info(`Received seek command to position: ${position}ms`); // 建议先上报缓冲状态,防止播控中心进度条跳动 await this.session?.setAVPlaybackState({ state: avSession.PlaybackState.PLAYBACK_STATE_BUFFERING }); // TODO: 调用内部播放器执行 seek 操作 // await internalPlayer.seek(position); // 操作完成后,同步真实播放状态 await this.updatePlaybackState(true, position); }); // 2. 循环模式切换 this.session.on('setLoopMode', async (mode: avSession.LoopMode) => { console.info(`Received setLoopMode: ${mode}`); // TODO: 更新内部播放器的循环模式 // internalPlayer.setLoopMode(mode); // 立即同步状态给系统播控中心 await this.session?.setAVPlaybackState({ loopMode: mode }); }); // 3. 收藏状态同步 this.session.on('toggleFavorite', async (isFavorite: boolean) => { console.info(`Received toggleFavorite: ${isFavorite}`); // TODO: 执行数据库收藏/取消收藏操作 // 同步状态 await this.session?.setAVPlaybackState({ isFavorite: isFavorite }); }); } public async updatePlaybackState(isPlaying: boolean, elapsedTime: number) { if (!this.session) return; await this.session.setAVPlaybackState({ state: isPlaying ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE, position: { elapsedTime: elapsedTime, updateTime: new Date().getTime() } }); } }

五、进阶场景:投播与虚拟扩展屏

在鸿蒙生态中,音视频播放不仅限于本地设备,还涉及跨设备的协同体验。

  1. 投播长时任务检测
    当应用进行音视频投播(将本端音视频投至远端设备)时,即使应用退至后台,只要音视频播放或投屏业务其一正常运行,系统就不会终止AUDIO_PLAYBACK长时任务。
  2. 扩展屏设备监听
    通过 AVSession 提供的getAllCastDisplays()接口,应用可以获取当前系统中所有支持扩展屏投播的显示设备。结合castDisplayChange事件监听,应用能在检测到远端设备连接或断开时,动态启动或停止扩展屏上的 UIAbility 绘制,实现双屏联动。
// CastDisplayManager.ets import { avSession } from '@kit.AVSessionKit'; import { common } from '@kit.AbilityKit'; export class CastDisplayManager { private session: avSession.AVSession | null = null; public async initCastDisplay(context: common.UIAbilityContext) { // 1. 创建视频类型的 AVSession this.session = await avSession.createAVSession(context, 'CastDisplay', 'video'); // 2. 监听扩展屏设备连接/断开状态变化 this.session.on('castDisplayChange', (info: avSession.CastDisplayInfo) => { if (info.state === avSession.CastDisplayState.STATE_ON) { console.info('扩展屏已连接,启动大屏 UIAbility'); // 启动扩展屏的 UIAbility,传入 displayId context.startAbility({ bundleName: context.abilityInfo.bundleName, abilityName: 'CastScreenAbility' }, { displayId: info.id }); } else if (info.state === avSession.CastDisplayState.STATE_OFF) { console.info('扩展屏已断开,停止大屏业务'); // TODO: 停止扩展屏业务或回退到本地播放 } }); // 3. 主动获取当前可用的扩展屏设备 const displays = await this.session.getAllCastDisplays(); if (displays.length > 0) { console.info(`发现 ${displays.length} 个可用扩展屏`); } } }

六、 资源释放与异常处理规范

健壮的生命周期管理是防止内存泄漏和系统异常的关键。

  1. 会话销毁规范
    当应用完全退出或不再有任何音视频播放业务时,必须严格按照顺序释放资源:先调用deactivate()停用会话,再调用destroy()彻底销毁 Session 对象,同时取消已申请的后台长时任务。
  2. 暂停事件与任务解绑
    应用应设置监听音频暂停事件on('pause')。如果收到系统下发的暂停指令且后续不再需要继续播放,推荐主动取消已申请的AUDIO_PLAYBACK长时任务,以节省系统资源。
  3. 防冻结机制
    即使申请了长时任务,如果设备长时间没有有效的音频流输出,应用仍可能被系统冻结。因此,确保 AVPlayer 的播放状态与 AVSession 的上报状态保持绝对同步,是维持后台保活的核心前提。
// AVSessionCleanup.ets import { avSession } from '@kit.AVSessionKit'; import { backgroundTaskManager } from '@kit.BackgroundTasksKit'; export class AVSessionCleanup { // 严格的资源释放规范 public static async safeDestroy( session: avSession.AVSession, context: Context, taskId: string ) { try { // 1. 取消所有已注册的控制命令监听器(防止内存泄漏) session.off('play'); session.off('pause'); session.off('seek'); session.off('setLoopMode'); session.off('toggleFavorite'); session.off('castDisplayChange'); // 2. 停用并销毁会话 await session.deactivate(); await session.destroy(); // 3. 释放后台长时任务 await backgroundTaskManager.stopBackgroundRunning(context, taskId); console.info('AVSession 资源安全释放完毕'); } catch (err) { console.error('AVSession 资源释放失败:', err); } } }