android-UniversalMusicPlayer (统一简称UMP) 这个开源项目展示了如何实现一个横跨各种Android平台的完整音乐播放器。其中不但介绍了标准的播放器前后台实现,还包括了如何横跨Android Auto, Android Wear, Android TV等平台的提供一致性用户体验。
本文主要通过对UMP 项目的学习,弄懂弄清播放控制逻辑层 (主要是 MediaSession 框架)的基本使用。
一、UMP基本组成
(1)架构与项目组成
使用 Android Architecture Components (LiveData \ LifeCycle-ViewModel等等)架构,主要分为两个Module (app(主)和 mediaModule)
(2)开发语言
Kotlin (目前Google 最新的Demo 都采用Kotlin 语言 ,如果还没学习Kotlin的小伙伴得抓紧)
(3)底层播放器
ExoPlayer is an application level media player for Android. It provides an alternative to Android’s MediaPlayer API for playing audio and video both locally and over the Internet. ExoPlayer supports features not currently supported by Android’s MediaPlayer API, including DASH and SmoothStreaming adaptive playbacks. Unlike the MediaPlayer API, ExoPlayer is easy to customize and extend, and can be updated through Play Store application updates.
二、MediaSession 框架相关点简介
MediaSession 框架(专门用来解决媒体播放时界面和Service通讯的问题,实现了数据管理、播放控制、UI更新等功能)主要包含以下几个方面 ,MediaBrowser 、MediaBrowserService、MediaSession、MediaController ,分别如下:
(1)MediaBrowser
媒体浏览器,用来连接MediaBrowserService和订阅数据,通过它的回调接口我们可以获取和Service的连接状态以及获取在Service中异步获取的音乐库数据。媒体浏览器一般创建于客户端(可以理解为各个终端负责控制音乐播放的界面)中MediaBrowser 从连接服务到向其订阅数据大致流程 :connect -> onConnected -> subscribe -> onChildrenLoaded 。
(2)MediaBrowserService
浏览器服务,提供onGetRoot(控制客户端媒体浏览器的连接请求,通过返回值决定是否允许该客户端连接服务)和onLoadChildren(媒体浏览器向Service发送数据订阅时调用,一般在这执行异步获取数据的操作, 最后将数据发送至媒体浏览器的回调接口中)这两个抽象方法同时MediaBrowserService还作为承载媒体播放器(如MediaPlayer、ExoPlayer等)和MediaSession的容器。
MediaBrowserService 总结起来就两个作用:1)播放后台服务 ;2)客户端中获取音乐数据的服务,所有的音乐数据都通过该服务与服务端进行交互获取(或者直接获取手机中的本地音乐数据)。
(3)MediaSession
媒体会话,即受控端,通过设置MediaSessionCompat.Callback回调来接收媒体控制器MediaController发送的指令,当收到指令时会触发Callback中各个指令对应的回调方法(回调方法中会执行播放器相应的操作,如播放、暂停等)。
Session一般在Service.onCreate方法中创建,最后需调用setSessionToken方法设置用于和控制器配对的令牌并通知浏览器连接服务成功。
(4)MediaController
媒体控制器,在客户端中开发者不仅可以使用控制器向Service中的受控端发送指令,还可以通过设置MediaControllerCompat.Callback回调方法接收受控端的状态,从而根据相应的状态刷新界面UI。MediaController的创建需要受控端的配对令牌,因此需在浏览器成功连接服务的回调执行创建的操作。
三、UMP 项目实战剖析
在第二部分简单介绍了一些MediaSession 框架四大元素 :MediaBrowser 、MediaBrowserService、MediaSession 及MediaController ,现在结合UMP项目进行展开细讲。
(1)MediaBrowser
connect -> onConnected -> subscribe -> onChildrenLoaded 过程分析 :
1) MediaBrowser::connect
在MainActivity::onCreate()中绑定MusicService(MediaBrowserServiceCompat实例)并创建MediaSessionConnection实例
在MediaSessionConnection中创建 MediaBrowser ,执行connect()
2) MediaBrowserConnectionCallback::onConnected()
MediaBrowser::connect 连接成功后会调用 MediaBrowserConnectionCallback::onConnected(),在onConnected() 中做了两件事情 :其一 实例化MeidaController ;其二 更新 isConnected 状态 。
isConnected 使用了LiveData ,一旦值发生变化,就会执行下面定(MainActivityViewModel)的回调接口,进行下一步操作: 重新获取 mRootId(若Service允许客户端连接,则返回结果不为null,其值为数据内容层次结构的根ID;若拒绝连接,则返回null)
其中,mediaSessionConnection.rootMediaId 就是 MediaBrowser 的mRootId,而且获取mRootId时 MediaBrowser要求一定要连接上,不然会抛IllegalStateException。mRootId 设置是在 重写MediaBrowserServiceCompat ::onGetRoot 中构建的 BrowserRoot(Called to get the root information for browsing by a particular client ,用于区分)
在上面《MediaSession 框架相关点简介》已介绍 MediaBrowser是通过订阅方式向Service请求数据的。 而发起订阅请求需要两个参数,其一为mRootId ;而如果该mRootId已经被其他Browser实例订阅,则需要在订阅之前取消mRootId的订阅者( 注意 :订阅一个已被订阅的mRootId时会取代原Browser的订阅回调,但却无法触发onChildrenLoaded回调)
关于BrowserRoot 会在后续MediaBrowserService 部分讲解。
3)MediaBrowser:: subscribe
在MediaItemFragment::onActivityCreated 初始化MediaItemItemFragmentViewModel 利用MediaBrowser 向Service订阅服务 。
从Service 返回订阅数据,更新List
4)onChildrenLoaded
Service 通过 onChildrenLoaded 将数据返回给Client (MediaBrowser),这部分也在后续MediaBrowserService 部分讲解。
(2) MediaBrowserService
1)注册MusicService (MediaBrowserService )
2)播放后台服务
MediaBrowserService 作为一个后台播放服务却不是通过其自身直接实现的,而是通过MediaSession媒体会话这个类来实现的。在使用过程中媒体会话会与该服务关联起来,所有的播放操作都交由MediaSession实现。
在MusicService::onCreate() 中创建MediaSession ,设置sessionToken , 设置token后会触发MediaBrowserCompat.ConnectionCallback的回调方法。
另外,由于播放器使用的是ExoPlayer ,它会帮忙管理MediaSession。
3)在MusicService中重写onGetRoot(类似添加白名单)和 onLoadChildren(返回数据)
上面注释写得比较清楚,不展开讲。另外,UMP中播放器的Service是MusicService 继承自MediaBrowserServiceCompat,选择的IBinder方案是Messenger方案(Messenger 与AIDL异同点)
4)其他
BecomingNoisyReceiver :耳机插拔监听广播
MediaControllerCallback :数据状态变化 、播放状态 及 相关通知更新等
(3)MediaController
MediaController 的创建依赖于Session的配对令牌,当Browser和BrowserService连接成功(在前面已经介绍MediaBrowserConnectionCallback::onConnected() 中创建)我们就可以通过Browser拿到这个令牌了。控制器创建后,我们就可以通过MediaController.getTransportControls的方法发送播放指令,同时也可以注册MediaControllerCompat.Callback回调接收播放状态,用以刷新界面UI(播放状态 和数据状态)。
MediaController 核心 :mediaBrowser.sessionToken 和 MediaControllerCallback , MediaControllerCallback 实现如下 :
刷新UI
四、播放实例讲解
从上面介绍知,在已连接 有数据情况下,人操作播放器播放按钮,主要涉及 MediaController 和 MediaSession ,下面已播放为例来说明整个播放空中流程。
一般的播放流程包括 :
(1)获取MediaController对象
连接成功后创建MediaController对象
(2)通过MediaController拿到TransportControler对象
(3)通过TransportControler发送play()的指令
(4)MediaSession.Callback收到指令回调onPlay()方法
transportControls.play( ) ->
MediaControllerCompatApi21.TransportControls.play(mControlsObj) ->
对应对应MediaSessionRecord::ISessionController.Stub 实现 ,里面 play( )方法 调用 ISessionCallback ,ISessionCallback 接口如下 :
到此, transportControls 与MediaSession.Callback 就对接上了 ,执行onPlay 方法
(5)MediaController.Callback收到状态改变
(6)刷新UI界面
更新播放状态,最终更新UI界面
五、遇到的问题
在导入UMP项目时发现不能正常编译 ,AS报错如下 :
查看extension-mediasession 版本是2.8.4,寻根到底发现 exoplayer2.ext 库不存在。
解决办法 :升级库版本 到2.9.1,对应修改如下:
相关资料 :
(1)android-UniversalMusicPlayer
(2)Android 跨进程双向通信(Messenger与AIDL)详解