什么是HarmonyOS开发中的音频会话
HarmonyOS开发中的音频会话:AVSession、媒体会话创建与销毁、会话元数据与锁屏媒体控制
核心要点:音频会话(AVSession)是鸿蒙系统中媒体应用与系统 UI 之间的桥梁。本文从 AVSession 的核心概念入手,深入讲解媒体会话的创建与销毁生命周期、会话元数据的配置、控制命令的接收与响应、锁屏/通知栏媒体控制的实现,以及多应用会话协调的最佳实践。
一、背景与动机
你有没有注意过这样的体验:在手机上用音乐 App 听歌时,锁屏界面会显示当前歌曲的封面、歌名、歌手,还有播放/暂停/上一首/下一首的按钮?甚至蓝牙耳机上的按键也能控制播放?
这些功能不是"白来的"——它们都依赖于**音频会话(AVSession)**机制。
AVSession 的核心价值是:让媒体应用和系统 UI 解耦。你的 App 不需要自己画锁屏控件、不需要自己处理蓝牙按键,只需要创建一个 AVSession 并提供元数据,系统就会自动在锁屏、通知栏、控制中心等位置展示媒体控制界面。
但如果你不创建 AVSession 呢?那你的 App 就是一个"隐形"的媒体播放器——系统不知道你在播放什么,用户无法通过锁屏或蓝牙控制,甚至在切换应用后找不到回去的路。
说白了,不做 AVSession 的媒体 App,就像没有门牌号的房子——没人找得到你。
二、核心原理
2.1 AVSession 架构总览
flowchart TD
subgraph 应用层
APP[媒体应用]
SESSION[AVSession<br/>媒体会话]
end
subgraph 系统服务层
SM[AVSessionManager<br/>会话管理器]
SC[AVSessionController<br/>会话控制器]
end
subgraph 系统 UI 层
LS[锁屏界面]
NB[通知栏]
CC[控制中心]
BT[蓝牙/耳机]
end
APP -->|1. 创建会话| SESSION
SESSION -->|2. 注册到系统| SM
SM -->|3. 分发控制| SC
SC -->|4. 展示媒体信息| LS
SC --> NB
SC --> CC
SC --> BT
BT -->|5. 用户操作| SC
SC -->|6. 转发命令| SESSION
SESSION -->|7. 回调到应用| APP
classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
classDef error fill:#EF5350,stroke:#C62828,color:#fff
classDef info fill:#81C784,stroke:#388E3C,color:#000
classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000
class SESSION primary
class SM,SC info
class LS,NB,CC,BT purple
class APP warning
2.2 核心概念
| 概念 | 说明 |
|---|---|
| AVSession | 媒体会话,应用侧创建,用于向系统声明"我在播放媒体" |
| AVSessionManager | 会话管理器,系统服务,管理所有会话的生命周期 |
| AVSessionController | 会话控制器,系统 UI 侧使用,用于获取会话信息和发送控制命令 |
| 会话元数据 | 媒体信息:标题、作者、封面、时长等 |
| 控制命令 | 播放/暂停/上下曲/快进/快退/Seek 等 |
| 会话激活 | 激活的会话才能被系统 UI 展示和接收控制命令 |
2.3 会话生命周期
stateDiagram-v2
[*] --> Created: createAVSession()
Created --> Activated: session.activate()
Activated --> Deactivated: session.deactivate()
Deactivated --> Activated: session.activate()
Activated --> Destroyed: session.destroy()
Deactivated --> Destroyed: session.destroy()
Created --> Destroyed: session.destroy()
Destroyed --> [*]
note right of Activated: 可被系统UI发现\n可接收控制命令
note right of Deactivated: 不可被发现\n不接收命令
关键规则:
- 只有激活的会话才会出现在锁屏/通知栏
- 应用切到后台时,应该保持会话激活(而不是销毁),这样用户还能通过锁屏控制
- 应用退出时,必须销毁会话,否则会话会变成"僵尸"
- 同一应用可以创建多个会话(如音乐播放 + 视频播放),但同一时间只应激活一个
2.4 会话元数据结构
// AVSessionMetadata 核心字段
interface AVSessionMetadata {
assetId: string; // 媒体资源 ID(唯一标识)
title: string; // 标题(歌名/视频名)
artist: string; // 艺术家/作者
author: string; // 作者(与 artist 可不同)
album: string; // 专辑名
writer: string; // 作词/编剧
composer: string; // 作曲
duration: number; // 时长(毫秒)
mediaImage: image.PixelMap | string; // 封面图
subtitle: string; // 副标题
description: string; // 描述
lyric: string; // 歌词
previousAssetId: string; // 上一首资源 ID
nextAssetId: string; // 下一首资源 ID
}
三、代码实战
3.1 基础:创建 AVSession 并设置元数据
// 文件名:BasicAVSession.ets
// 功能:创建媒体会话,设置元数据,激活会话
import { avSession } from '@kit.AvSessionKit';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct BasicAVSessionPage {
@State sessionActive: boolean = false;
@State currentTitle: string = '未播放';
@State currentArtist: string = '-';
@State isPlaying: boolean = false;
private session: avSession.AVSession | null = null;
private sessionManager: avSession.AVSessionManager | null = null;
// 模拟播放列表
private playlist = [
{ id: 'song_1', title: '晚风', artist: '周杰伦', album: '七里香', duration: 270000 },
{ id: 'song_2', title: '晴天', artist: '周杰伦', album: '叶惠美', duration: 269000 },
{ id: 'song_3', title: '稻香', artist: '周杰伦', album: '魔杰座', duration: 223000 },
];
private currentIndex: number = 0;
aboutToAppear() {
this.createSession();
}
// 创建媒体会话
async createSession() {
try {
// 1. 获取会话管理器
this.sessionManager = avSession.getSessionManager(getContext(this) as common.UIAbilityContext);
// 2. 创建会话——指定会话类型和标签
this.session = await this.sessionManager.createAVSession(
'music_session_001', // 会话 ID(全局唯一)
'我的音乐播放器', // 会话标签(用于调试)
avSession.AVSessionType.AUDIO // 会话类型:音频
);
console.info('[AVSession] 会话创建成功');
// 3. 设置会话元数据
await this.updateMetadata();
// 4. 注册控制命令回调
this.registerControlCommands();
// 5. 激活会话
await this.session.activate();
this.sessionActive = true;
console.info('[AVSession] 会话已激活');
} catch (error) {
console.error(`[AVSession] 创建失败: ${JSON.stringify(error)}`);
}
}
// 更新会话元数据
async updateMetadata() {
if (!this.session) return;
const song = this.playlist[this.currentIndex];
// 构建元数据
const metadata: avSession.AVMetadata = {
assetId: song.id,
title: song.title,
artist: song.artist,
album: song.album,
duration: song.duration,
// 封面图——使用网络图片 URL
mediaImage: 'https://example.com/album_cover.jpg',
// 上下首资源 ID
previousAssetId: this.currentIndex > 0 ? this.playlist[this.currentIndex - 1].id : '',
nextAssetId: this.currentIndex < this.playlist.length - 1 ? this.playlist[this.currentIndex + 1].id : '',
};
// 设置元数据到会话
await this.session.setAVMetadata(metadata);
// 更新 UI
this.currentTitle = song.title;
this.currentArtist = song.artist;
console.info(`[AVSession] 元数据已更新: ${song.title} - ${song.artist}`);
}
// 注册控制命令回调
private registerControlCommands() {
if (!this.session) return;
// ★ 核心:监听系统 UI 发来的控制命令
// 这些命令来自锁屏、通知栏、控制中心、蓝牙耳机等
// 播放命令
this.session.on('play', () => {
console.info('[AVSession] 收到播放命令');
this.isPlaying = true;
// 实际开发中,这里调用播放器的 play() 方法
});
// 暂停命令
this.session.on('pause', () => {
console.info('[AVSession] 收到暂停命令');
this.isPlaying = false;
});
// 停止命令
this.session.on('stop', () => {
console.info('[AVSession] 收到停止命令');
this.isPlaying = false;
});
// 下一首命令
this.session.on('playNext', () => {
console.info('[AVSession] 收到下一首命令');
this.playNext();
});
// 上一首命令
this.session.on('playPrevious', () => {
console.info('[AVSession] 收到上一首命令');
this.playPrevious();
});
// 快进命令
this.session.on('fastForward', () => {
console.info('[AVSession] 收到快进命令');
});
// 快退命令
this.session.on('rewind', () => {
console.info('[AVSession] 收到快退命令');
});
// Seek 命令
this.session.on('seek', (time: number) => {
console.info(`[AVSession] 收到 Seek 命令: ${time}ms`);
});
// 设置播放速度
this.session.on('setSpeed', (speed: number) => {
console.info(`[AVSession] 收到设置速度命令: ${speed}x`);
});
// 设置循环模式
this.session.on('setLoopMode', (mode: avSession.LoopMode) => {
console.info(`[AVSession] 收到设置循环模式命令: ${mode}`);
});
// 收藏/取消收藏
this.session.on('toggleFavorite', (assetId: string) => {
console.info(`[AVSession] 收到收藏切换命令: ${assetId}`);
});
}
// 播放下一首
private async playNext() {
if (this.currentIndex < this.playlist.length - 1) {
this.currentIndex++;
await this.updateMetadata();
}
}
// 播放上一首
private async playPrevious() {
if (this.currentIndex > 0) {
this.currentIndex--;
await this.updateMetadata();
}
}
// 模拟播放/暂停
private async togglePlay() {
this.isPlaying = !this.isPlaying;
// ★ 重要:同步更新会话的播放状态
// 这样锁屏/通知栏的按钮状态才会正确更新
if (this.session) {
await this.session.setAVPlaybackState({
state: this.isPlaying ? avSession.PlaybackState.PLAYBACK_STATE_PLAY :
avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
speed: 1.0,
position: {
elapsedTime: 0, // 已播放时间(ms)
updateTime: Date.now(), // 更新时间戳
},
bufferedTime: 0, // 缓冲时间(ms)
loopMode: avSession.LoopMode.LOOP_MODE_SEQUENCE,
isFavorite: false,
});
}
}
build() {
Column() {
Text('AVSession 媒体会话')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#E0E0E0')
.margin({ bottom: 20 })
// 会话状态指示
Row() {
Circle({ width: 12, height: 12 })
.fill(this.sessionActive ? '#81C784' : '#EF5350')
Text(this.sessionActive ? '会话已激活' : '会话未激活')
.fontSize(14)
.fontColor(this.sessionActive ? '#81C784' : '#EF5350')
.margin({ left: 8 })
}
.margin({ bottom: 20 })
// 当前播放信息
Column() {
// 封面占位
Row() {
Column() {
Text('🎵')
.fontSize(48)
}
.width(120)
.height(120)
.borderRadius(12)
.backgroundColor('#1a1a2e')
.justifyContent(FlexAlign.Center)
Column() {
Text(this.currentTitle)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#E0E0E0')
Text(this.currentArtist)
.fontSize(14)
.fontColor('#888888')
.margin({ top: 8 })
Text(`曲目 ${this.currentIndex + 1} / ${this.playlist.length}`)
.fontSize(12)
.fontColor('#6C63FF')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 15 })
}
}
.width('100%')
.padding(20)
.borderRadius(12)
.backgroundColor('#16213e')
.margin({ bottom: 20 })
// 播放控制
Row() {
Button('上一首')
.onClick(() => this.playPrevious())
.width(80)
.height(44)
.backgroundColor('#16213e')
.fontColor('#E0E0E0')
.fontSize(13)
Button(this.isPlaying ? '暂停' : '播放')
.onClick(() => this.togglePlay())
.width(80)
.height(44)
.backgroundColor('#6C63FF')
.fontSize(13)
Button('下一首')
.onClick(() => this.playNext())
.width(80)
.height(44)
.backgroundColor('#16213e')
.fontColor('#E0E0E0')
.fontSize(13)
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ bottom: 20 })
// 提示信息
Text('锁屏/通知栏/控制中心将显示媒体控制')
.fontSize(12)
.fontColor('#888888')
.margin({ bottom: 8 })
Text('蓝牙耳机按键也可控制播放')
.fontSize(12)
.fontColor('#888888')
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#0d0d1a')
}
aboutToDisappear() {
// ★ 重要:销毁会话,避免"僵尸会话"
this.destroySession();
}
// 销毁会话
async destroySession() {
try {
if (this.session) {
await this.session.deactivate();
await this.session.destroy();
this.session = null;
this.sessionActive = false;
console.info('[AVSession] 会话已销毁');
}
} catch (error) {
console.error(`[AVSession] 销毁失败: ${JSON.stringify(error)}`);
}
}
}
3.2 进阶:播放状态同步与锁屏媒体控制
锁屏媒体控制的关键在于播放状态的实时同步。应用侧的播放状态变化必须及时通知到 AVSession,否则锁屏界面的按钮状态会与实际不一致。
// 文件名:LockScreenControl.ets
// 功能:完整的锁屏媒体控制实现,包含播放状态同步
import { avSession } from '@kit.AvSessionKit';
import { common } from '@kit.AbilityKit';
// 播放器状态
interface PlayerState {
isPlaying: boolean;
currentTime: number; // 当前播放位置(ms)
duration: number; // 总时长(ms)
speed: number; // 播放速度
loopMode: avSession.LoopMode;
isFavorite: boolean;
bufferedTime: number; // 缓冲进度(ms)
}
@Entry
@Component
struct LockScreenControlPage {
@State playerState: PlayerState = {
isPlaying: false,
currentTime: 0,
duration: 270000, // 4:30
speed: 1.0,
loopMode: avSession.LoopMode.LOOP_MODE_SEQUENCE,
isFavorite: false,
bufferedTime: 270000,
};
@State sessionInfo: string = '未创建';
private session: avSession.AVSession | null = null;
private playTimer: number = -1;
aboutToAppear() {
this.initSessionWithFullControl();
}
// 初始化会话——注册完整的控制命令
async initSessionWithFullControl() {
try {
const context = getContext(this) as common.UIAbilityContext;
const sessionManager = avSession.getSessionManager(context);
this.session = await sessionManager.createAVSession(
'full_control_session',
'完整控制播放器',
avSession.AVSessionType.AUDIO
);
// 设置初始元数据
await this.session.setAVMetadata({
assetId: 'track_001',
title: '夜曲',
artist: '周杰伦',
album: '十一月的萧邦',
duration: this.playerState.duration,
mediaImage: 'https://example.com/cover.jpg',
});
// ★ 核心:设置支持的控制命令
// 只有注册了的命令,锁屏界面才会显示对应的按钮
await this.session.setAVPlaybackState({
state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
speed: 1.0,
position: { elapsedTime: 0, updateTime: Date.now() },
bufferedTime: this.playerState.bufferedTime,
loopMode: this.playerState.loopMode,
isFavorite: false,
});
// 注册所有控制命令
this.registerAllCommands();
// 激活会话
await this.session.activate();
this.sessionInfo = '会话已激活 ✓';
} catch (error) {
console.error(`[Session] 初始化失败: ${JSON.stringify(error)}`);
this.sessionInfo = `初始化失败: ${error.message}`;
}
}
// 注册所有控制命令
private registerAllCommands() {
if (!this.session) return;
// 播放
this.session.on('play', () => {
console.info('[Control] play');
this.playerState.isPlaying = true;
this.syncPlaybackState();
this.startProgressTimer();
});
// 暂停
this.session.on('pause', () => {
console.info('[Control] pause');
this.playerState.isPlaying = false;
this.syncPlaybackState();
this.stopProgressTimer();
});
// 停止
this.session.on('stop', () => {
console.info('[Control] stop');
this.playerState.isPlaying = false;
this.playerState.currentTime = 0;
this.syncPlaybackState();
this.stopProgressTimer();
});
// 下一首
this.session.on('playNext', () => {
console.info('[Control] playNext');
this.playerState.currentTime = 0;
this.syncPlaybackState();
});
// 上一首
this.session.on('playPrevious', () => {
console.info('[Control] playPrevious');
this.playerState.currentTime = 0;
this.syncPlaybackState();
});
// 快进
this.session.on('fastForward', () => {
console.info('[Control] fastForward');
this.playerState.currentTime = Math.min(
this.playerState.duration,
this.playerState.currentTime + 10000
);
this.syncPlaybackState();
});
// 快退
this.session.on('rewind', () => {
console.info('[Control] rewind');
this.playerState.currentTime = Math.max(
0,
this.playerState.currentTime - 10000
);
this.syncPlaybackState();
});
// Seek 到指定位置
this.session.on('seek', (time: number) => {
console.info(`[Control] seek to ${time}ms`);
this.playerState.currentTime = Math.max(0, Math.min(time, this.playerState.duration));
this.syncPlaybackState();
});
// 设置速度
this.session.on('setSpeed', (speed: number) => {
console.info(`[Control] setSpeed ${speed}x`);
this.playerState.speed = speed;
this.syncPlaybackState();
});
// 设置循环模式
this.session.on('setLoopMode', (mode: avSession.LoopMode) => {
console.info(`[Control] setLoopMode ${mode}`);
this.playerState.loopMode = mode;
this.syncPlaybackState();
});
// 收藏切换
this.session.on('toggleFavorite', (assetId: string) => {
console.info(`[Control] toggleFavorite ${assetId}`);
this.playerState.isFavorite = !this.playerState.isFavorite;
this.syncPlaybackState();
});
}
// ★★★ 核心:同步播放状态到 AVSession ★★★
// 每次播放状态变化时都必须调用此方法
private async syncPlaybackState() {
if (!this.session) return;
try {
await this.session.setAVPlaybackState({
// 播放状态
state: this.playerState.isPlaying
? avSession.PlaybackState.PLAYBACK_STATE_PLAY
: avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
// 播放速度
speed: this.playerState.speed,
// 当前播放位置
position: {
elapsedTime: this.playerState.currentTime,
updateTime: Date.now(),
},
// 缓冲进度
bufferedTime: this.playerState.bufferedTime,
// 循环模式
loopMode: this.playerState.loopMode,
// 是否收藏
isFavorite: this.playerState.isFavorite,
});
} catch (error) {
console.error(`[Session] 状态同步失败: ${JSON.stringify(error)}`);
}
}
// 启动进度计时器
private startProgressTimer() {
this.stopProgressTimer();
this.playTimer = setInterval(() => {
if (this.playerState.isPlaying) {
this.playerState.currentTime += 1000 * this.playerState.speed;
if (this.playerState.currentTime >= this.playerState.duration) {
this.playerState.currentTime = this.playerState.duration;
this.playerState.isPlaying = false;
this.stopProgressTimer();
}
// 定期同步状态(不需要每秒都同步,每 5 秒一次即可)
if (Math.floor(this.playerState.currentTime / 1000) % 5 === 0) {
this.syncPlaybackState();
}
}
}, 1000);
}
// 停止进度计时器
private stopProgressTimer() {
if (this.playTimer !== -1) {
clearInterval(this.playTimer);
this.playTimer = -1;
}
}
build() {
Column() {
Text('锁屏媒体控制')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#E0E0E0')
.margin({ bottom: 8 })
Text(this.sessionInfo)
.fontSize(13)
.fontColor('#6C63FF')
.margin({ bottom: 20 })
// 播放进度条
Column() {
Row() {
Text(this.formatTime(this.playerState.currentTime))
.fontSize(12)
.fontColor('#888888')
.width(50)
Progress({
value: this.playerState.currentTime,
total: this.playerState.duration,
type: ProgressType.Linear
})
.layoutWeight(1)
.color('#6C63FF')
Text(this.formatTime(this.playerState.duration))
.fontSize(12)
.fontColor('#888888')
.width(50)
.textAlign(TextAlign.End)
}
}
.width('100%')
.padding(15)
.borderRadius(12)
.backgroundColor('#16213e')
.margin({ bottom: 15 })
// 播放状态信息
Column() {
Grid() {
GridItem() {
this.StateItem('播放状态', this.playerState.isPlaying ? '播放中' : '已暂停',
this.playerState.isPlaying ? '#81C784' : '#FFB74D')
}
GridItem() {
this.StateItem('播放速度', `${this.playerState.speed}x`, '#4FC3F7')
}
GridItem() {
this.StateItem('循环模式', this.getLoopModeText(this.playerState.loopMode), '#CE93D8')
}
GridItem() {
this.StateItem('收藏', this.playerState.isFavorite ? '❤️' : '🤍', '#EF5350')
}
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
.width('100%')
.height(140)
}
.width('100%')
.padding(15)
.borderRadius(12)
.backgroundColor('#16213e')
.margin({ bottom: 15 })
// 控制按钮
Row() {
Button('⏮').onClick(() => {
this.playerState.currentTime = 0;
this.syncPlaybackState();
}).width(50).height(50).backgroundColor('#16213e').fontSize(20)
Button(this.playerState.isPlaying ? '⏸' : '▶️')
.onClick(() => {
this.playerState.isPlaying = !this.playerState.isPlaying;
this.syncPlaybackState();
if (this.playerState.isPlaying) {
this.startProgressTimer();
} else {
this.stopProgressTimer();
}
})
.width(60).height(60).backgroundColor('#6C63FF').fontSize(24)
Button('⏭').onClick(() => {
this.playerState.currentTime = 0;
this.syncPlaybackState();
}).width(50).height(50).backgroundColor('#16213e').fontSize(20)
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ bottom: 20 })
// 速度控制
Row() {
ForEach([0.5, 0.75, 1.0, 1.25, 1.5, 2.0], (speed: number) => {
Button(`${speed}x`)
.onClick(() => {
this.playerState.speed = speed;
this.syncPlaybackState();
})
.fontSize(11)
.height(32)
.backgroundColor(this.playerState.speed === speed ? '#6C63FF' : '#16213e')
.fontColor(this.playerState.speed === speed ? '#FFFFFF' : '#888888')
.margin({ right: 4 })
})
}
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#0d0d1a')
}
@Builder
StateItem(label: string, value: string, color: string) {
Column() {
Text(value).fontSize(16).fontWeight(FontWeight.Bold).fontColor(color)
Text(label).fontSize(11).fontColor('#888888').margin({ top: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.borderRadius(8)
.backgroundColor('#1a1a2e')
}
private formatTime(ms: number): string {
const seconds = Math.floor(ms / 1000);
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
private getLoopModeText(mode: avSession.LoopMode): string {
switch (mode) {
case avSession.LoopMode.LOOP_MODE_SINGLE: return '单曲循环';
case avSession.LoopMode.LOOP_MODE_SEQUENCE: return '列表循环';
case avSession.LoopMode.LOOP_MODE_SHUFFLE: return '随机播放';
default: return '不循环';
}
}
aboutToDisappear() {
this.stopProgressTimer();
this.session?.deactivate();
this.session?.destroy();
}
}
3.3 高级:多应用会话协调与系统级控制
在多个媒体应用同时运行时,系统需要协调各个会话的优先级。下面的示例展示了如何监听系统会话变化并实现跨应用控制。
// 文件名:SessionCoordinator.ets
// 功能:多应用会话协调器——监听系统所有会话变化
import { avSession } from '@kit.AvSessionKit';
import { common } from '@kit.AbilityKit';
// 会话信息摘要
interface SessionSummary {
sessionId: string;
tag: string;
type: avSession.AVSessionType;
isActive: boolean;
title: string;
artist: string;
isTopSession: boolean;
}
@Entry
@Component
struct SessionCoordinatorPage {
@State sessions: SessionSummary[] = [];
@State topSessionId: string = '';
@State isMonitoring: boolean = false;
private sessionManager: avSession.AVSessionManager | null = null;
aboutToAppear() {
this.initManager();
}
// 初始化会话管理器
async initManager() {
try {
const context = getContext(this) as common.UIAbilityContext;
this.sessionManager = avSession.getSessionManager(context);
console.info('[Coordinator] 会话管理器初始化完成');
} catch (error) {
console.error(`[Coordinator] 初始化失败: ${JSON.stringify(error)}`);
}
}
// 开始监听系统会话变化
async startMonitoring() {
if (!this.sessionManager) return;
this.isMonitoring = true;
try {
// 监听会话创建
this.sessionManager.on('sessionCreate', (session: avSession.AVSessionDescriptor) => {
console.info(`[Coordinator] 新会话创建: ${session.sessionTag}`);
this.refreshSessionList();
});
// 监听会话销毁
this.sessionManager.on('sessionDestroy', (session: avSession.AVSessionDescriptor) => {
console.info(`[Coordinator] 会话销毁: ${session.sessionTag}`);
this.refreshSessionList();
});
// 监听顶级会话变化
this.sessionManager.on('topSessionChange', (session: avSession.AVSessionDescriptor) => {
console.info(`[Coordinator] 顶级会话变化: ${session.sessionTag}`);
this.topSessionId = session.sessionId;
this.refreshSessionList();
});
// 初始刷新
await this.refreshSessionList();
} catch (error) {
console.error(`[Coordinator] 监听启动失败: ${JSON.stringify(error)}`);
}
}
// 停止监听
stopMonitoring() {
this.isMonitoring = false;
// 移除监听
this.sessionManager?.off('sessionCreate');
this.sessionManager?.off('sessionDestroy');
this.sessionManager?.off('topSessionChange');
}
// 刷新会话列表
private async refreshSessionList() {
if (!this.sessionManager) return;
try {
// 获取所有已激活的会话
const allSessions = await this.sessionManager.getActivatedSessionDescriptors();
this.sessions = allSessions.map(desc => ({
sessionId: desc.sessionId,
tag: desc.sessionTag ?? '未知',
type: desc.sessionType,
isActive: true,
title: '媒体内容',
artist: '未知',
isTopSession: desc.sessionId === this.topSessionId,
}));
console.info(`[Coordinator] 当前活跃会话: ${this.sessions.length} 个`);
} catch (error) {
console.error(`[Coordinator] 刷新失败: ${JSON.stringify(error)}`);
}
}
// 向指定会话发送控制命令
async sendControlCommand(sessionId: string, command: string) {
if (!this.sessionManager) return;
try {
// 创建会话控制器
const controller = await this.sessionManager.createController(sessionId);
// 根据命令类型发送控制
switch (command) {
case 'play':
await controller.sendControlCommand({ command: 'play' });
break;
case 'pause':
await controller.sendControlCommand({ command: 'pause' });
break;
case 'playNext':
await controller.sendControlCommand({ command: 'playNext' });
break;
case 'playPrevious':
await controller.sendControlCommand({ command: 'playPrevious' });
break;
case 'stop':
await controller.sendControlCommand({ command: 'stop' });
break;
}
console.info(`[Coordinator] 已发送命令: ${command} → ${sessionId}`);
// 释放控制器
controller.destroy();
} catch (error) {
console.error(`[Coordinator] 发送命令失败: ${JSON.stringify(error)}`);
}
}
build() {
Scroll() {
Column() {
Text('多应用会话协调器')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#E0E0E0')
.margin({ bottom: 20 })
// 监控状态
Row() {
Circle({ width: 12, height: 12 })
.fill(this.isMonitoring ? '#81C784' : '#EF5350')
Text(this.isMonitoring ? '监控中' : '未启动')
.fontSize(14)
.fontColor(this.isMonitoring ? '#81C784' : '#EF5350')
.margin({ left: 8 })
Text(`活跃会话: ${this.sessions.length}`)
.fontSize(14)
.fontColor('#4FC3F7')
.margin({ left: 20 })
}
.margin({ bottom: 20 })
// 会话列表
if (this.sessions.length === 0) {
Column() {
Text('暂无活跃会话')
.fontSize(16)
.fontColor('#888888')
Text('请打开其他媒体应用后刷新')
.fontSize(12)
.fontColor('#666666')
.margin({ top: 8 })
}
.width('100%')
.height(150)
.justifyContent(FlexAlign.Center)
.borderRadius(12)
.backgroundColor('#16213e')
} else {
ForEach(this.sessions, (session: SessionSummary) => {
Column() {
Row() {
// 顶级会话标记
if (session.isTopSession) {
Text('👑')
.fontSize(16)
}
Column() {
Text(session.tag)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#E0E0E0')
Text(`类型: ${session.type === avSession.AVSessionType.AUDIO ? '音频' : '视频'}`)
.fontSize(12)
.fontColor('#888888')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 控制按钮
Row() {
Button('⏮')
.onClick(() => this.sendControlCommand(session.sessionId, 'playPrevious'))
.width(36).height(36).backgroundColor('#1a1a2e').fontSize(14)
Button('⏸')
.onClick(() => this.sendControlCommand(session.sessionId, 'pause'))
.width(36).height(36).backgroundColor('#1a1a2e').fontSize(14)
Button('▶️')
.onClick(() => this.sendControlCommand(session.sessionId, 'play'))
.width(36).height(36).backgroundColor('#1a1a2e').fontSize(14)
Button('⏭')
.onClick(() => this.sendControlCommand(session.sessionId, 'playNext'))
.width(36).height(36).backgroundColor('#1a1a2e').fontSize(14)
}
}
}
.width('100%')
.padding(15)
.borderRadius(12)
.backgroundColor(session.isTopSession ? '#1a2744' : '#16213e')
.border({
width: session.isTopSession ? 1 : 0,
color: '#6C63FF',
})
.margin({ bottom: 8 })
})
}
// 控制按钮
Row() {
Button(this.isMonitoring ? '停止监控' : '开始监控')
.onClick(() => {
if (this.isMonitoring) {
this.stopMonitoring();
} else {
this.startMonitoring();
}
})
.width(150)
.height(50)
.backgroundColor(this.isMonitoring ? '#EF5350' : '#6C63FF')
.borderRadius(12)
Button('刷新列表')
.onClick(() => this.refreshSessionList())
.width(100)
.height(50)
.backgroundColor('#16213e')
.fontColor('#4FC3F7')
.borderRadius(12)
.margin({ left: 10 })
}
.margin({ top: 20 })
}
.width('100%')
.padding(20)
}
.width('100%')
.height('100%')
.backgroundColor('#0d0d1a')
}
aboutToDisappear() {
this.stopMonitoring();
}
}
四、踩坑与注意事项
4.1 常见陷阱
| 陷阱 | 现象 | 解决方案 |
|---|---|---|
| 忘记激活会话 | 锁屏/通知栏不显示媒体控制 | 创建后必须调用 session.activate() |
| 不同步播放状态 | 锁屏按钮状态与应用不一致 | 每次状态变化都调用 setAVPlaybackState() |
| 不销毁会话 | 应用退出后锁屏仍显示"幽灵"控制 | 在 aboutToDisappear 中调用 destroy() |
| 元数据封面过大 | 封面图加载慢或 OOM | 使用适当分辨率的图片,建议不超过 512×512 |
| 控制命令未注册 | 锁屏上某些按钮不显示 | 确保注册了对应的命令回调 |
| 多会话冲突 | 同一应用多个会话互相覆盖 | 同一时间只激活一个会话 |
4.2 会话元数据最佳实践
// 元数据设置的最佳实践
async function setBestPracticeMetadata(session: avSession.AVSession) {
await session.setAVMetadata({
// ★ assetId 必须全局唯一,建议使用 "应用包名_媒体ID" 格式
assetId: 'com.example.music_track_001',
// 标题和艺术家——这是锁屏上最显眼的信息,务必准确
title: '歌曲名称',
artist: '歌手名',
// 专辑——有助于用户识别
album: '专辑名',
// 时长——单位是毫秒,不是秒!
duration: 270000, // 4分30秒 = 270000ms
// 封面图——推荐使用 PixelMap 而非 URL
// URL 需要网络加载,PixelMap 是本地数据,加载更快
// mediaImage: pixelMap,
// 上下首 ID——帮助系统 UI 显示"上一首/下一首"按钮状态
previousAssetId: 'com.example.music_track_000',
nextAssetId: 'com.example.music_track_002',
});
}
4.3 会话与音频焦点的关系
AVSession 和音频焦点是两个独立但相关的机制:
- AVSession 解决的是"系统 UI 如何展示和控制媒体"
- 音频焦点 解决的是"多个音频流谁优先播放"
最佳实践是两者配合使用:
// 会话激活 + 音频焦点请求
async function startPlaybackWithFocus(
session: avSession.AVSession,
renderer: audio.AudioRenderer
) {
// 1. 激活会话(让系统 UI 知道你在播放)
await session.activate();
// 2. 请求音频焦点(让系统音频策略知道你要播放)
// AudioRenderer 创建时自动请求焦点
// 3. 开始播放
await renderer.start();
// 4. 同步播放状态
await session.setAVPlaybackState({
state: avSession.PlaybackState.PLAYBACK_STATE_PLAY,
speed: 1.0,
position: { elapsedTime: 0, updateTime: Date.now() },
bufferedTime: 0,
loopMode: avSession.LoopMode.LOOP_MODE_SEQUENCE,
isFavorite: false,
});
}
五、HarmonyOS 6 适配
5.1 版本差异
| 特性 | HarmonyOS 5 (API 12) | HarmonyOS 6 (API 14) |
|---|---|---|
| 锁屏控制 | 基础播放控制 | 增强:支持歌词滚动显示、进度条拖动 |
| 会话类型 | AUDIO / VIDEO | 新增:VOICE_CALL 通话类型 |
| 控制命令 | 基础命令 | 新增:setRating 评分命令、customCommand 自定义命令 |
| 多设备 | 单设备 | 新增:AVCastSession 投播会话,支持跨设备控制 |
| 会话分组 | 无 | 新增:AVSessionGroup 会话分组,多会话统一管理 |
5.2 迁移指南
// HarmonyOS 5 写法——基础控制命令
this.session.on('play', () => { /* 播放 */ });
this.session.on('pause', () => { /* 暂停 */ });
// HarmonyOS 6 写法——新增自定义命令和评分
// this.session.on('customCommand', (command: string, args: Record<string, Object>) => {
// console.info(`[Session] 自定义命令: ${command}, 参数: ${JSON.stringify(args)}`);
// switch (command) {
// case 'add_to_playlist':
// // 添加到播放列表
// break;
// case 'share':
// // 分享当前曲目
// break;
// }
// });
//
// this.session.on('setRating', (rating: number) => {
// console.info(`[Session] 评分: ${rating}`);
// // 保存用户评分
// });
//
// // 锁屏歌词显示
// await this.session.setAVMetadata({
// ...metadata,
// lyric: '[00:00.00]晚风 - 周杰伦\n[00:05.00]晚风轻轻吹过...', // LRC 格式歌词
// });
5.3 注意事项
- HarmonyOS 6 的锁屏歌词需要 LRC 格式,不支持逐字歌词
customCommand的命令字符串建议使用"应用包名.命令名"格式避免冲突AVCastSession投播会话需要设备在同一局域网内
六、总结
mindmap
root((音频会话 AVSession))
核心概念
AVSession 媒体会话
AVSessionManager 会话管理
AVSessionController 会话控制
会话元数据
生命周期
创建 createAVSession
激活 activate
停用 deactivate
销毁 destroy
控制命令
播放/暂停/停止
上下曲切换
快进/快退/Seek
速度/循环模式
收藏切换
锁屏媒体控制
元数据同步
播放状态同步
封面图设置
进度条更新
多应用协调
会话列表监听
顶级会话管理
跨应用控制
焦点与会话配合
HarmonyOS 6
锁屏歌词
自定义命令
投播会话
会话分组
classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
classDef error fill:#EF5350,stroke:#C62828,color:#fff
classDef info fill:#81C784,stroke:#388E3C,color:#000
classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000
关键知识点回顾:
- AVSession 是媒体应用与系统 UI 的桥梁——不创建会话,你的 App 就是"隐形的"
- 会话必须激活才能被系统发现和控制——创建后别忘了
activate() - 播放状态必须实时同步——每次状态变化都调用
setAVPlaybackState() - 应用退出时必须销毁会话——避免"僵尸会话"困扰用户
- AVSession 和音频焦点是两个独立机制——两者配合使用才是最佳实践
音频会话让你的媒体 App 从"自说自话"变成"融入系统生态",这是专业媒体应用的标配。
- 点赞
- 收藏
- 关注作者
评论(0)