前言
最近維護專案的時候遇到了MediaSessionCompat框架的音樂播放器,簡單搜索記錄一下這套實作播放器的結構吧,
MediaSession框架簡介
我們先來看看如何設計一款音樂播放App的架構,傳統的做法是這樣的:
- 注冊一個Service,用于異步獲取音樂庫資料、音樂控制等,在Service中我們可能還需要自定義一些狀態值和回呼介面用于流程控制
- 通過廣播(其他方式如介面、Messenger都可以)實作Activity和Service之間的通信,使得用戶可以通過界面上的組件控制音樂的播放、暫停、拖動進度條等操作
如果我們的音樂播放器還需要支持通知欄快捷控制音樂播放的功能,那么又得新增一套廣播和相應的介面去回應通知欄按鈕的事件,
如果還需要支持多端(電視、手表、耳機等)控制同一個播放器,那么整個系統架構可能會變得非常復雜,我們要花費大量的時間和精力去設計、優化代碼的結構,那么有什么方法可以節省這些作業,提高我們的效率,然后還可以優雅地實作上述這些功能呢?
Google在Android 5.0中加入了MediaSession框架(在support-v4中同樣提供了相應的兼容包,相關的類以Compat結尾,Api基本相同),專門用來解決媒體播放時界面和Service通訊的問題,意在規范上述這些功能的流程,使用這個框架我們可以減少一些流程復雜的開發作業,例如使用各種廣播來控制播放器,而且其代碼可讀性、結構耦合度方面都控制得非常好,因此推薦大家嘗試下這個框架,下面我們就開始介紹MediaSession框架的核心成員和使用流程,
MediaSessionCompat位于android/support/v4/media/session包下,主要是用于替代Android L 之后推出的MessionSession,我們通過一張別人的圖來了解一下(這張圖說的是MediaSession,而不是MediaSessionCompat,但大致原理是一樣的):

常用成員類概述
MediaSession框架中有四個常用的成員類,它們是整個流程控制的核心
MediaBrowser
媒體瀏覽器,用來連接MediaBrowserService和訂閱資料,通過它的回呼介面我們可以獲取和Service的連接狀態以及獲取在Service中異步獲取的音樂庫資料,媒體瀏覽器一般創建于客戶端(可以理解為各個終端負責控制音樂播放的界面)中;
MediaBrowserService
瀏覽器服務,提供onGetRoot(控制客戶端媒體瀏覽器的連接請求,通過回傳值決定是否允許該客戶端連接服務)和onLoadChildren(媒體瀏覽器向Service發送資料訂閱時呼叫,一般在這執行異步獲取資料的操作,最后將資料發送至媒體瀏覽器的回呼介面中)這兩個抽象方法;
同時MediaBrowserService還作為承載媒體播放器(如MediaPlayer、ExoPlayer等)和MediaSession的容器
MediaSession
媒體會話,即受控端,通過設定MediaSessionCompat.Callback回呼來接收媒體控制器MediaController發送的指令,當收到指令時會觸發Callback中各個指令對應的回呼方法(回呼方法中會執行播放器相應的操作,如播放、暫停等),Session一般在Service.onCreate方法中創建,最后需呼叫setSessionToken方法設定用于和控制器配對的令牌并通知瀏覽器連接服務成功
MediaController
媒體控制器,在客戶端中開發者不僅可以使用控制器向Service中的受控端發送指令,還可以通過設定MediaControllerCompat.Callback回呼方法接收受控端的狀態,從而根據相應的狀態重繪界面UI,MediaController的創建需要受控端的配對令牌,因此需在瀏覽器成功連接服務的回呼執行創建的操作
通過上述的簡介中我們不難看出這四個成員之間有著非常明確的分工和作用范圍,使得整個代碼結構變得清晰易讀,
除此之外,MediaSession框架中還有一些同樣重要的類需要拿出來講,例如封裝了各種播放狀態的PlaybackState,和Map相似通過鍵值對保存媒體資訊的MediaMetadata,以及用于MediaBrowser和MediaBrowserService之間進行資料互動的MediaItem等等,下面我們通過實作一個簡單的demo來具體分析這套框架的作業流程,
使用MediaSession框架構建簡單的音樂播放器
例如我們的demo是這樣的(見下圖),只提供簡單的播放暫停操作,音樂資料源從raw資源檔案夾中獲取:

按照作業流程,我們就從獲取音樂庫資料開始吧,首先界面上方添加一個RecyclerView來展示獲取的音樂串列,我們在DemoActivity中完成一些RecyclerView的初始化操作
public class DemoActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private List<MediaBrowserCompat.MediaItem> list;
private DemoAdapter demoAdapter;
private LinearLayoutManager layoutManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
list = new ArrayList<>();
layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
demoAdapter = new DemoAdapter(this,list);
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(demoAdapter);
}
}
注意List元素的型別為MediaBrowserCompat.MediaItem,因為MediaBrowser從服務中獲取的每一首音樂都會封裝成MediaItem物件,接下來我們創建MediaBrowser,并執行連接服務端和訂閱資料的操作
public class DemoActivity extends AppCompatActivity {
...
private MediaBrowserCompat mBrowser;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
mBrowser = new MediaBrowserCompat(
this,
new ComponentName(this, MusicService.class),//系結服務端
browserConnectionCallback,//設定連接回呼
null
);
}
@Override
protected void onStart() {
super.onStart();
//Browser發送連接請求
mBrowser.connect();
}
@Override
protected void onStop() {
super.onStop();
mBrowser.disconnect();
}
/**
* 連接狀態的回呼介面,連接成功時會呼叫onConnected()方法
*/
private MediaBrowserCompat.ConnectionCallback browserConnectionCallback=
new MediaBrowserCompat.ConnectionCallback(){
@Override
public void onConnected() {
Log.e(TAG,"onConnected------");
//必須在確保連接成功的前提下執行訂閱的操作
if (mBrowser.isConnected()) {
//mediaId即為MediaBrowserService.onGetRoot的回傳值
//若Service允許客戶端連接,則回傳結果不為null,其值為資料內容層次結構的根ID
//若拒絕連接,則回傳null
String mediaId = mBrowser.getRoot();
//Browser通過訂閱的方式向Service請求資料,發起訂閱請求需要兩個引數,其一為mediaId
//而如果該mediaId已經被其他Browser實體訂閱,則需要在訂閱之前取消mediaId的訂閱者
//雖然訂閱一個 已被訂閱的mediaId 時會取代原Browser的訂閱回呼,但卻無法觸發onChildrenLoaded回呼
//ps:雖然基本的概念是這樣的,但是Google在官方demo中有這么一段注釋...
// This is temporary: A bug is being fixed that will make subscribe
// consistently call onChildrenLoaded initially, no matter if it is replacing an existing
// subscriber or not. Currently this only happens if the mediaID has no previous
// subscriber or if the media content changes on the service side, so we need to
// unsubscribe first.
//大概的意思就是現在這里還有BUG,即只要發送訂閱請求就會觸發onChildrenLoaded回呼
//所以無論怎樣我們發起訂閱請求之前都需要先取消訂閱
mBrowser.unsubscribe(mediaId);
//之前說到訂閱的方法還需要一個引數,即設定訂閱回呼SubscriptionCallback
//當Service獲取資料后會將資料發送回來,此時會觸發SubscriptionCallback.onChildrenLoaded回呼
mBrowser.subscribe(mediaId, BrowserSubscriptionCallback);
}
}
@Override
public void onConnectionFailed() {
Log.e(TAG,"連接失敗!");
}
};
/**
* 向媒體服務器(MediaBrowserService)發起資料訂閱請求的回呼介面
*/
private final MediaBrowserCompat.SubscriptionCallback browserSubscriptionCallback =
new MediaBrowserCompat.SubscriptionCallback(){
@Override
public void onChildrenLoaded(@NonNull String parentId,
@NonNull List<MediaBrowserCompat.MediaItem> children) {
Log.e(TAG,"onChildrenLoaded------");
//children 即為Service發送回來的媒體資料集合
for (MediaBrowserCompat.MediaItem item:children){
Log.e(TAG,item.getDescription().getTitle().toString());
list.add(item);
}
//在onChildrenLoaded可以執行重繪串列UI的操作
demoAdapter.notifyDataSetChanged();
}
};
}
通過上述的代碼和注釋大家應該清楚MediaBrowser從連接服務到向其訂閱資料的流程了,簡單總結一下就是:
connect → onConnected → subscribe → onChildrenLoaded
那么Service端那邊在這段流程中又做了什么呢?首先我們得繼承MediaBrowserService(這里使用了support-v4包的類)創建MusicService類,MediaBrowserService繼承自Service,所以記得在AndroidManifest.xml中完成配置:
<service
android:name=".demo.MusicService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
我們需要在Service初始化的時候就完成MediaSession的構建,并為它設定相應的標志、狀態等,具體的代碼如下:
public class MusicService extends MediaBrowserServiceCompat {
private MediaSessionCompat mSession;
private PlaybackStateCompat mPlaybackState;
@Override
public void onCreate() {
super.onCreate();
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_NONE,0,1.0f)
.build();
mSession = new MediaSessionCompat(this,"MusicService");
mSession.setCallback(SessionCallback);//設定回呼
mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mSession.setPlaybackState(mPlaybackState);
//設定token后會觸發MediaBrowserCompat.ConnectionCallback的回呼方法
//表示MediaBrowser與MediaBrowserService連接成功
setSessionToken(mSession.getSessionToken());
}
}
這里解釋下其中的一些細節,首先是呼叫MediaSession.setFlag為Session設定標志位,以便Session接收控制器的指令,然后是播放狀態的設定,需呼叫MediaSession.setPlaybackState,那么PlaybackState又是什么呢?之前我們簡單介紹過它是封裝了各種播放狀態的類,我們可以通過判斷當前播放狀態來控制各個成員的行為,而PlaybackState類為我們定義了各種狀態的規范,此外我們還需要設定SessionCallback回呼,當客戶端使用控制器發送指令時,就會觸發這些回呼方法,從而達到控制播放器的目的,
public class MusicService extends MediaBrowserServiceCompat {
...
private MediaPlayer mMediaPlayer;
@Override
public void onCreate() {
...
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnPreparedListener(PreparedListener);
mMediaPlayer.setOnCompletionListener(CompletionListener);
}
/**
* 回應控制器指令的回呼
*/
private android.support.v4.media.session.MediaSessionCompat.Callback SessionCallback = new MediaSessionCompat.Callback(){
/**
* 回應MediaController.getTransportControls().play
*/
@Override
public void onPlay() {
Log.e(TAG,"onPlay");
if(mPlaybackState.getState() == PlaybackStateCompat.STATE_PAUSED){
mMediaPlayer.start();
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING,0,1.0f)
.build();
mSession.setPlaybackState(mPlaybackState);
}
}
/**
* 回應MediaController.getTransportControls().onPause
*/
@Override
public void onPause() {
Log.e(TAG,"onPause");
if(mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING){
mMediaPlayer.pause();
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED,0,1.0f)
.build();
mSession.setPlaybackState(mPlaybackState);
}
}
/**
* 回應MediaController.getTransportControls().playFromUri
* @param uri
* @param extras
*/
@Override
public void onPlayFromUri(Uri uri, Bundle extras) {
Log.e(TAG,"onPlayFromUri");
try {
switch (mPlaybackState.getState()){
case PlaybackStateCompat.STATE_PLAYING:
case PlaybackStateCompat.STATE_PAUSED:
case PlaybackStateCompat.STATE_NONE:
mMediaPlayer.reset();
mMediaPlayer.setDataSource(MusicService.this,uri);
mMediaPlayer.prepare();//準備同步
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_CONNECTING,0,1.0f)
.build();
mSession.setPlaybackState(mPlaybackState);
//我們可以保存當前播放音樂的資訊,以便客戶端重繪UI
mSession.setMetadata(new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE,extras.getString("title"))
.build()
);
break;
}
}catch (IOException e){
e.printStackTrace();
}
}
@Override
public void onPlayFromSearch(String query, Bundle extras) {
}
};
/**
* 監聽MediaPlayer.prepare()
*/
private MediaPlayer.OnPreparedListener PreparedListener = new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
mMediaPlayer.start();
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING,0,1.0f)
.build();
mSession.setPlaybackState(mPlaybackState);
}
} ;
/**
* 監聽播放結束的事件
*/
private MediaPlayer.OnCompletionListener CompletionListener = new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_NONE,0,1.0f)
.build();
mSession.setPlaybackState(mPlaybackState);
mMediaPlayer.reset();
}
};
}
MediaSessionCompat.Callback中還有許多回呼方法,大家可以按需覆寫重寫即可:

構建好MediaSession后記得呼叫setSessionToken保存Session的配對令牌,同時呼叫此方法也會回呼MediaBrowser.ConnectionCallback的onConnected方法,告知客戶端Browser與BrowserService連接成功了,我們也就完成了MediaSession的創建和初始化
之前我們還講到Browser與BrowserService的訂閱關系,在MediaBrowserService中我們需要重寫onGetRoot和onLoadChildren方法,其作用之前已經講過就不多贅述了,
public class MusicService extends MediaBrowserServiceCompat {
@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
Log.e(TAG,"onGetRoot-----------");
return new BrowserRoot(MEDIA_ID_ROOT, null);
}
@Override
public void onl oadChildren(@NonNull String parentId, @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
Log.e(TAG,"onLoadChildren--------");
//將資訊從當前執行緒中移除,允許后續呼叫sendResult方法
result.detach();
//我們模擬獲取資料的程序,真實情況應該是異步從網路或本地讀取資料
MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ""+R.raw.jinglebells)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, "圣誕歌")
.build();
ArrayList<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
mediaItems.add(createMediaItem(metadata));
//向Browser發送資料
result.sendResult(mediaItems);
}
private MediaBrowserCompat.MediaItem createMediaItem(MediaMetadataCompat metadata){
return new MediaBrowserCompat.MediaItem(
metadata.getDescription(),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
);
}
}
最后我們回到客戶端這邊,四大成員還剩下控制器MediaController沒講,MediaController的創建依賴于Session的配對令牌,當Browser和BrowserService連接成功我們就可以通過Browser拿到這個令牌了,控制器創建后,我們就可以通過MediaController.getTransportControls的方法發送播放指令,同時也可以注冊MediaControllerCompat.Callback回呼接收播放狀態,用以重繪界面UI:
public class DemoActivity extends AppCompatActivity {
...
private Button btnPlay;
private TextView textTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
btnPlay = (Button) findViewById(R.id.btn_play);
textTitle = (TextView) findViewById(R.id.text_title);
}
public void clickEvent(View view) {
switch (view.getId()) {
case R.id.btn_play:
if(mController!=null){
handlerPlayEvent();
}
break;
}
}
/**
* 處理播放按鈕事件
*/
private void handlerPlayEvent(){
switch (mController.getPlaybackState().getState()){
case PlaybackStateCompat.STATE_PLAYING:
mController.getTransportControls().pause();
break;
case PlaybackStateCompat.STATE_PAUSED:
mController.getTransportControls().play();
break;
default:
mController.getTransportControls().playFromSearch("", null);
break;
}
}
/**
* 連接狀態的回呼介面,連接成功時會呼叫onConnected()方法
*/
private MediaBrowserCompat.ConnectionCallback browserConnectionCallback =
new MediaBrowserCompat.ConnectionCallback(){
@Override
public void onConnected() {
Log.e(TAG,"onConnected------");
if (mBrowser.isConnected()) {
...
try{
mController = new MediaControllerCompat(DemoActivity.this,mBrowser.getSessionToken());
//注冊回呼
mController.registerCallback(ControllerCallback);
}catch (RemoteException e){
e.printStackTrace();
}
}
}
@Override
public void onConnectionFailed() {
Log.e(TAG,"連接失敗!");
}
};
/**
* 媒體控制器控制播放程序中的回呼介面,可以用來根據播放狀態更新UI
*/
private final MediaControllerCompat.Callback ControllerCallback =
new MediaControllerCompat.Callback() {
/***
* 音樂播放狀態改變的回呼
* @param state
*/
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
switch (state.getState()){
case PlaybackStateCompat.STATE_NONE://無任何狀態
textTitle.setText("");
btnPlay.setText("開始");
break;
case PlaybackStateCompat.STATE_PAUSED:
btnPlay.setText("開始");
break;
case PlaybackStateCompat.STATE_PLAYING:
btnPlay.setText("暫停");
break;
}
}
/**
* 播放音樂改變的回呼
* @param metadata
*/
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
textTitle.setText(metadata.getDescription().getTitle());
}
};
private Uri rawToUri(int id){
String uriStr = "android.resource://" + getPackageName() + "/" + id;
return Uri.parse(uriStr);
}
}
MediaSession框架的基本用法我們已經分析完了,對于各自的播放器可能結構稍有不同,需要結合實際情況再分析,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/282644.html
標籤:其他
