23. 媒体会话与控制:MediaSession框架、远程控制、锁屏控制
媒体播放这件事,说白了就是「谁在播、谁来控、怎么控」这三个问题。Android 从 5.0 开始引入了 MediaSession 框架,目的就是把这套控制逻辑标准化。我当年刚接触这个框架时,觉得它绕来绕去很麻烦,后来在做一个车载音乐 App 时,才真正体会到它的设计精妙——没有它,远程控制、锁屏控制、甚至语音助手这些场景,根本没法统一处理。
23.1 MediaSession 框架的核心角色
MediaSession 框架其实就两个核心角色:Session 和 Controller。
- MediaSession:播放器端。它负责发布「当前在播什么」、「状态如何」、「支持哪些控制命令」。
- MediaController:控制端。它通过 Binder 与 Session 通信,发送播放/暂停/下一曲等指令,并接收状态回调。
你想想看,一个 App 里可能有多个播放器(比如音乐列表和视频播放器),每个播放器对应一个 Session。系统层面通过 MediaSessionManager 来管理所有 Session,而外部组件(比如锁屏界面、蓝牙耳机、车载系统)通过 MediaController 来与某个 Session 交互。
关键点:Session 和 Controller 是跨进程通信的。这意味着锁屏界面、通知栏、甚至另一个 App 都可以控制你的播放器,只要它们持有对应的 Controller。
23.2 创建 MediaSession 的基本流程
我习惯在 Service 里创建 Session,因为播放器通常需要后台运行。下面是一个最简示例:
// 创建 MediaSession,传入包名和上下文
MediaSession session = new MediaSession(context, "MusicPlayerSession");
// 设置回调,处理来自 Controller 的命令
session.setCallback(new MediaSession.Callback() {
@Override
public void onPlay() {
// 开始播放
}
@Override
public void onPause() {
// 暂停播放
}
@Override
public void onSkipToNext() {
// 下一曲
}
@Override
public void onStop() {
// 停止播放
}
});
// 设置为活跃状态,表示当前有可控制的播放
session.setActive(true);
// 发布初始的播放状态和元数据
MediaMetadata metadata = new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_TITLE, "晴天")
.putString(MediaMetadata.METADATA_KEY_ARTIST, "周杰伦")
.putLong(MediaMetadata.METADATA_KEY_DURATION, 300000L)
.build();
PlaybackState state = new PlaybackState.Builder()
.setState(PlaybackState.STATE_PLAYING, 0, 1.0f)
.setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SKIP_TO_NEXT)
.build();
session.setMetadata(metadata);
session.setPlaybackState(state);
嗯,这里要注意:setActive(true) 这一步很容易忘。我曾经排查过一个 Bug,锁屏界面死活不显示控制按钮,最后发现是 Session 没有设为活跃状态。系统只把活跃的 Session 暴露给外部控制器。
23.3 远程控制:从蓝牙耳机到车载系统
远程控制的核心,就是外部设备通过 MediaController 发送命令。Android 系统会自动将蓝牙按键、耳机线控、甚至 Wear OS 手表上的操作,映射为对应的 onPlay()、onPause() 等回调。
举个例子,用户按下蓝牙耳机的「下一曲」按钮,系统会查找当前活跃的 MediaSession,然后调用它的 onSkipToNext()。你的回调里只需要实现具体的跳转逻辑即可。
个人经验:我在做车载 Android 系统时,发现有些车机发送的按键事件不是标准的 KEYCODE_MEDIA_NEXT,而是自定义的广播。这时候需要在 onCommand() 方法里手动处理自定义命令。MediaSession 的 Callback 提供了 onCommand(String command, Bundle args, ResultReceiver cb),可以兜底处理所有非标准指令。
23.4 锁屏控制:MediaStyle 通知与锁屏界面
锁屏控制其实分两层:
- 通知栏控制:通过
MediaStyle通知展示播放控制按钮。 - 系统锁屏界面:Android 系统会读取活跃 Session 的元数据和状态,自动在锁屏上显示控制组件。
通知栏控制是最常见的做法。我一般这样构建通知:
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_music)
.setContentTitle("晴天")
.setContentText("周杰伦")
.setLargeIcon(albumArt)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // 锁屏可见
.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(session.getSessionToken())
.setShowActionsInCompactView(0, 1, 2)) // 紧凑模式下显示三个按钮
.addAction(R.drawable.ic_prev, "上一曲", prevPendingIntent)
.addAction(R.drawable.ic_play, "播放", playPendingIntent)
.addAction(R.drawable.ic_next, "下一曲", nextPendingIntent);
startForeground(NOTIFICATION_ID, builder.build());
这里有个坑:setVisibility(VISIBILITY_PUBLIC) 必须设置,否则锁屏上不会显示通知内容。我曾经在某个项目里忘了这行,结果锁屏只显示一个图标,用户完全无法控制播放。
23.5 MediaSession 的生命周期与状态管理
Session 的状态管理是很多开发者容易搞乱的地方。我画了一张图,帮你理清核心流程:
从图里可以看到,Session 创建后必须调用 setActive(true) 才能被外部发现。之后你可以持续更新播放状态和元数据。当播放结束时,记得调用 setActive(false) 并释放资源。
注意:不要忘记在 onDestroy() 中调用 session.release()。否则会导致 Session 泄漏,系统里残留无效的 Session 对象,影响其他 App 的媒体控制。我曾经在线上环境遇到过,用户切换 App 后,锁屏上还显示着上一个 App 的播放信息,就是因为没有正确释放。
23.6 多 Session 场景下的控制策略
一个系统里可能有多个 App 同时创建了 Session。比如你在听音乐时,突然打开视频 App 看视频。这时候系统怎么决定锁屏上显示哪个控制界面?
答案是:系统会选择一个「当前最合适的 Session」。判断依据包括:
- Session 是否处于活跃状态
- Session 的播放状态(播放中的优先于暂停的)
- Session 的最近使用时间
我个人建议,如果你的 App 有多个播放器(比如一个播客、一个音乐),最好只维护一个全局的 MediaSession,通过切换元数据和状态来区分不同内容。这样可以避免多个 Session 互相干扰。
23.7 避坑指南:我踩过的几个坑
最后分享几个实战中容易翻车的地方:
- Session Token 传递错误:
MediaStyle.setMediaSession()需要传入session.getSessionToken(),而不是 Session 对象本身。我见过有人直接传 Session,编译不报错但运行时通知不响应。 - Callback 线程问题:
setCallback()默认在主线程回调。如果你的播放器在子线程,记得用setCallback(callback, handler)指定 Handler。 - 锁屏控制不显示:检查是否设置了
setVisibility(VISIBILITY_PUBLIC),以及 Session 是否setActive(true)。 - 蓝牙断开后状态残留:监听
AudioManager.ACTION_AUDIO_BECOMING_NOISY广播,在蓝牙断开时自动暂停播放并更新 PlaybackState。
嗯,MediaSession 框架其实不复杂,但细节很多。只要把 Session 的生命周期管好,状态同步做对,远程控制和锁屏控制就能稳定运行。我在多个项目里验证过这套方案,从手机到车机再到电视,核心逻辑几乎不用改。