MediaSession相关介绍

昨天下午跟一个技术朋友聊起来这个MediaSession,感觉好像是遗忘了些许,光顾着在项目中使用了,今天有时间把这个梳理一下。

MediaSession 是 Android 5.0 引入的一套核心多媒体框架,旨在解决音视频应用中播放器与用户界面(UI)之间的通信和解耦问题。在车载开发中,这套框架尤为重要,因为它为手机、车机(Android Auto / Android Automotive OS)、耳机、智能手表等不同设备提供了统一的媒体控制接口。

简单来说,MediaSession 框架让播放器(Player)和控制器(Controller)分离,通过标准化的接口进行通信,使得一个播放器可以被多个不同的界面(如应用内UI、锁屏界面、车载系统、通知栏)同时控制,并且状态能够实时同步。

🧩 核心架构与组件

MediaSession 框架属于典型的客户端/服务端(C/S)架构,主要由四个核心组件构成:

服务端 (Server Side)

服务端负责管理播放器实例和媒体状态。

  1. MediaBrowserService

    • 角色:作为媒体应用的服务端核心,负责处理客户端的连接请求和媒体内容浏览。
    • 关键方法 :
      • onGetRoot(): 决定是否允许特定客户端连接。
      • onLoadChildren(): 当客户端订阅媒体内容(如歌曲列表)时,异步返回数据。
    • 功能 :它充当了播放器(如 ExoPlayer, MediaPlayer)和 MediaSession 的容器。
  2. MediaSession

    • 角色:媒体会话的核心,负责与播放器直接通信,并维护播放状态。
    • 功能 :
      • 隐藏底层播放器的具体 API,对外只暴露标准的控制接口。
      • 维护播放状态(PlaybackState,如播放、暂停、进度)和正在播放内容的元数据(MediaMetadata,如歌名、艺术家、封面)。
      • 通过 SessionToken 作为"令牌",允许 MediaController 连接并控制它。
      • 接收来自一个或多个 MediaController 的回调指令(如播放、暂停、切歌)。
客户端 (Client Side)

客户端负责展示 UI 并与用户交互。

  1. MediaBrowser

    • 角色 :媒体浏览器,是客户端连接 MediaBrowserService 的桥梁。
    • 功能 :
      • 负责发起与 MediaBrowserService 的连接。
      • 通过 ConnectionCallback 获取连接状态。
      • 连接成功后,可以订阅和获取服务端的媒体内容列表。
  2. MediaController

    • 角色 :媒体控制器,是客户端与 MediaSession 交互的接口。
    • 功能 :
      • 隔离 UI 代码,UI 只与 MediaController 通信,不直接操作播放器。
      • 将用户的操作(如点击播放按钮)转换为对 MediaSession 的回调。
      • 监听 MediaSession 的状态变化,并自动更新关联的 UI。
      • 必须在 MediaBrowser 连接成功后,使用 SessionToken 创建。

🚗 在车载开发中的应用

MediaSession 框架是 Android Auto 和 Android Automotive OS 的基石。当用户在车机上使用音乐应用时,其工作流程如下:

  1. 连接 :车机系统(作为客户端)通过 MediaBrowser 连接到手机上的音乐应用(作为服务端)。
  2. 浏览:连接成功后,车机可以获取音乐应用的歌曲列表等媒体内容。
  3. 控制 :用户在车机屏幕上点击播放,车机通过 MediaController 发送播放指令。
  4. 执行与同步 :手机上的 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 的状态。

你需要维护两个核心对象:

  1. MediaMetadata (元数据) :告诉车机"现在放的是什么"。
    • 包含:标题、艺术家、专辑图、时长。
  2. 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)中:

  1. 使用 MediaBrowserCompat 连接你的 MusicService。
  2. 连接成功后 (onConnected),通过 mediaBrowser.sessionToken 创建一个 MediaControllerCompat。
  3. 通过 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 的状态。

需要维护两个核心对象:

  1. MediaMetadata (元数据) :告诉车机"现在放的是什么"。
    • 包含:标题、艺术家、专辑图、时长。
  2. 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)中:

  1. 使用 MediaBrowserCompat 连接你的 MusicService。
  2. 连接成功后 (onConnected),通过 mediaBrowser.sessionToken 创建一个 MediaControllerCompat。
  3. 通过 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 来改变通知栏上的按钮(比如播放变暂停),你不需要手动去更新通知。


🚗 车载开发的特殊注意事项

还需要特别注意以下几点:

  1. Android Auto 模板 (Templates):

    • 现在的 Android Auto 推荐使用 模板化 UI 。你不需要自己画复杂的布局,而是通过 MediaBrowserService 返回层级结构(列表、网格)。
    • 你需要在 AndroidManifest.xml 中声明支持 androidx.car.app.category.MEDIA。
  2. 语音控制 (Voice):

    • 车载场景下,语音是首选。确保你的 MediaMetadata 填写完整(特别是标题和艺术家),这样用户说"播放周杰伦的歌"时,Google Assistant 才能准确找到内容。
  3. 音频焦点 (Audio Focus):

    • 车载系统对音频焦点非常敏感。当导航播报或电话进来时,你的 App 必须通过 MediaSession 妥善处理焦点丢失(暂停或压低音量),否则会遭到系统"降权"甚至杀掉。
  4. 调试技巧:

    • 使用 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 专门增强了线程安全性,减少了因线程切换导致的崩溃或异常。

3. 功能增强:原生支持现代媒体特性

随着 Android 版本的更新,很多新功能在旧版 API 中是无法直接使用的,或者需要极其复杂的 Hack。

功能 旧版 (MediaSessionCompat) MediaSession2 / Media3
断点续录/续播 不支持,需手动管理文件流 原生支持 pause() / resume(),自动处理时间戳和编码器状态
广告插入 需手动拼接媒体流,极其复杂 原生支持广告插入的元数据和状态处理
会话管理 较难管理多个会话 提供 SessionToken 的层级结构,便于管理多会话
权限控制 较粗糙 更细粒度的连接控制(onConnect 中可精确控制客户端权限)

4. 更好的车载与后台保活支持

在 Android 12/13/14 之后,系统对后台服务的限制越来越严格。

  • 旧版痛点:

    • 在 Android 13+ 上,如果未正确声明前台服务类型,后台播放极易被系统杀掉。旧版 API 对 MediaSessionService 的支持不够完善,开发者需要手动处理很多生命周期逻辑。
  • MediaSession2/Media3 优势:

    • MediaSessionService :官方推荐并原生支持继承 MediaSessionService。它会自动处理前台服务的启动、通知的关联以及生命周期的保活,完美适配 Android 13/14 的后台限制。
    • 车载集成:对于 Android Automotive OS (AAOS),Media3 提供了更标准的集成方式,能够更轻松地处理车机特有的 UX 限制(如行车时禁止视频播放)。

5. 演进路线:Media3 是终极形态

需要注意的是,Google 目前的重心已经完全转移到了 Media3 (androidx.media3) 上。Media2 可以看作是通往 Media3 的过渡,但 Media3 是最终的集大成者。

  • Media3 = 新版 ExoPlayer + 新版 MediaSession + 新版 MediaExtractor/Decoder。
  • 建议 :如果你是新项目,或者正在重构旧项目,强烈建议直接使用 Media3 。它继承了 MediaSession2 的所有优点,并且包名统一,不再需要混合使用 androidx.media2 和 com.google.android.exoplayer。

总结

MediaSession2/Media3 相比旧版的核心价值在于:

  1. 解耦与统一:不再需要写"翻译代码",播放器和会话直接连通。
  2. 现代化:拥抱 Kotlin、协程和 Future,代码更易读。
  3. 合规与保活:原生适配 Android 13/14 的后台服务限制,让车载音乐播放更稳定,不易被杀后台。
相关推荐
三少爷的鞋4 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)7 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码9 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程10 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程12 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓14 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer