一、Media3库简介
1.1 Media3是什么?
官方释义:
Jetpack Media3 is the new home for media libraries that enables Android apps to display rich audio and visual experiences. Media3 offers a simple architecture with powerful customization, reliability, and optimizations based on device capabilities to abstract away the complexity that comes with fragmentation.
个人理解:
Media3是Google推出的Android媒体播放库的最新版本,作为之前Media2库的后续升级版本,集成了ExoPlayer作为核心播放引擎。
Media3的目标是统一之前分散的多个媒体库(如ExoPlayer,Media2等)到单一、现代化的API体系之下。
1.2 Media2与Media3的不同
- Media3提供了对Media2 API的向前兼容性,同时整合了ExoPlayer强大的媒体处理能力。
- 代码迁移成本降低:开发者可以更顺畅地从Media2迁移至Media3,尽可能减少影响现有应用功能的风险。可参考《Media3迁移指南》
- Media3将原有Media2的基础组件与ExoPlayer的高级特性结合起来,使得开发者可以在统一的架构下使用更先进的功能。
二、为什么要用Media3
2.1 目前常用框架类型
目前常用的框架如下所示的媒体框架示意图如下所示:
媒体中心主要对接两端,上端承接各路控制源,下路承接媒体App:
2.1.1 媒体App接入
媒体框架通常有一个中心管理器——MediaCenter,即媒体中心。所有的音、视频多媒体都需要接入MediaCenter,并完成以下任务:
- 启动时需要第一时间注册媒体中心,通知媒体中心自己的包名以及其他基本信息
- 注册成功媒体中心会返回一个token,后续使用token来进行媒体控制
- 所有的播放/暂停/结束/切歌/seek/歌词等播放控制和信息传递都需要通知媒体中心
- 媒体App需要监听媒体中心的控制消息,并完成响应的指令
这样,所有的媒体App可以在媒体中心的调度下有条不紊的运行
2.1.2 控制源接入
媒体框架另一端对接各种控制源,主要包括:
- 控制中心
- 语音输入
- 方控
控制源相对比较固定,而且各个控制源的类型不太一样,所以没有完全统一的接入方式。
整体来讲一个App需要接入媒体中心并能够在控制中心同步媒体状态需要经过以下流程:
暂时无法在路特斯桌面文档外展示此内容
2.2 MediaSession解决方案
一个媒体App要做的事情无非就是播放器状态与UI的控制,如下:
- Player: 播放器负责解码并渲染音视频内容
- UI: 播放的内容需要在UI上显示,并可以通过UI对播放器状态进行控制
而媒体中心的任务就是调度各个App的Player
,同时给用户提供一个统一的显示及控制入口。
MedaSession就是Android官方提供的一个中间管理器
2.2.1 什么是MediaSession
官方释义:
Media sessions provide a universal way of interacting with an audio or video player. In Media3, the default player is the
ExoPlayer
class, which implements thePlayer
interface. Connecting the media session to the player allows an app to advertise media playback externally and to receive playback commands from external sources.
个人理解:
媒体会话,即向系统公开正在播放的媒体信息,并对外开放控制端口。可以用它在多个App之间协调媒体控制的机制,通过创建一个中心化的会话来管理与音视频播放相关的各种操作。
2.2.2 MediaSession用法
在使用MediaSession之前,我们需要了解几个模块:
- MediaSession: 媒体会话,用来展示媒体播放信息,并对外提供控制接口
- MediaSessionService: 将MediaSession及其关联的Player保存在与应用的主
Activity
不同的服务中,以便于后台播放。 - MediaController: 用于向MediaSession发送命令,例如从其他应用或系统本身发送命令。这些命令会被发送到关联
MediaSession
的底层Player
。 - MediaBrowser: 用来浏览媒体应用的内容库,并选择要播放的内容。
使用步骤如下:
1、连接MediaService
MediaBrowser作为客户端,远程连接Player所在的MediaService:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val component = ComponentName(this, MediaService::class.java)
mMediaBrowser = MediaBrowser(this, component, connectionCallback, null);
mMediaBrowser.connect()
}
2、连接状态回调
在connect之后可以拿到回调,并获取服务端在onGetRoot中设置的MediaID。如果连接成功则可以创建媒体控制器后续对媒体进行控制:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val component = ComponentName(this, MediaService::class.java)
mMediaBrowser = MediaBrowser(this, component, connectionCallback, null);
mMediaBrowser.connect()
}
private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
override fun onConnected() {
super.onConnected()
// todo 可创建MediaContrller
}
override fun onConnectionFailed() {
super.onConnectionFailed()
}
override fun onConnectionSuspended() {
super.onConnectionSuspended()
}
}
3、媒体控制结果返回
private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
override fun onConnected() {
super.onConnected()
if(mMediaBrowser.isConnected) {
val mediaId = mMediaBrowser.root
mMediaBrowser.getItem(mediaId, itemCallback)
}
}
}
private val itemCallback = object : MediaBrowser.ItemCallback(){
override fun onItemLoaded(item: MediaBrowser.MediaItem?) {
super.onItemLoaded(item)
}
override fun onError(mediaId: String) {
super.onError(mediaId)
}
}
4、订阅服务
在连接成功后,我们需要订阅服务,同样也需要注册订阅回调。订阅成功会拿到当前的媒体信息(MediaItem
),可以在UI中展示当前的音乐列表数据:
private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
override fun onConnected() {
super.onConnected()
...
if(mMediaBrowser.isConnected) {
val mediaId = mMediaBrowser.root
mMediaBrowser.unsubscribe(mediaId)
mMediaBrowser.subscribe(mediaId, subscribeCallback)
}
}
}
private val subscribeCallback = object : MediaBrowser.SubscriptionCallback(){
override fun onChildrenLoaded(
parentId: String,
children: MutableList<MediaBrowser.MediaItem>
) {
super.onChildrenLoaded(parentId, children)
}
override fun onChildrenLoaded(
parentId: String,
children: MutableList<MediaBrowser.MediaItem>,
options: Bundle
) {
super.onChildrenLoaded(parentId, children, options)
}
override fun onError(parentId: String) {
super.onError(parentId)
}
override fun onError(parentId: String, options: Bundle) {
super.onError(parentId, options)
}
}
5、播放控制
MediaController的创建需要对应的MediaID,所以必须在MediaBrowser连接成功并拿到MediaID之后才可以创建:
private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
override fun onConnected() {
super.onConnected()
// ...
if(mMediaBrowser.isConnected) {
val sessionToken = mMediaBrowser.sessionToken
mMediaController = MediaController(applicationContext,sessionToken)
}
}
}
然后就可以使用mMediaController对媒体进行控制了,比如控制媒体播放的代码如下:
private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
override fun onConnected() {
super.onConnected()
// ...
if(mMediaBrowser.isConnected) {
val sessionToken = mMediaBrowser.sessionToken
mMediaController = MediaController(applicationContext, sessionToken)
}
}
}
private fun play() {
// 控制播放
mMediaController.transportControls.play()
}
private fun pause() {
// 控制暂停
mMediaController.transportControls.pause()
}
我们可以通过MediaContrller
的transportControls
接口完成播放控制
6、接收MediaSession回调
为了保持UI和Player的状态一直,我们除了控制播放器之外,还需要监听由MediaSession发过来的回调事件:
private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
override fun onConnected() {
super.onConnected()
...
if(mMediaBrowser.isConnected) {
val sessionToken = mMediaBrowser.sessionToken
mMediaController = MediaController(applicationContext,sessionToken)
mMediaController.registerCallback(controllerCallback)
}
}
}
private val controllerCallback = object : MediaController.Callback() {
override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) {
super.onAudioInfoChanged(info)
}
override fun onExtrasChanged(extras: Bundle?) {
super.onExtrasChanged(extras)
}
}
7、实现MediaBrowserService
用于承载Player的Service,MediaSession也和Player一样位于Service中。实现MediaBrowserService需要复写亮哥方法:
- onGetRoot: 客户端连接的时候调用,在里面可以决定是否允许客户端连接,返回null表示拒绝,否则同意
- onLoadChildren: 客户端订阅服务时触发,可以选择返回给客户端的服务数据
示例代码如下:
class MediaService : MediaBrowserService() {
override fun onGetRoot(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
): BrowserRoot? {
return BrowserRoot(ID, null)
}
override fun onLoadChildren(
parentId: String,
result: Result<MutableList<MediaBrowser.MediaItem>>
) {
when (parentId) {
ID -> {
// todo 查询媒体或者数据库,找到客户端需要的数据
result.detach()
result.sendResult()
}
else -> {
}
}
}
}
最后,记得在Manifest里注册Service:
<service
android:name=".MediaService"
android:label="@string/media_service">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
三、基于Media2的改进
3.1 前台播放
3.1.1 Media2时代方案
前台播放的时候,比如视频、直播场景。可以把播放器和UI放在同一个Activity中。Media2的架构如下:
由于在同一个Activity,Player和UI可以很方便的相互操作,而媒体信息同步到MediaSession需要有一个注册回调的过程,这中间需要提供一个连接器,即Connector,所有的操作都需要从Connector进行中转,增加了系统复杂性及出错概率。
3.1.2 Media3改进方式
Media3直接将ExoPlayer作为了Player的默认实现,并且实现了标准的播放器接口,从而UI和MediaSession都可以支持改接口。这样就可以免去连接器,使整体框架更稳定
3.2 后台播放
3.2.1 Media2时代方案
后台播放的改进也很明显,与前台播放不一样,后台播放需要把Player放到Service中,而UI保留在Activity中,整个会变成C/S架构:
首先我们将Player和UI进行了分离,那么相互之间的控制就需要通过MediaSession来进行。其中MediaSession在Server端进行统一管理,而UI作为Client创建MediaController连接MediaSession进行通信。
这里同样会有前台播放的问题,即Player无法与MediaSession直接通信,需要单独增加Connector。在Client端,MediaContrller和UI的接口也不同,则也需要一个Connector进行连接。项目整体复杂度进一步增加。
3.2.2 Media3改进方案
以下是Media3的后台播放方案:
如前文所述,Media3使用了一种通用的Player接口来消除连接器,并且默认使用ExoPlayer作为播放器,内部已经实现了Player接口,如此即可直接与MediaSeesion兼容,这样就可以通过Player直接完成MediaSession、MediaController的直接通信,可以去掉Connetor中多余的中转代码。
四、Media3改进的秘密
综上,Media3一个非常明显的好处就是省略了Connector连接器,大幅简化的层级结构,代码量也可以减少很多。这个得益于Media3直接将ExoPlayer纳入麾下,成为了默认的播放器实现。那下面来聊聊为什么ExoPlayer的加入带来这么多好处。
4.1 设计思想的改进
4.1.1 Media2的设计思想
在Media2中,Player和UI的通信依赖MediaBrowserService
和MediaBrowser
配合使用,提供了一种Client和Service之间的沟通机制。当一个App想要播放媒体时,实际上是创建了一个MediaBrowser实例,并通过它来连接到后台的 MediaBrowserService
来实现的。这个过程涉及到绑定服务、处理异步回调等复杂的交互流程,这就是我们说的的"connection"。
这种模型允许媒体控制和播放在应用的不同组件(例如,不同的Activity、Fragment或者后台服务)之间能够保持一致性。此外,它也支持跨应用的媒体控制,比如可以从其他应用或者Android系统级别的媒体控制界面控制播放。
4.1.2 Media3的设计思想
进入Media3时代,Google对这套API进行了重新设计,摒弃了连接这个概念。Media3直接整合了ExoPlayer,提供了一套更为简洁的API。
Media3兼容类似Media2中的MediaBrowser
和远程播放控制功能,但它实现了一种更轻量级的方式来管理这些操作,不再需要显式地管理服务连接。
Media3利用了新的架构,将MediaBrowser
和播放能力内聚在少数几个组件中,比如 MediaSession
和 MediaController
。通过这种设计,Media3能够让媒体播放和控制更加直接和高效,同时也简化了应用架构。
总体来讲,Media2是需要连接器的,因为它采用了C/S架构来处理媒体播放任务,使其能够支持跨应用的媒体共享和控制。而Media3是无需连接的,通过简化API和直接整合ExoPlayer的方式来提升开发效率和用户体验。
4.2 代码上的改进
无需再创建Connection进行连接,ExoPlayer可以直接构建出MediaSession对象,播控由ExoPlayer内部完成MediaController和MediaSession的交互,并实现了统一的Player
接口,用来回调播放器的状态,摆脱了C/S架构,代码更整洁
class ExamplePlaybackService : MediaSessionService() {
private var exoPlayer: ExoPlayer? = null
private var mediaSession: MediaSession? = null
override fun onCreate() {
super.onCreate()
// 创建ExoPlayer
exoPlayer = ExoPlayer.Builder(this).build()
// 基于已创建的ExoPlayer创建MediaSession
exoPlayer?.let { mediaSession = MediaSession.Builder(this, it).build() }
}
override fun onDestroy() {
// 释放相关实例
exoPlayer?.stop()
exoPlayer?.release()
exoPlayer = null
mediaSession?.release()
mediaSession = null
super.onDestroy()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
return mediaSession
}
}
五、车载媒体开发技术展望
Media3技术目前已经比较成熟,各大媒体App都相继支持,且作为官方大力推荐的工具,可以将很多复杂的工作交由Android系统完成,兼容性和稳定性都有一定的保障。
未来车载媒体可能会接入更多第三方媒体,比如爱奇艺、优酷、喜马拉雅、在线音乐等,按照当前架构就需要他们集成MediaCenter.jar
并按照我们定义的接口协议完成开发,除了第三方的工作量之外,我们也需要提供不少的技术支持及Bugfix的排查定位工作。
综上,MediaCenter未来会计划接入MediaSession,目前还在调研阶段,希望未来能够更快的实现媒体接口的统一。