AVSession:媒体会话控制与锁屏播放(192)
·
在鸿蒙(HarmonyOS)应用开发中,AVSession Kit(音视频播控服务)是实现专业级音视频体验的核心框架。它不仅能让应用在锁屏状态下继续播放,还能将播放控制权无缝移交给系统播控中心、蓝牙耳机、智能手表等设备。
一、 核心机制与强制约束
- 后台播放的“通行证”
鸿蒙系统对后台音视频播放有严格的约束。如果应用需要后台播放(如音乐、听书、长视频),必须同时接入AVSession和BackgroundTasks Kit(申请AUDIO_PLAYBACK长时任务)。 - 系统级管控
若应用有后台播放需求但未接入AVSession,当应用退到后台或锁屏时,系统会强制暂停音频播放或静音通话声音,以防止恶意后台发声。 - 统一管控与解耦
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);
}
}
}
二、 接入流程与状态同步
- 创建并激活会话
根据业务场景选择正确的会话类型(如'audio'对应音乐,'video'对应视频,'voice_call'对应通话),这决定了播控中心展示的按钮样式。创建后,必须调用activate()激活会话。 - 设置元数据(Metadata)
通过setAVMetadata上报当前媒体的标题、歌手、专辑封面、时长等信息,这些信息会实时展示在锁屏界面和播控中心。 - 注册控制命令与状态同步
应用需注册播放、暂停、上一首、下一首等回调,以响应外部控制。同时,当应用内部状态改变时,必须通过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);
}
}
三、 性能优化
- 会话生命周期管理
AVSession对象在后台播放期间必须保持存活。严禁将其定义在页面组件内部,否则页面销毁会导致 Session 释放,后台播放随即被系统掐断。应将其挂载到全局变量或单例中。 - 严格的初始化顺序
接入流程必须遵循:设置元数据 -> 注册控制命令 -> 激活 Session。如果先激活再注册,可能会导致播控中心界面显示不全或按钮置灰。 - 配置防错
在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 的核心价值之一在于屏蔽底层硬件差异,让应用能够无缝响应各种外设的控制指令。
- 蓝牙耳机与有线耳机适配
应用无需单独监听蓝牙或有线耳机的媒体按键事件。只要正确接入了 AVSession 并注册了相应的控制指令,系统会自动将耳机的按键事件(如播放/暂停、上下一首、佩戴检测等)转换为 AVSession 的控制指令发送给应用。 - 进度条拖拽控制
对于支持进度调整的场景,应用需注册seek控制命令。当用户在锁屏界面或播控中心拖动进度条时,应用会收到目标时间戳的回调。此时建议先将播放状态上报为BUFFERING(缓冲中),待应用内播放器完成 Seek 操作后,再同步更新真实的播放状态与进度。 - 循环模式与收藏状态同步
播控中心支持展示循环模式和收藏状态。应用需在收到setLoopMode等指令后,更新内部播放器状态,并立即通过setAVPlaybackState将最新的loopMode或isFavorite状态上报给系统,确保 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()
}
});
}
}
五、进阶场景:投播与虚拟扩展屏
在鸿蒙生态中,音视频播放不仅限于本地设备,还涉及跨设备的协同体验。
- 投播长时任务检测
当应用进行音视频投播(将本端音视频投至远端设备)时,即使应用退至后台,只要音视频播放或投屏业务其一正常运行,系统就不会终止AUDIO_PLAYBACK长时任务。 - 扩展屏设备监听
通过 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} 个可用扩展屏`);
}
}
}
六、 资源释放与异常处理规范
健壮的生命周期管理是防止内存泄漏和系统异常的关键。
- 会话销毁规范
当应用完全退出或不再有任何音视频播放业务时,必须严格按照顺序释放资源:先调用deactivate()停用会话,再调用destroy()彻底销毁 Session 对象,同时取消已申请的后台长时任务。 - 暂停事件与任务解绑
应用应设置监听音频暂停事件on('pause')。如果收到系统下发的暂停指令且后续不再需要继续播放,推荐主动取消已申请的AUDIO_PLAYBACK长时任务,以节省系统资源。 - 防冻结机制
即使申请了长时任务,如果设备长时间没有有效的音频流输出,应用仍可能被系统冻结。因此,确保 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);
}
}
}
更多推荐




所有评论(0)