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 创建通知,并将其与 MediaSessionsessionToken 关联,通知栏会自动显示当前播放的歌曲信息和控制按钮(播放/暂停/上一首/下一首)。
  • 状态驱动 UI :当 MediaSessionPlaybackState 发生变化时(例如从播放变为暂停),通知栏和锁屏界面上的按钮状态也会实时更新,无需开发者手动处理。

✨ 版本演进

  • Android 5.0 (API 21) :首次引入 MediaSessionMediaController
  • Support Library :提供了 MediaSessionCompatMediaControllerCompat,使得新特性可以向下兼容。
  • Android 12 (API 31) 及以上 :引入了 MediaSession2MediaController2,提供了更强大的功能,如更灵活的跨设备音频路由和多设备控制。

详细的流程

在车载开发(无论是 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)

神奇之处 :一旦关联,系统会自动根据 MediaSessionPlaybackState 来改变通知栏上的按钮(比如播放变暂停),你不需要手动去更新通知。

在车载开发(无论是 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)

神奇之处 :一旦关联,系统会自动根据 MediaSessionPlaybackState 来改变通知栏上的按钮(比如播放变暂停),你不需要手动去更新通知。


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

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

  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.sessionsupport-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.media2com.google.android.exoplayer

总结

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

  1. 解耦与统一:不再需要写"翻译代码",播放器和会话直接连通。
  2. 现代化:拥抱 Kotlin、协程和 Future,代码更易读。
  3. 合规与保活:原生适配 Android 13/14 的后台服务限制,让车载音乐播放更稳定,不易被杀后台。
相关推荐
张风捷特烈1 小时前
状态管理大乱斗#08 | MobX 源码评析 - 透明魔法
android·前端·flutter
i220818 Faiz Ul1 小时前
个人健康系统|健康管理|基于java+Android+微信小程序的个人健康系统设计与实现(源码+数据库+文档)
android·java·vue.js·spring boot·微信小程序·毕设·个人健康系统
赏金术士10 小时前
Kotlin 习题集 · 高级篇
android·开发语言·kotlin
问心无愧051312 小时前
ctf show web 入门42
android·前端·android studio
没什么本事13 小时前
关于C# panel 添加lable问题 -- 明确X和Y 位置错误
android·java·c#
REDcker16 小时前
Android HWASan 详解:硬件标记原理、Clang 启用与排障实践
android·linux·debug·编译·clang·asan·hwasan
2501_9159090617 小时前
全面解析前端开发中常用的浏览器调试工具及其使用场景
android·ios·小程序·https·uni-app·iphone·webview
angerdream17 小时前
Android手把手编写儿童手机远程监控App之SQLite详解2
android
-SOLO-17 小时前
Python 爬取小红书 文章标题和内容 仅供学习
android·python·学习