什么是HarmonyOS APP媒体框架的中枢神经
核心要点:AVSession 是 HarmonyOS 媒体框架的「中枢神经」,它让音频/视频应用与系统媒体控制中心(锁屏、通知栏、蓝牙设备)无缝对接。掌握会话的创建、激活、元数据设置和生命周期管理,是构建专业级媒体应用的第一步。
| 项目 | 说明 |
|---|---|
| 核心模块 | @ohos.multimedia.avsession |
一、背景与动机
想象一下这个场景:你正在用手机听音乐,突然来了个电话,挂断电话后音乐自动恢复播放——这一切丝滑的背后,就是「媒体会话」在默默工作。
再想想,你戴着蓝牙耳机,按了一下耳机上的播放/暂停键,手机里的音乐就停了。你有没有好奇过,耳机怎么知道手机上在播放什么?手机又怎么响应耳机的按键?
答案就是 AVSession——HarmonyOS 的媒体会话框架。
如果没有 AVSession,每个音乐 App 都得自己处理蓝牙按键、自己对接锁屏控件、自己管理音频焦点……这简直是灾难。AVSession 的出现,就是把这些「脏活累活」统一收口,让开发者专注于业务逻辑,让系统来协调一切。
简单来说,AVSession 解决了三个核心问题:
- 媒体控制标准化——统一的播放/暂停/上下曲命令体系
- 系统级媒体展示——锁屏、通知栏、控制面板自动显示当前播放信息
- 跨设备媒体协同——蓝牙、投屏等场景下的无缝联动
二、核心原理
2.1 AVSession 架构总览
AVSession 框架的核心思想是「发布-订阅」模式。媒体应用作为「提供者」创建会话并发布媒体信息,系统媒体控制中心作为「消费者」订阅会话并展示控制界面。
flowchart TB
classDef primary fill:#1890ff,stroke:#096dd9,color:#fff
classDef warning fill:#fa8c16,stroke:#d48806,color:#fff
classDef error fill:#f5222d,stroke:#cf1322,color:#fff
classDef info fill:#13c2c2,stroke:#006d75,color:#fff
classDef purple fill:#722ed1,stroke:#531dab,color:#fff
subgraph 应用层
A[音乐App]:::primary
B[视频App]:::primary
C[播客App]:::primary
end
subgraph 会话层
D[AVSession 1]:::warning
E[AVSession 2]:::warning
F[AVSession N]:::warning
end
subgraph 系统服务层
G[AVSessionService]:::purple
end
subgraph 控制端
H[锁屏控件]:::info
I[通知栏]:::info
J[控制面板]:::info
K[蓝牙设备]:::info
end
A --> D
B --> E
C --> F
D --> G
E --> G
F --> G
G --> H
G --> I
G --> J
G --> K
2.2 会话生命周期
一个 AVSession 从创建到销毁,经历以下关键阶段:
stateDiagram-v2
classDef primary fill:#1890ff,stroke:#096dd9,color:#fff
classDef warning fill:#fa8c16,stroke:#d48806,color:#fff
classDef error fill:#f5222d,stroke:#cf1322,color:#fff
classDef info fill:#13c2c2,stroke:#006d75,color:#fff
[*] --> 已创建:createSession
已创建 --> 已激活:activate
已激活 --> 已失活:deactivate
已失活 --> 已激活:activate
已激活 --> 已销毁:destroy
已失活 --> 已销毁:destroy
已创建 --> 已销毁:destroy
已销毁 --> [*]
关键规则:
- 只有激活状态的会话才会被系统媒体控制中心发现
- 同一时刻,系统中只有一个顶级会话(Top Session)获得焦点
- 会话销毁后不可恢复,需要重新创建
2.3 会话类型
AVSession 支持三种媒体类型:
| 类型 | 常量 | 说明 |
|---|---|---|
| 音频 | avsession.SessionType.AUDIO |
音乐、播客、有声书等 |
| 视频 | avsession.SessionType.VIDEO |
电影、短视频、直播等 |
| 语音通话 | avsession.SessionType.VOICE_CALL |
电话、VoIP 等 |
三、代码实战
3.1 创建并激活媒体会话
这是最基础的用法——创建一个音频类型的媒体会话,设置元数据,然后激活它。
import { avsession } from '@kit.MultimediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 媒体会话管理器
let session: avsession.AVSession | null = null;
/**
* 创建并激活媒体会话
* tag: 会话标签,用于标识会话来源
* type: 会话类型(AUDIO / VIDEO / VOICE_CALL)
*/
async function createAndActivateSession(): Promise<void> {
try {
// 第一步:创建会话
// tag 是自定义标签,方便调试时识别
session = await avsession.createAVSession(
getContext(this),
'MyMusicPlayer', // 会话标签
avsession.SessionType.AUDIO // 会话类型
);
console.info(`[AVSession] 会话创建成功,sessionId: ${session.sessionId}`);
// 第二步:设置会话元数据(让系统知道你在播放什么)
const metadata: avsession.AVMetadata = {
assetId: 'song_001', // 媒体资源唯一标识
title: '夜曲', // 歌曲标题
artist: '周杰伦', // 艺术家
album: '十一月的萧邦', // 专辑名
duration: 235000, // 时长(毫秒)
mediaType: 'AUDIO', // 媒体类型
// 封面图 URI(支持本地路径和网络路径)
mediaImage: avsession.createAVMediaImage('https://example.com/cover.jpg'),
};
await session.setAVMetadata(metadata);
// 第三步:设置播放状态(让系统知道当前播放进度)
const playbackState: avsession.AVPlaybackState = {
state: avsession.PlaybackState.PLAYBACK_STATE_PLAY, // 正在播放
speed: 1.0, // 播放速度
position: { // 播放位置
elapsedTime: 30000, // 已播放时长(毫秒)
updateTime: Date.now(), // 更新时间戳
},
bufferedTime: 180000, // 缓冲时长(毫秒)
loopMode: avsession.LoopMode.LOOP_MODE_SINGLE, // 单曲循环
isFavorite: false, // 是否收藏
};
await session.setAVPlaybackState(playbackState);
// 第四步:激活会话(只有激活后系统才能发现它)
await session.activate();
console.info('[AVSession] 会话已激活,现在可以在锁屏/通知栏看到播放信息了');
} catch (err) {
const error = err as BusinessError;
console.error(`[AVSession] 创建会话失败: code=${error.code}, msg=${error.message}`);
}
}
3.2 监听媒体控制命令
当用户在锁屏、通知栏或蓝牙设备上操作时,系统会通过 AVSession 下发控制命令。我们需要监听这些命令并做出响应。
import { avsession } from '@kit.MultimediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 注册媒体控制命令监听
* 这是应用响应系统媒体控制的关键入口
*/
function setupCommandListeners(session: avsession.AVSession): void {
// ========== 播放控制命令 ==========
// 播放命令
session.on('play', () => {
console.info('[AVSession] 收到播放命令');
// 调用你的播放逻辑
playerPlay();
});
// 暂停命令
session.on('pause', () => {
console.info('[AVSession] 收到暂停命令');
playerPause();
});
// 停止命令
session.on('stop', () => {
console.info('[AVSession] 收到停止命令');
playerStop();
});
// 下一曲命令
session.on('playNext', () => {
console.info('[AVSession] 收到下一曲命令');
playNextTrack();
});
// 上一曲命令
session.on('playPrevious', () => {
console.info('[AVSession] 收到上一曲命令');
playPreviousTrack();
});
// 快进命令
session.on('fastForward', () => {
console.info('[AVSession] 收到快进命令');
seekToPosition(getCurrentPosition() + 10000); // 快进10秒
});
// 快退命令
session.on('rewind', () => {
console.info('[AVSession] 收到快退命令');
seekToPosition(Math.max(0, getCurrentPosition() - 10000)); // 快退10秒
});
// ========== 进度跳转命令 ==========
session.on('seek', (time: number) => {
console.info(`[AVSession] 收到跳转命令,目标位置: ${time}ms`);
seekToPosition(time);
});
// ========== 速度设置命令 ==========
session.on('setSpeed', (speed: number) => {
console.info(`[AVSession] 收到速度设置命令: ${speed}x`);
setPlaybackSpeed(speed);
});
// ========== 循环模式命令 ==========
session.on('setLoopMode', (mode: avsession.LoopMode) => {
console.info(`[AVSession] 收到循环模式命令: ${mode}`);
setLoopMode(mode);
});
// ========== 收藏命令 ==========
session.on('toggleFavorite', (assetId: string) => {
console.info(`[AVSession] 收到收藏切换命令,资源ID: ${assetId}`);
toggleFavorite(assetId);
});
}
// ========== 模拟播放器方法 ==========
function playerPlay(): void { /* 实际播放逻辑 */ }
function playerPause(): void { /* 实际暂停逻辑 */ }
function playerStop(): void { /* 实际停止逻辑 */ }
function playNextTrack(): void { /* 切换下一曲 */ }
function playPreviousTrack(): void { /* 切换上一曲 */ }
function seekToPosition(time: number): void { /* 跳转到指定位置 */ }
function getCurrentPosition(): number { return 0; /* 获取当前播放位置 */ }
function setPlaybackSpeed(speed: number): void { /* 设置播放速度 */ }
function setLoopMode(mode: avsession.LoopMode): void { /* 设置循环模式 */ }
function toggleFavorite(assetId: string): void { /* 切换收藏状态 */ }
3.3 多会话管理与会话切换
在复杂应用中(比如既有音乐播放又有语音通话),可能需要同时管理多个会话。下面展示如何优雅地管理多会话。
import { avsession } from '@kit.MultimediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 多会话管理器
* 管理应用内的多个媒体会话,确保同一时刻只有一个活跃会话
*/
class MultiSessionManager {
// 会话池:用 Map 存储不同类型的会话
private sessionMap: Map<string, avsession.AVSession> = new Map();
// 当前活跃会话的 key
private activeSessionKey: string = '';
/**
* 创建指定类型的会话
* @param key 会话标识(如 'music', 'voice_call')
* @param type 会话类型
*/
async createSession(key: string, type: avsession.SessionType): Promise<avsession.AVSession | null> {
try {
// 如果已存在同名会话,先销毁旧的
if (this.sessionMap.has(key)) {
await this.destroySession(key);
}
const session = await avsession.createAVSession(
getContext(this),
`Session_${key}`,
type
);
this.sessionMap.set(key, session);
console.info(`[MultiSession] 会话创建成功: ${key}, sessionId: ${session.sessionId}`);
// 注册通用命令监听
this.registerCommonListeners(key, session);
return session;
} catch (err) {
const error = err as BusinessError;
console.error(`[MultiSession] 创建会话失败: ${error.message}`);
return null;
}
}
/**
* 激活指定会话(自动失活其他会话)
* 系统中同一时刻只有一个顶级会话获得焦点
*/
async activateSession(key: string): Promise<boolean> {
const targetSession = this.sessionMap.get(key);
if (!targetSession) {
console.error(`[MultiSession] 会话不存在: ${key}`);
return false;
}
try {
// 先失活当前活跃会话
if (this.activeSessionKey && this.activeSessionKey !== key) {
const currentSession = this.sessionMap.get(this.activeSessionKey);
if (currentSession) {
await currentSession.deactivate();
console.info(`[MultiSession] 已失活会话: ${this.activeSessionKey}`);
}
}
// 激活目标会话
await targetSession.activate();
this.activeSessionKey = key;
console.info(`[MultiSession] 已激活会话: ${key}`);
return true;
} catch (err) {
const error = err as BusinessError;
console.error(`[MultiSession] 激活会话失败: ${error.message}`);
return false;
}
}
/**
* 销毁指定会话
*/
async destroySession(key: string): Promise<void> {
const session = this.sessionMap.get(key);
if (!session) return;
try {
// 先移除所有监听
session.off('play');
session.off('pause');
session.off('stop');
session.off('playNext');
session.off('playPrevious');
// 销毁会话
await session.destroy();
this.sessionMap.delete(key);
// 如果销毁的是当前活跃会话,清空标记
if (this.activeSessionKey === key) {
this.activeSessionKey = '';
}
console.info(`[MultiSession] 会话已销毁: ${key}`);
} catch (err) {
const error = err as BusinessError;
console.error(`[MultiSession] 销毁会话失败: ${error.message}`);
}
}
/**
* 获取指定会话
*/
getSession(key: string): avsession.AVSession | undefined {
return this.sessionMap.get(key);
}
/**
* 销毁所有会话(应用退出时调用)
*/
async destroyAll(): Promise<void> {
const keys = Array.from(this.sessionMap.keys());
for (const key of keys) {
await this.destroySession(key);
}
console.info('[MultiSession] 所有会话已销毁');
}
/**
* 注册通用命令监听
*/
private registerCommonListeners(key: string, session: avsession.AVSession): void {
session.on('play', () => {
console.info(`[MultiSession] ${key} 收到播放命令`);
});
session.on('pause', () => {
console.info(`[MultiSession] ${key} 收到暂停命令`);
});
session.on('stop', () => {
console.info(`[MultiSession] ${key} 收到停止命令`);
});
}
}
// ========== 使用示例 ==========
const manager = new MultiSessionManager();
async function initSessions(): Promise<void> {
// 创建音乐会话
const musicSession = await manager.createSession('music', avsession.SessionType.AUDIO);
// 创建语音通话会话
const callSession = await manager.createSession('voice_call', avsession.SessionType.VOICE_CALL);
// 激活音乐会话
await manager.activateSession('music');
// 来电时切换到语音通话会话
// await manager.activateSession('voice_call');
// 挂断后切回音乐会话
// await manager.activateSession('music');
}
四、踩坑与注意事项
4.1 会话必须激活才能被发现
这是新手最容易踩的坑。创建会话后如果不调用 activate(),系统媒体控制中心完全看不到你的会话,锁屏上也不会显示播放信息。
// ❌ 错误:只创建不激活
const session = await avsession.createAVSession(context, 'tag', type);
await session.setAVMetadata(metadata);
// 忘记 activate(),锁屏上看不到任何信息
// ✅ 正确:创建后立即激活
const session = await avsession.createAVSession(context, 'tag', type);
await session.setAVMetadata(metadata);
await session.activate(); // 关键一步!
4.2 元数据中的封面图处理
mediaImage 字段支持两种类型:string(URI)和 image.PixelMap(像素图)。使用 URI 时要注意:
- 网络图片需要应用有网络权限
- 本地图片需要使用
file://协议前缀 - 图片过大可能导致系统渲染卡顿,建议压缩到 200KB 以内
import { image } from '@kit.ImageKit';
// 方式一:使用 URI(简单,推荐)
const metadataWithUri: avsession.AVMetadata = {
assetId: '001',
title: '测试歌曲',
mediaImage: avsession.createAVMediaImage('file:///data/cover.jpg'),
};
// 方式二:使用 PixelMap(适合动态生成的封面)
async function createMetadataWithPixelMap(pixelMap: image.PixelMap): Promise<avsession.AVMetadata> {
return {
assetId: '001',
title: '测试歌曲',
mediaImage: avsession.createAVMediaImage(pixelMap),
};
}
4.3 播放状态必须实时更新
很多开发者设置了元数据后忘了更新播放状态,导致锁屏上的进度条不动、播放/暂停按钮状态不对。播放状态需要随播放进度持续更新。
// ❌ 错误:只在初始化时设置一次播放状态
await session.setAVPlaybackState(playbackState);
// 之后再也不更新了
// ✅ 正确:播放过程中持续更新
function onPlayerPositionUpdate(currentTime: number): void {
if (!session) return;
session.setAVPlaybackState({
state: avsession.PlaybackState.PLAYBACK_STATE_PLAY,
position: {
elapsedTime: currentTime,
updateTime: Date.now(),
},
});
}
4.4 会话销毁时机
会话是系统级资源,用完必须销毁。否则会占用系统资源,甚至导致其他应用的会话无法正常显示。
// 在 Ability 的 onDestroy 中销毁会话
@Entry
@Component
struct Index {
aboutToAppear(): void {
// 创建会话...
}
aboutToDisappear(): void {
// 页面销毁时清理会话
if (session) {
session.destroy().then(() => {
console.info('[AVSession] 会话已销毁');
session = null;
});
}
}
}
4.5 assetId 的唯一性
assetId 是媒体资源的唯一标识,系统用它来判断是否是同一首歌。如果两首歌的 assetId 相同,系统可能不会更新锁屏信息。
// ❌ 错误:所有歌曲用同一个 assetId
const metadata: avsession.AVMetadata = {
assetId: 'music', // 所有歌曲都用 'music',系统无法区分
title: '歌曲A',
};
// ✅ 正确:每首歌使用唯一 ID
const metadata: avsession.AVMetadata = {
assetId: `track_${songId}`, // 如 'track_001', 'track_002'
title: songTitle,
};
五、HarmonyOS 6 适配
5.1 API 变更
| 变更项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| 会话创建 | avsession.createAVSession() |
新增 avsession.createAVSessionConfig() 支持更多配置项 |
| 元数据 | AVMetadata |
新增 lyric 字段支持歌词展示 |
| 播放状态 | AVPlaybackState |
新增 playbackError 字段支持错误状态上报 |
| 多设备 | 单设备会话 | 新增分布式会话,支持跨设备媒体流转 |
5.2 迁移指南
// HarmonyOS 5 写法
const session = await avsession.createAVSession(context, 'tag', type);
// HarmonyOS 6 写法(新增配置项)
const config: avsession.AVSessionConfig = {
sessionTag: 'MyMusicPlayer',
sessionType: avsession.SessionType.AUDIO,
persistent: true, // 新增:会话是否持久化(应用切后台不销毁)
};
const session = await avsession.createAVSession(context, config);
5.3 权限变更
HarmonyOS 6 中,AVSession 不再需要额外权限声明,但分布式会话场景需要新增:
// module.json5
{
"requestPermissions": [
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"reason": "$string:distributed_media_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
六、总结
| 知识点 | 核心内容 |
|---|---|
| AVSession 本质 | 媒体应用与系统媒体控制中心的桥梁,基于发布-订阅模式 |
| 会话创建 | createAVSession(context, tag, type) 三要素:上下文、标签、类型 |
| 会话激活 | 必须调用 activate() 才能被系统发现,这是最易遗忘的步骤 |
| 元数据设置 | setAVMetadata() 设置标题、艺术家、封面等信息,assetId 必须唯一 |
| 播放状态 | setAVPlaybackState() 需要持续更新,否则锁屏进度条不动 |
| 命令监听 | 通过 on('play') / on('pause') 等监听系统下发的控制命令 |
| 多会话管理 | 同一时刻只有一个顶级会话,切换时先 deactivate 再 activate |
| 会话销毁 | 用完必须 destroy(),在页面/Ability 销毁时清理 |
| HarmonyOS 6 | 新增分布式会话、歌词展示、错误状态上报等能力 |
💡 一句话总结:AVSession 是媒体应用的「身份证」,让系统认识你、展示你、控制你。创建-设元数据-激活-监听命令-实时更新状态-销毁,这六步走完,你的媒体应用就拥有了专业的系统级媒体控制能力。
- 点赞
- 收藏
- 关注作者
评论(0)