昨天下午跟一个技术朋友聊起来这个MediaSession,感觉好像是遗忘了些许,光顾着在项目中使用了,今天有时间把这个梳理一下。
MediaSession 是 Android 5.0 引入的一套核心多媒体框架,旨在解决音视频应用中播放器与用户界面(UI)之间的通信和解耦问题。在车载开发中,这套框架尤为重要,因为它为手机、车机(Android Auto / Android Automotive OS)、耳机、智能手表等不同设备提供了统一的媒体控制接口。
简单来说,MediaSession 框架让播放器(Player)和控制器(Controller)分离,通过标准化的接口进行通信,使得一个播放器可以被多个不同的界面(如应用内UI、锁屏界面、车载系统、通知栏)同时控制,并且状态能够实时同步。
🧩 核心架构与组件
MediaSession 框架属于典型的客户端/服务端(C/S)架构,主要由四个核心组件构成:
服务端 (Server Side)
服务端负责管理播放器实例和媒体状态。
-
MediaBrowserService- 角色:作为媒体应用的服务端核心,负责处理客户端的连接请求和媒体内容浏览。
- 关键方法 :
onGetRoot(): 决定是否允许特定客户端连接。onLoadChildren(): 当客户端订阅媒体内容(如歌曲列表)时,异步返回数据。
- 功能 :它充当了播放器(如 ExoPlayer, MediaPlayer)和
MediaSession的容器。
-
MediaSession- 角色:媒体会话的核心,负责与播放器直接通信,并维护播放状态。
- 功能 :
- 隐藏底层播放器的具体 API,对外只暴露标准的控制接口。
- 维护播放状态(
PlaybackState,如播放、暂停、进度)和正在播放内容的元数据(MediaMetadata,如歌名、艺术家、封面)。 - 通过
SessionToken作为"令牌",允许MediaController连接并控制它。 - 接收来自一个或多个
MediaController的回调指令(如播放、暂停、切歌)。
客户端 (Client Side)
客户端负责展示 UI 并与用户交互。
-
MediaBrowser- 角色 :媒体浏览器,是客户端连接
MediaBrowserService的桥梁。 - 功能 :
- 负责发起与
MediaBrowserService的连接。 - 通过
ConnectionCallback获取连接状态。 - 连接成功后,可以订阅和获取服务端的媒体内容列表。
- 负责发起与
- 角色 :媒体浏览器,是客户端连接
-
MediaController- 角色 :媒体控制器,是客户端与
MediaSession交互的接口。 - 功能 :
- 隔离 UI 代码,UI 只与
MediaController通信,不直接操作播放器。 - 将用户的操作(如点击播放按钮)转换为对
MediaSession的回调。 - 监听
MediaSession的状态变化,并自动更新关联的 UI。 - 必须在
MediaBrowser连接成功后,使用SessionToken创建。
- 隔离 UI 代码,UI 只与
- 角色 :媒体控制器,是客户端与
🚗 在车载开发中的应用
MediaSession 框架是 Android Auto 和 Android Automotive OS 的基石。当用户在车机上使用音乐应用时,其工作流程如下:
- 连接 :车机系统(作为客户端)通过
MediaBrowser连接到手机上的音乐应用(作为服务端)。 - 浏览:连接成功后,车机可以获取音乐应用的歌曲列表等媒体内容。
- 控制 :用户在车机屏幕上点击播放,车机通过
MediaController发送播放指令。 - 执行与同步 :手机上的
MediaSession接收到指令,控制播放器播放音乐,并更新播放状态。这个状态变化会同步回给车机,车机 UI 随之更新(例如,播放按钮变为暂停按钮)。
这套机制确保了无论控制端是手机、手表还是汽车,都能以统一、流畅的方式控制媒体播放。
📢 与系统 UI 的集成
MediaSession 框架还极大地简化了与系统 UI 的集成,最典型的就是通知栏 和锁屏界面。
- 自动状态同步 :通过使用
MediaStyle创建通知,并将其与MediaSession的sessionToken关联,通知栏会自动显示当前播放的歌曲信息和控制按钮(播放/暂停/上一首/下一首)。 - 状态驱动 UI :当
MediaSession的PlaybackState发生变化时(例如从播放变为暂停),通知栏和锁屏界面上的按钮状态也会实时更新,无需开发者手动处理。
✨ 版本演进
- Android 5.0 (API 21) :首次引入
MediaSession和MediaController。 - Support Library :提供了
MediaSessionCompat和MediaControllerCompat,使得新特性可以向下兼容。 - Android 12 (API 31) 及以上 :引入了
MediaSession2和MediaController2,提供了更强大的功能,如更灵活的跨设备音频路由和多设备控制。
详细的流程
在车载开发(无论是 Android Auto 还是 Android Automotive OS)中,MediaSession 是绝对的核心。你可以把它想象成连接你的音乐 App 和汽车大屏之间的"翻译官"和"指挥官"。
核心概念:一个生动的类比
在写代码之前,请先在脑海中建立这个模型:
- 播放器 (Player/ExoPlayer) :是**"音响"**,负责真正发出声音,但它很笨,不知道外面发生了什么。
- MediaSession :是**"音响的控制面板"**。它记录当前在放什么歌(元数据)、是播放还是暂停(状态)。
- MediaBrowserService (服务端) :是**"唱片店老板"**。它持有播放器和管理面板,负责把歌曲列表(媒体库)展示给外面的世界。
- MediaController (客户端) :是**"遥控器"**。车机屏幕、锁屏界面、耳机按钮都是遥控器,它们通过遥控器向"控制面板"发送指令。
第一步:搭建服务端 (Server Side)
这是你作为开发者最需要动手的部分。你需要创建一个后台服务来承载播放器。
1. 继承 MediaBrowserService
你需要创建一个 Service,继承自 MediaBrowserServiceCompat(或者新版的 MediaBrowserService)。这是车机系统连接你 App 的入口。
onGetRoot: 这是"门卫"。当车机尝试连接时,系统会调用这个方法。你需要返回一个根目录 ID,告诉车机"允许连接"。onLoadChildren: 这是"展示货架"。当车机连上后,它会问"你有哪些歌?"。你需要在这个方法里异步返回你的歌曲列表(MediaItem)。
2. 初始化 MediaSession 和 Player
在 Service 的 onCreate 中,你需要把"音响"和"控制面板"组装起来。
Kotlin
1class MusicService : MediaBrowserServiceCompat() {
2
3 private lateinit var player: ExoPlayer // 或者是 MediaPlayer
4 private lateinit var mediaSession: MediaSessionCompat
5
6 override fun onCreate() {
7 super.onCreate()
8
9 // 1. 初始化播放器
10 player = ExoPlayer.Builder(this).build()
11
12 // 2. 初始化 MediaSession
13 mediaSession = MediaSessionCompat(this, "MyCarAudioSession").apply {
14 // 设置回调,监听控制器的指令(播放、暂停等)
15 setCallback(object : MediaSessionCompat.Callback() {
16 override fun onPlay() {
17 player.play() // 真正播放
18 updatePlaybackState() // 更新状态
19 }
20 override fun onPause() {
21 player.pause() // 真正暂停
22 updatePlaybackState()
23 }
24 // 还可以处理 onSkipToNext, onSeekTo 等
25 })
26 }
27
28 // 3. 将 Session 的 Token 交给 Service,让外部能找到它
29 setSessionToken(mediaSession.sessionToken)
30 }
31}
关键点 :MediaSession 本身不播放音乐,它只负责接收指令 并更新状态 。你需要手动把指令转发给 player。
第二步:状态同步 (State Management)
这是最容易踩坑的地方。车机屏幕上显示的"播放/暂停"图标、进度条、歌名,完全取决于你是否正确地更新了 MediaSession 的状态。
你需要维护两个核心对象:
- MediaMetadata (元数据) :告诉车机"现在放的是什么"。
- 包含:标题、艺术家、专辑图、时长。
- PlaybackState (播放状态) :告诉车机"现在是什么状态"。
- 包含:正在播放 (
STATE_PLAYING)、暂停 (STATE_PAUSED)、当前进度位置。
- 包含:正在播放 (
代码示例(在 Service 中):
Kotlin
1 private fun updatePlaybackState() {
2 val state = if (player.isPlaying) {
3 PlaybackStateCompat.STATE_PLAYING
4 } else {
5 PlaybackStateCompat.STATE_PAUSED
6 }
7
8 // 构建状态对象
9 val playbackState = PlaybackStateCompat.Builder()
10 .setState(state, player.currentPosition, 1.0f) // 状态,位置,速度
11 .setActions(PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE) // 声明支持的操作
12 .build()
13
14 mediaSession.setPlaybackState(playbackState)
15}
注意 :每当播放器状态改变(比如播完了、用户点了暂停),你都必须调用 setPlaybackState,否则车机上的按钮状态不会变。
第三步:客户端连接 (Client Side)
虽然车载系统通常会自动充当客户端,但了解这一端有助于你调试。
在你的 App 的 UI 界面(Activity/Fragment)中:
- 使用
MediaBrowserCompat连接你的MusicService。 - 连接成功后 (
onConnected),通过mediaBrowser.sessionToken创建一个MediaControllerCompat。 - 通过
MediaController发送指令(如transportControls.play()),并注册回调 (Callback) 来监听状态变化,从而更新你自己 App 界面上的 UI。
第四步:通知栏与锁屏集成 (Notification)
为了让音乐在后台播放时用户能控制,你需要一个前台通知。MediaSession 让这变得非常简单。
使用 MediaStyle 构建通知,并将它关联到你的 mediaSession.sessionToken。
Kotlin
1 val notification = NotificationCompat.Builder(this, CHANNEL_ID)
2 .setSmallIcon(R.drawable.ic_music)
3 .setContentTitle("七里香")
4 .setContentText("周杰伦")
5 // 核心代码:关联 MediaSession
6 .setStyle(androidx.media.app.NotificationCompat.MediaStyle()
7 .setMediaSession(mediaSession.sessionToken)
8 .setShowActionsInCompactView(0, 1, 2) // 在折叠视图中显示3个按钮
9 )
10 .addAction(R.drawable.ic_prev, "上一首", prevPendingIntent)
11 .addAction(R.drawable.ic_pause, "暂停", pausePendingIntent)
12 .addAction(R.drawable.ic_next, "下一首", nextPendingIntent)
13 .build()
14
15 startForeground(NOTIFICATION_ID, notification)
神奇之处 :一旦关联,系统会自动根据 MediaSession 的 PlaybackState 来改变通知栏上的按钮(比如播放变暂停),你不需要手动去更新通知。
在车载开发(无论是 Android Auto 还是 Android Automotive OS)中,MediaSession 是绝对的核心。你可以把它想象成连接你的音乐 App 和汽车大屏之间的"翻译官"和"指挥官"。
核心概念:一个生动的类比
在写代码之前,请先在脑海中建立这个模型:
- 播放器 (Player/ExoPlayer) :是**"音响"**,负责真正发出声音,但它很笨,不知道外面发生了什么。
- MediaSession :是**"音响的控制面板"**。它记录当前在放什么歌(元数据)、是播放还是暂停(状态)。
- MediaBrowserService (服务端) :是**"唱片店老板"**。它持有播放器和管理面板,负责把歌曲列表(媒体库)展示给外面的世界。
- MediaController (客户端) :是**"遥控器"**。车机屏幕、锁屏界面、耳机按钮都是遥控器,它们通过遥控器向"控制面板"发送指令。
第一步:搭建服务端 (Server Side)
1. 继承 MediaBrowserService
你需要创建一个 Service,继承自 MediaBrowserServiceCompat(或者新版的 MediaBrowserService)。这是车机系统连接你 App 的入口。
onGetRoot: 这是"门卫"。当车机尝试连接时,系统会调用这个方法。你需要返回一个根目录 ID,告诉车机"允许连接"。onLoadChildren: 这是"展示货架"。当车机连上后,它会问"你有哪些歌?"。你需要在这个方法里异步返回你的歌曲列表(MediaItem)。
2. 初始化 MediaSession 和 Player
在 Service 的 onCreate 中,你需要把"音响"和"控制面板"组装起来。
Kotlin
1 class MusicService : MediaBrowserServiceCompat() {
2
3 private lateinit var player: ExoPlayer // 或者是 MediaPlayer
4 private lateinit var mediaSession: MediaSessionCompat
5
6 override fun onCreate() {
7 super.onCreate()
8
9 // 1. 初始化播放器
10 player = ExoPlayer.Builder(this).build()
11
12 // 2. 初始化 MediaSession
13 mediaSession = MediaSessionCompat(this, "MyCarAudioSession").apply {
14 // 设置回调,监听控制器的指令(播放、暂停等)
15 setCallback(object : MediaSessionCompat.Callback() {
16 override fun onPlay() {
17 player.play() // 真正播放
18 updatePlaybackState() // 更新状态
19 }
20 override fun onPause() {
21 player.pause() // 真正暂停
22 updatePlaybackState()
23 }
24 // 还可以处理 onSkipToNext, onSeekTo 等
25 })
26 }
27
28 // 3. 将 Session 的 Token 交给 Service,让外部能找到它
29 setSessionToken(mediaSession.sessionToken)
30 }
31 }
关键点 :MediaSession 本身不播放音乐,它只负责接收指令 并更新状态 。你需要手动把指令转发给 player。
第二步:状态同步 (State Management)
这是最容易踩坑的地方。车机屏幕上显示的"播放/暂停"图标、进度条、歌名,完全取决于你是否正确地更新了 MediaSession 的状态。
需要维护两个核心对象:
- MediaMetadata (元数据) :告诉车机"现在放的是什么"。
- 包含:标题、艺术家、专辑图、时长。
- PlaybackState (播放状态) :告诉车机"现在是什么状态"。
- 包含:正在播放 (
STATE_PLAYING)、暂停 (STATE_PAUSED)、当前进度位置。
- 包含:正在播放 (
Kotlin
1 private fun updatePlaybackState() {
2 val state = if (player.isPlaying) {
3 PlaybackStateCompat.STATE_PLAYING
4 } else {
5 PlaybackStateCompat.STATE_PAUSED
6 }
7
8 // 构建状态对象
9 val playbackState = PlaybackStateCompat.Builder()
10 .setState(state, player.currentPosition, 1.0f) // 状态,位置,速度
11 .setActions(PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE) // 声明支持的操作
12 .build()
13
14 mediaSession.setPlaybackState(playbackState)
15 }
注意 :每当播放器状态改变(比如播完了、用户点了暂停),你都必须调用 setPlaybackState,否则车机上的按钮状态不会变。
第三步:客户端连接 (Client Side)
虽然车载系统通常会自动充当客户端,但了解这一端有助于你调试。
在你的 App 的 UI 界面(Activity/Fragment)中:
- 使用
MediaBrowserCompat连接你的MusicService。 - 连接成功后 (
onConnected),通过mediaBrowser.sessionToken创建一个MediaControllerCompat。 - 通过
MediaController发送指令(如transportControls.play()),并注册回调 (Callback) 来监听状态变化,从而更新你自己 App 界面上的 UI。
第四步:通知栏与锁屏集成 (Notification)
为了让音乐在后台播放时用户能控制,你需要一个前台通知。MediaSession 让这变得非常简单。
使用 MediaStyle 构建通知,并将它关联到你的 mediaSession.sessionToken。
Kotlin
1 val notification = NotificationCompat.Builder(this, CHANNEL_ID)
2 .setSmallIcon(R.drawable.ic_music)
3 .setContentTitle("七里香")
4 .setContentText("周杰伦")
5 // 核心代码:关联 MediaSession
6 .setStyle(androidx.media.app.NotificationCompat.MediaStyle()
7 .setMediaSession(mediaSession.sessionToken)
8 .setShowActionsInCompactView(0, 1, 2) // 在折叠视图中显示3个按钮
9 )
10 .addAction(R.drawable.ic_prev, "上一首", prevPendingIntent)
11 .addAction(R.drawable.ic_pause, "暂停", pausePendingIntent)
12 .addAction(R.drawable.ic_next, "下一首", nextPendingIntent)
13 .build()
14
15 startForeground(NOTIFICATION_ID, notification)
神奇之处 :一旦关联,系统会自动根据 MediaSession 的 PlaybackState 来改变通知栏上的按钮(比如播放变暂停),你不需要手动去更新通知。
🚗 车载开发的特殊注意事项
还需要特别注意以下几点:
-
Android Auto 模板 (Templates):
- 现在的 Android Auto 推荐使用 模板化 UI 。你不需要自己画复杂的布局,而是通过
MediaBrowserService返回层级结构(列表、网格)。 - 你需要在
AndroidManifest.xml中声明支持androidx.car.app.category.MEDIA。
- 现在的 Android Auto 推荐使用 模板化 UI 。你不需要自己画复杂的布局,而是通过
-
语音控制 (Voice):
- 车载场景下,语音是首选。确保你的
MediaMetadata填写完整(特别是标题和艺术家),这样用户说"播放周杰伦的歌"时,Google Assistant 才能准确找到内容。
- 车载场景下,语音是首选。确保你的
-
音频焦点 (Audio Focus):
- 车载系统对音频焦点非常敏感。当导航播报或电话进来时,你的 App 必须通过
MediaSession妥善处理焦点丢失(暂停或压低音量),否则会遭到系统"降权"甚至杀掉。
- 车载系统对音频焦点非常敏感。当导航播报或电话进来时,你的 App 必须通过
-
调试技巧:
- 使用
adb shell dumpsys media_session命令。这是神器!它可以打印出当前系统中所有 MediaSession 的状态,你可以看到 Token、状态、元数据是否正确,是排查"车机上没显示歌名"或"按钮点不动"问题的最佳工具。
- 使用
总结
使用 MediaSession 的核心逻辑就是:
Service 持有 Player 和 Session -> 更新 Session 的状态 -> 系统/车机读取 Session 状态并显示 UI -> 用户点击 UI -> 指令传回 Session -> Session 控制 Player。
MediaSession2相关
MediaSession2(通常指 androidx.media2 库中的组件,以及后来演进为 Media3 的一部分)相比旧版(android.media.session 和 support-v4 中的 MediaSessionCompat),不仅仅是版本的迭代,更是架构层面的重构。
对于车载开发而言,MediaSession2/Media3 带来了显著的优势,主要体现在架构统一、API 现代化、以及对新特性的原生支持上。
以下是详细的对比和优势分析:
1. 架构统一:消灭"胶水代码" (最大的优势)
在旧版架构中,组件之间是割裂的,你需要写大量的代码来充当"翻译官"。
-
旧版痛点:
ExoPlayer(播放器)不懂MediaSession(会话)。- 你需要使用
MediaSessionConnector这样的中间件,把 ExoPlayer 的状态"翻译"给 MediaSession,再把 MediaSession 收到的指令"翻译"回 ExoPlayer。 - UI 层和 Service 层的通信也需要手动维护,容易出现状态不同步(例如:通知栏显示播放,实际已经暂停)。
-
MediaSession2/Media3 优势:
- 统一接口 :定义了统一的
Player接口。新的播放器(如ExoPlayer的升级版)直接实现了这个接口,MediaSession也直接支持这个接口。 - 开箱即用 :不再需要
Connector。你可以直接把 Player 实例传给 MediaSession,它们天生就能互相理解。 - 代码量减少:极大地减少了样板代码,降低了维护成本,状态同步更加稳定。
- 统一接口 :定义了统一的
2. API 现代化:Kotlin 友好与协程支持
旧版 API 设计较早,大量依赖回调(Callback)和监听器,容易导致"回调地狱"。
-
旧版痛点:
- 大量使用
PendingIntent和广播来传递指令。 - 异步操作主要靠接口回调,逻辑分散,难以追踪。
- 大量使用
-
MediaSession2/Media3 优势:
- ListenableFuture :引入了 Guava 的
ListenableFuture(并在 Media3 中进一步适配),使得异步操作(如连接服务、加载媒体)可以链式调用,甚至配合 Kotlin 协程使用,代码逻辑更加线性、清晰。 - 线程安全:Media2 专门增强了线程安全性,减少了因线程切换导致的崩溃或异常。
- ListenableFuture :引入了 Guava 的
3. 功能增强:原生支持现代媒体特性
随着 Android 版本的更新,很多新功能在旧版 API 中是无法直接使用的,或者需要极其复杂的 Hack。
| 功能 | 旧版 (MediaSessionCompat) | MediaSession2 / Media3 |
|---|---|---|
| 断点续录/续播 | 不支持,需手动管理文件流 | 原生支持 pause() / resume(),自动处理时间戳和编码器状态 |
| 广告插入 | 需手动拼接媒体流,极其复杂 | 原生支持广告插入的元数据和状态处理 |
| 会话管理 | 较难管理多个会话 | 提供 SessionToken 的层级结构,便于管理多会话 |
| 权限控制 | 较粗糙 | 更细粒度的连接控制(onConnect 中可精确控制客户端权限) |
4. 更好的车载与后台保活支持
在 Android 12/13/14 之后,系统对后台服务的限制越来越严格。
-
旧版痛点:
- 在 Android 13+ 上,如果未正确声明前台服务类型,后台播放极易被系统杀掉。旧版 API 对
MediaSessionService的支持不够完善,开发者需要手动处理很多生命周期逻辑。
- 在 Android 13+ 上,如果未正确声明前台服务类型,后台播放极易被系统杀掉。旧版 API 对
-
MediaSession2/Media3 优势:
- MediaSessionService :官方推荐并原生支持继承
MediaSessionService。它会自动处理前台服务的启动、通知的关联以及生命周期的保活,完美适配 Android 13/14 的后台限制。 - 车载集成:对于 Android Automotive OS (AAOS),Media3 提供了更标准的集成方式,能够更轻松地处理车机特有的 UX 限制(如行车时禁止视频播放)。
- MediaSessionService :官方推荐并原生支持继承
5. 演进路线:Media3 是终极形态
需要注意的是,Google 目前的重心已经完全转移到了 Media3 (androidx.media3) 上。Media2 可以看作是通往 Media3 的过渡,但 Media3 是最终的集大成者。
- Media3 = 新版 ExoPlayer + 新版 MediaSession + 新版 MediaExtractor/Decoder。
- 建议 :如果你是新项目,或者正在重构旧项目,强烈建议直接使用 Media3 。它继承了 MediaSession2 的所有优点,并且包名统一,不再需要混合使用
androidx.media2和com.google.android.exoplayer。
总结
MediaSession2/Media3 相比旧版的核心价值在于:
- 解耦与统一:不再需要写"翻译代码",播放器和会话直接连通。
- 现代化:拥抱 Kotlin、协程和 Future,代码更易读。
- 合规与保活:原生适配 Android 13/14 的后台服务限制,让车载音乐播放更稳定,不易被杀后台。