通過上一篇的學(xué)習(xí)實(shí)踐,我們了解了ExoPlayer的優(yōu)缺點(diǎn)以及基本用法,今天我們進(jìn)入ExoPlayer的音頻播放實(shí)踐,我們來一起實(shí)現(xiàn)一個(gè)簡單的音頻播放器。

目錄
- 媒體播放框架MediaSession
- MediaSession框架+ExoPlayer 簡單音樂播放器實(shí)踐
- 播放網(wǎng)絡(luò)音樂
- 播放/暫停
- 歌曲切換
- 倍速播放
- 資料
- 收獲
一、媒體播放框架MediaSession
音頻播放器并不總是需要使其UI可見。一旦開始播放音頻,播放器就可以作為后臺(tái)任務(wù)運(yùn)行。用戶可以切換到另一個(gè)應(yīng)用程序,并繼續(xù)聽。
要在Android中實(shí)現(xiàn)這一設(shè)計(jì),您可以使用兩個(gè)組件構(gòu)建一個(gè)音頻應(yīng)用程序: activity(展示所用) 和播放器service。如果用戶切換到另一個(gè)應(yīng)用程序,則該service可以在后臺(tái)運(yùn)行。通過將音頻應(yīng)用程序的兩個(gè)部分分解為單獨(dú)的組件,每個(gè)組件可以獨(dú)立運(yùn)行。與播放器相比,UI通常是短暫的,可能會(huì)在沒有UI的情況下運(yùn)行很長時(shí)間。
在設(shè)計(jì)音樂播放器APP架構(gòu)時(shí),有幾種常用的做法
方案一
- 注冊(cè)Service,用于數(shù)據(jù)設(shè)置、音樂控制,在Service中自定義播放器的一些狀態(tài)值和回調(diào)接口用于流程控制
- 通過廣播、aidl等實(shí)現(xiàn)和頁面層邏輯的通信,使得用戶可以通過界面控制音樂的播放、暫停、切換、seek等操作
- 使用RemoteControlClient(低版本)或者M(jìn)ediaSession(>5.0或者M(jìn)ediaSessionCompat)進(jìn)行多端設(shè)備或者跨APP媒體會(huì)話
方案二
Android5.0時(shí)推出的MediaSession框架(Supprot包中MediaSessionCompat也對(duì)低版本做了支持),專門用來解決媒體播放時(shí)界面和Service通信的問題,在結(jié)構(gòu)低耦合方面的設(shè)計(jì)做的比較好
支持庫提供了兩個(gè)類來實(shí)現(xiàn)此客戶端/服務(wù)器方法:MediaBrowserService和MediaBrowser。該服務(wù)組件被實(shí)現(xiàn)為包含媒體會(huì)話及其播放器的MediaBrowserService的子類。使用UI和媒體控制器的活動(dòng)應(yīng)包括與MediaBrowserService進(jìn)行通信的MediaBrowser。
使用MediaBrowserService可以讓隨身設(shè)備(如Android Auto and Wear)輕松發(fā)現(xiàn)您的應(yīng)用,連接到它,瀏覽內(nèi)容和控制播放,而無需訪問您的Activity
我們今天的學(xué)習(xí)實(shí)踐是基于方案二的MediaSession的框架

圖片來自 媒體應(yīng)用架構(gòu)概覽
MediaBrowser
用來連接MediaBrowserService和訂閱數(shù)據(jù),通過他的回調(diào)可以獲取和Service的連接狀態(tài)以及獲取在Service中異步獲取的音樂數(shù)據(jù)(這個(gè)一般不在Service中進(jìn)行獲取,因?yàn)樯婕暗降氖蔷唧w的業(yè)務(wù)邏輯)
MediaBrowserService
是一個(gè)Service,封裝了媒體相關(guān)的一些功能,通過onGetRoot的返回值決定是否允許客戶端連接。onLoadChildren回調(diào)在Sercive中異步獲取的數(shù)據(jù)給到MediaBrowser。也包含媒體播放器實(shí)例(比如我們本篇實(shí)踐的ExoPlayer)
MediaSession
一般在MediaBrowserService的onCreate中創(chuàng)建,通過MediaSession.CallBack回調(diào)接收MediaController發(fā)來的指令,觸發(fā)對(duì)應(yīng)的播放器相關(guān)的操作
MediaController
MediaContoller的創(chuàng)建需要MediaSession的配對(duì)令牌,在MediaBrowser連接服務(wù)成功之后創(chuàng)建。MediaController可以主動(dòng)的發(fā)送指令或者被動(dòng)的接收MediaController.Callback回調(diào)來改變播放狀態(tài)和界面刷新。
更詳細(xì)的介紹請(qǐng)參考官方文檔或者Android 媒體播放框架MediaSession分析與實(shí)踐
二、 簡單實(shí)踐
下面我們看下如何使用MediaSession框架實(shí)現(xiàn)簡單的音頻播放
2.1 Server端實(shí)現(xiàn)
首先我們繼承MediaBrowserServiceCompat實(shí)現(xiàn)和注冊(cè)Service
public class MusicService extends MediaBrowserServiceCompat {
private static final String TAG = "MusicService";
private SimpleExoPlayer exoPlayer;
private MediaSessionCompat mediaSession;
/**
* 當(dāng)服務(wù)收到onCreate()生命周期回調(diào)方法時(shí),它應(yīng)該執(zhí)行以下步驟:
* 1. 創(chuàng)建并初始化media session
* 2. 設(shè)置media session回調(diào)
* 3. 設(shè)置media session token
*/
@Override
public void onCreate() {
Log.i(TAG, "onCreate: ");
super.onCreate();
//1. 創(chuàng)建并初始化MediaSession
mediaSession = new MediaSessionCompat(getApplicationContext(), TAG);
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder()
.setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PLAY_PAUSE |
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID |
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH |
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS |
PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SEEK_TO)
.build();
mediaSession.setPlaybackState(playbackState);
//2. 設(shè)置mediaSession回調(diào)
mediaSession.setCallback(new MyMediaSessionCallBack());
//3. 設(shè)置mediaSessionToken
setSessionToken(mediaSession.getSessionToken());
//創(chuàng)建播放器實(shí)例
exoPlayer = new SimpleExoPlayer.Builder(getApplicationContext()).build();
}
}
MediaSessionCompat.Callback的回調(diào)用于接收業(yè)務(wù)成通過mediaController.getTransportControls進(jìn)行播放相關(guān)操作(播放、暫停、seek、倍速等等)的回調(diào)
/**
* 用于接收由MediaControl觸發(fā)的改變,內(nèi)部封裝實(shí)現(xiàn)播放器和播放狀態(tài)的改變
*/
private class MyMediaSessionCallBack extends MediaSessionCompat.Callback {
@Override
public void onPlay() {
super.onPlay();
Log.i(TAG, "onPlay: ");
exoPlayer.play();
}
@Override
public void onPause() {
super.onPause();
Log.i(TAG, "onPause: ");
exoPlayer.pause();
}
@Override
public void onSeekTo(long pos) {
super.onSeekTo(pos);
Log.i(TAG, "onSeekTo: pos=" + pos);
exoPlayer.seekTo(pos);
}
...
}
MediaBrowserServiceCompat有兩個(gè)回調(diào)方法onGetRoot和onLoadChildren。其中onGetRoot用于告訴MediaBrowser是否連接連接成功;onLoadChildren則是加載音視頻數(shù)據(jù)。
具體使用如下:
@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
Log.i(TAG, "onGetRoot: clientPackageName=" + clientPackageName + " clientUid=" + clientUid + " pid=" + Binder.getCallingPid()
+ " uid=" + Binder.getCallingUid());
//返回非空,表示連接成功
return new BrowserRoot("media_root_id", null);
}
//獲取音視頻信息(這個(gè)更應(yīng)該是在業(yè)務(wù)層處理事情)
@Override
public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
Log.i(TAG, "onLoadChildren: parentId=" + parentId);
List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
if (TextUtils.equals("media_root_id", parentId)) {
}
ArrayList<MusicEntity> musicEntityList = getMusicEntityList();
for (int i = 0; i < musicEntityList.size(); i++) {
MusicEntity musicEntity = musicEntityList.get(i);
MediaMetadataCompat metadataCompat = buildMediaMetadata(musicEntity);
if (i == 0) {
mediaSession.setMetadata(metadataCompat);
}
mediaItems.add(new MediaBrowserCompat.MediaItem(metadataCompat.getDescription(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));
exoPlayer.addMediaItem(MediaItem.fromUri(musicEntity.source));
}
//當(dāng)設(shè)置多首歌曲組成隊(duì)列時(shí)報(bào)錯(cuò)
// IllegalStateException: sendResult() called when either sendResult() or sendError() had already been called for: media_root_id
//原因,之前在for處理了,應(yīng)該在設(shè)置好mediaItems列表后,統(tǒng)一設(shè)置result
result.sendResult(mediaItems);
Log.i(TAG, "onLoadChildren: addMediaItem");
initExoPlayerListener();
exoPlayer.prepare();
Log.i(TAG, "onLoadChildren: prepare");
}
private void initExoPlayerListener() {
exoPlayer.addListener(new Player.EventListener() {
@Override
public void onPlaybackStateChanged(int state) {
long currentPosition = exoPlayer.getCurrentPosition();
long duration = exoPlayer.getDuration();
//狀態(tài)改變(播放器內(nèi)部發(fā)生狀態(tài)變化的回調(diào),
// 包括
// 1. 用戶觸發(fā)的 比如: 手動(dòng)切歌曲、暫停、播放、seek等;
// 2. 播放器內(nèi)部觸發(fā) 比如: 播放結(jié)束、自動(dòng)切歌曲等)
//該如何通知給ui業(yè)務(wù)層吶??好些只能通過回調(diào)
//那有該如何 --》查看源碼得知通過setPlaybackState設(shè)置
Log.i(TAG, "onPlaybackStateChanged: currentPosition=" + currentPosition + " duration=" + duration + " state=" + state);
int playbackState;
switch (state) {
default:
case Player.STATE_IDLE:
playbackState = PlaybackStateCompat.STATE_NONE;
break;
case Player.STATE_BUFFERING:
playbackState = PlaybackStateCompat.STATE_BUFFERING;
break;
case Player.STATE_READY:
if(exoPlayer.getPlayWhenReady()){
playbackState = PlaybackStateCompat.STATE_PLAYING;
}else {
playbackState = PlaybackStateCompat.STATE_PAUSED;
}
break;
case Player.STATE_ENDED:
playbackState = PlaybackStateCompat.STATE_STOPPED;
break;
}
//播放器的狀態(tài)變化,通過mediasession告訴在ui業(yè)務(wù)層注冊(cè)的MediaControllerCompat.Callback進(jìn)行回調(diào)
setPlaybackState(playbackState);
}
private void setPlaybackState(int playbackState) {
float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());
}
@NotNull
private ArrayList<MusicEntity> getMusicEntityList() {
ArrayList<MusicEntity> list = new ArrayList<MusicEntity>();
...
MusicEntity musicEntity2 = new MusicEntity();
musicEntity2.id = "wake_up_02";
musicEntity2.title = "Geisha";
musicEntity2.album = "Wake Up";
musicEntity2.artist = "Media Right Productions";
musicEntity2.genre = "Electronic";
musicEntity2.source = "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/02_-_Geisha.mp3";
musicEntity2.image = "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg";
musicEntity2.trackNumber = 2;
musicEntity2.totalTrackCount = 13;
musicEntity2.duration = 267;
musicEntity2.site = "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/";
list.add(musicEntity2);
return list;
}
2.2 Client端實(shí)現(xiàn)
下面我們?cè)賮砜聪翪lient端的實(shí)現(xiàn)
public class ExoSimpleAudioPlayerActivity extends Activity implements View.OnClickListener {
private MediaBrowserCompat mediaBrowser;
private MediaBrowserCompat.ConnectionCallback mConnectionCallbacks = new MyConnectionCallback();
private MediaControllerCompat.Callback mMediaControllerCallback;
private MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple_audio);
...
//mConnectionCallbacks 是C-S連接的callback
mediaBrowser = new MediaBrowserCompat(this, new ComponentName(this, MusicService.class),
mConnectionCallbacks, null);
}
@Override
protected void onStart() {
super.onStart();
Log.i(TAG, "onStart: ");
//發(fā)出C-S連接請(qǐng)求 創(chuàng)建MusicService,收到onGetRoot回調(diào)值不為空說明建立連接成功--》然后觸發(fā)MyConnectionCallback的回調(diào)onConnected
mediaBrowser.connect();
// subscribe();
}
@Override
protected void onStop() {
super.onStop();
Log.i(TAG, "onStop: ");
mediaBrowser.disconnect();
}
}
MediaBrowserCompat.ConnectionCallback用于接收與Server端連接的狀態(tài)回調(diào)
public class MyConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
@Override
public void onConnected() {
super.onConnected();
Log.i(TAG, "onConnected: MyConnectionCallback");
//MediaBrowser和MediaBrowerService建立連接之后會(huì)回調(diào)該方法
MediaSessionCompat.Token sessionToken = mediaBrowser.getSessionToken();
//建立連接之后再創(chuàng)建MediaController
mediaController = new MediaControllerCompat(ExoSimpleAudioPlayerActivity.this, sessionToken);
MediaControllerCompat.setMediaController(ExoSimpleAudioPlayerActivity.this, mediaController);
subscribe();
//MediaController發(fā)送命令
buildTransportControls();
if (mMediaControllerCallback == null) {
//這個(gè)callback 是Controller的callback,即用戶觸發(fā)了播放、暫停,后發(fā)生狀態(tài)變化的回調(diào)。
//像播放結(jié)束、自動(dòng)切歌,則無法收到該回調(diào)(那該如何處理吶?)
mMediaControllerCallback = new MediaControllerCompat.Callback() {
//這里的回調(diào),只有用戶觸發(fā)的才會(huì)有相應(yīng)的回調(diào)。
//播放結(jié)束 這里沒有
//ExoPlayer getDuration : https://stackoverflow.com/questions/35298125/exoplayer-getduration
// Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
super.onPlaybackStateChanged(state);
Log.i(TAG, "onPlaybackStateChanged: state=" + state.getState());
if (PlaybackStateCompat.STATE_PLAYING == state.getState()) {
playButton.setText("暫停");
} else {
playButton.setText("播放");
}
updatePlaybackState(state);
MediaMetadataCompat metadata = mediaController.getMetadata();
updateDuration(metadata);
}
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
super.onMetadataChanged(metadata);
durationSet = false;
Log.i(TAG, "onMetadataChanged: metadata=" + metadata.toString());
updateDuration(metadata);
}
}
mediaController.registerCallback(mMediaControllerCallback);
PlaybackStateCompat state = mediaController.getPlaybackState();
updatePlaybackState(state);
updateProgress();
if (state != null && (state.getState() == PlaybackStateCompat.STATE_PLAYING ||
state.getState() == PlaybackStateCompat.STATE_BUFFERING)) {
scheduleSeekbarUpdate();
}
//通過mediaController獲取MediaMetadataCompat
MediaMetadataCompat metadata = mediaController.getMetadata();
updateDuration(metadata);
}
@Override
public void onConnectionFailed() {
super.onConnectionFailed();
}
}
2.3 基本功能
歌曲播放播放暫停
當(dāng)用戶點(diǎn)擊了播放/暫停按鈕后,獲取當(dāng)前的播放狀態(tài),通過mediaController.getTransportControls給到通過Binder給到mediaSession,在service中MediaSessionCompat.Callback改變Exoplayer的播放狀態(tài),exoplayer的onPlaybackStateChanged收到播放狀態(tài)改變的通知后觸發(fā),給mediasession設(shè)置mediaSession.setPlaybackState
對(duì)應(yīng)關(guān)鍵代碼如下:
client端用戶點(diǎn)擊事件處理
//ExoSimpleAudioPlayerActivity.java
PlaybackStateCompat playbackState = mediaController.getPlaybackState();
int state = playbackState.getState();
Log.i(TAG, "onClick: state=" + state);
//通過 mediaController.getTransportControls 觸發(fā)MediaSessionCompat.Callback回調(diào)--》進(jìn)行播放控制
if (state == PlaybackStateCompat.STATE_PLAYING) {
mediaController.getTransportControls().pause();
} else {
mediaController.getTransportControls().play();
}
//Server端MediasessionCallback實(shí)現(xiàn),接收mediaController.getTransportControls()的事件
//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack
@Override
public void onPlay() {
super.onPlay();
Log.i(TAG, "onPlay: ");
exoPlayer.play();
}
@Override
public void onPause() {
super.onPause();
Log.i(TAG, "onPause: ");
exoPlayer.pause();
}
//server端 exoplayer狀態(tài)變化監(jiān)聽
//com.example.myplayer.audio.MusicService#initExoPlayerListener
exoPlayer.addListener(new Player.EventListener() {
@Override
public void onPlaybackStateChanged(int state) {
long currentPosition = exoPlayer.getCurrentPosition();
long duration = exoPlayer.getDuration();
//狀態(tài)改變(播放器內(nèi)部發(fā)生狀態(tài)變化的回調(diào),
// 包括
// 1. 用戶觸發(fā)的 比如: 手動(dòng)切歌曲、暫停、播放、seek等;
// 2. 播放器內(nèi)部觸發(fā) 比如: 播放結(jié)束、自動(dòng)切歌曲等)
//該如何通知給ui業(yè)務(wù)層吶??好些只能通過回調(diào)
//那有該如何 --》查看源碼得知通過setPlaybackState設(shè)置
Log.i(TAG, "onPlaybackStateChanged: currentPosition=" + currentPosition + " duration=" + duration + " state=" + state);
int playbackState;
switch (state) {
default:
case Player.STATE_IDLE:
playbackState = PlaybackStateCompat.STATE_NONE;
break;
case Player.STATE_BUFFERING:
playbackState = PlaybackStateCompat.STATE_BUFFERING;
break;
case Player.STATE_READY:
if(exoPlayer.getPlayWhenReady()){
playbackState = PlaybackStateCompat.STATE_PLAYING;
}else {
playbackState = PlaybackStateCompat.STATE_PAUSED;
}
break;
case Player.STATE_ENDED:
playbackState = PlaybackStateCompat.STATE_STOPPED;
break;
}
//播放器的狀態(tài)變化,通過mediasession告訴在ui業(yè)務(wù)層注冊(cè)的MediaControllerCompat.Callback進(jìn)行回調(diào)
setPlaybackState(playbackState);
}
}
private void setPlaybackState(int playbackState) {
float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());
}
雖然知道了怎么使用,但是整個(gè)流程是怎樣的吶?
其中用到了Handler和Binder的線程和進(jìn)程通信相關(guān)的知識(shí),后續(xù)我們專題單獨(dú)深入學(xué)習(xí)實(shí)踐下,這里我們先順著流程畫下播放/暫停的流程圖,從用戶按下按鈕到播放器開始播放以及頁面更新的整個(gè)流程是怎樣的。

上一首下一首切換
歌曲切換流程個(gè)上面的播放流程基本上一致,
//com.example.myplayer.audio.ExoSimpleAudioPlayerActivity#onClick
if (id == R.id.prev) {
if (mediaController != null) {
mediaController.getTransportControls().skipToPrevious();
}
} else if (id == R.id.next) {
if (mediaController != null) {
mediaController.getTransportControls().skipToNext();
}
}
區(qū)別在于 沒有觸發(fā)ExoPlayer的播放回調(diào),需要再sessionCallback中調(diào)用exoplayer的next/prev進(jìn)行歌曲切換,并且設(shè)置新的playstate狀態(tài)給到mession
//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack
@Override
public void onSkipToNext() {
super.onSkipToNext();
Log.i(TAG, "onSkipToNext: ");
exoPlayer.next();
exoPlayer.setPlayWhenReady(true);
setPlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_NEXT);
mediaSession.setMetadata(getMediaMetadata(1));
}
@Override
public void onSkipToPrevious() {
super.onSkipToPrevious();
Log.i(TAG, "onSkipToPrevious: ");
exoPlayer.previous();
exoPlayer.setPlayWhenReady(true);
setPlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS);
mediaSession.setMetadata(getMediaMetadata(0));
}
最終MediaControllerCallback的onPlaybackStateChanged收到回調(diào),根據(jù)狀態(tài)進(jìn)行
public void onPlaybackStateChanged(PlaybackStateCompat state) {
super.onPlaybackStateChanged(state);
...
if (state.getState() == PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS || state.getState() == PlaybackStateCompat.STATE_SKIPPING_TO_NEXT) {
updateShowMediaInfo(description);
}
}
private void updateShowMediaInfo(MediaDescriptionCompat description) {
if (description == null) return;
titleView.setText(description.getTitle());
artistView.setText(description.getSubtitle());
Glide.with(ExoSimpleAudioPlayerActivity.this).load(description.getIconUri().toString()).into(iconView);
Uri mediaUri = description.getMediaUri();
Uri iconUri = description.getIconUri();
Log.i(TAG, "onChildrenLoaded: title=" + description.getTitle() + " subtitle=" + description.getSubtitle()
+ " mediaUri=" + mediaUri + " iconUri=" + iconUri);
}
倍速
//com.example.myplayer.audio.ExoSimpleAudioPlayerActivity#onClick
if (id == R.id.speed) {
if (mediaController != null) {
float speed = getSpeed();
speedView.setText("倍速 " + speed);
mediaController.getTransportControls().setPlaybackSpeed(speed);
}
}
float[] speedArray = new float[]{0.5f, 1f, 1.5f, 2f};
int curSpeedIndex = 1;
private float getSpeed() {
if (curSpeedIndex > 3) {
curSpeedIndex = 0;
}
return speedArray[curSpeedIndex++];
}
然后再M(fèi)ediaSessionCallBack中實(shí)現(xiàn)onSetPlaybackSpeed回調(diào),進(jìn)行播放倍速設(shè)置以及mession的設(shè)置
//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack
@Override
public void onSetPlaybackSpeed(float speed) {
super.onSetPlaybackSpeed(speed);
Log.i(TAG, "onSetPlaybackSpeed: speed=" + speed);
PlaybackParameters playParams = new PlaybackParameters(speed);
exoPlayer.setPlaybackParameters(playParams);
//重新設(shè)置mediaSession.setPlaybackState 告知 監(jiān)聽者 speed變化
setPlaybackState(exoPlayer.getPlaybackState());
}
private void setPlaybackState(int playbackState) {
float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());
}
需要注意
播放狀態(tài) MediaSession框架和ExoPlayer的不同與聯(lián)系
//android.support.v4.media.session.PlaybackStateCompat
TATE_NONE, STATE_STOPPED, STATE_PAUSED, STATE_PLAYING, STATE_FAST_FORWARDING,
STATE_REWINDING, STATE_BUFFERING, STATE_ERROR, STATE_CONNECTING,
STATE_SKIPPING_TO_PREVIOUS, STATE_SKIPPING_TO_NEXT, STATE_SKIPPING_TO_QUEUE_ITEM
//com.google.android.exoplayer2.Player.State
STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED
2.4 存在的問題
上面的實(shí)踐中存在一些問題,比如數(shù)據(jù)如何交互,我們看到Activity直接和Service通過MediaSession框架中的各種回調(diào)進(jìn)行通信,播放器ExoPlayer封裝在Service內(nèi),數(shù)據(jù)的獲取也在Service中。這明顯和真實(shí)的場景有差異。
另外播放管理相關(guān)的沒有分離,播放隊(duì)列的維護(hù),播放狀態(tài)的管理等等沒有統(tǒng)一的管理,不利于擴(kuò)展擴(kuò)展更換播放器等。
下一篇我們來分析umap的實(shí)現(xiàn),它是如何進(jìn)行架構(gòu)的,如何解決上面的問題的。
完整代碼已上傳至 github https://github.com/ayyb1988/mediajourney
三、資料
ExoPlayer
- Android開發(fā)之ExoPlayer的學(xué)習(xí)和使用(音頻)講解
- Media streaming with ExoPlayer
- ExoPlayer blog
- ExoPlayer developer guide
- Easy Audio Focus with ExoPlayer
UAMP相關(guān)
- Android 解讀開源項(xiàng)目UniversalMusicPlayer(播放控制層)
- Android 媒體播放框架MediaSession分析與實(shí)踐
- Android媒體應(yīng)用(一)
- 音頻應(yīng)用概覽
- 打造基于MediaSessionCompat的音樂播放(一)
- 打造基于MediaSessionCompat的音樂播放(二)
音頻播放器相關(guān)開源項(xiàng)目
其他
網(wǎng)絡(luò)接口以及歌曲來源
來自google官方的uamp開源項(xiàng)目
http://storage.googleapis.com/automotive-media/music.json
https://storage.googleapis.com/uamp/catalog.json
Music provided by the [Free Music Archive](http://freemusicarchive.org/).
- [Irsen's Tale](http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/) by
[Kai Engel](http://freemusicarchive.org/music/Kai_Engel/).
- [Wake Up](http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/) by
[The Kyoto Connection](http://freemusicarchive.org/music/The_Kyoto_Connection/).
長音頻:https://v.typlog.com/oohomechat/8385162738_706123.mp3
四、收獲
通過本篇的學(xué)習(xí)實(shí)踐,
- 了解媒體播放框架MediaSession
- 使用MediaSession框架實(shí)現(xiàn)簡單的音頻播放器(播放/暫停、切歌、倍速)
- 了解原理、具體實(shí)踐以及流程分析,我們基本了解MediaSession的框架以及ExoPlayer簡單實(shí)用。
但是一個(gè)音頻播放器以下功能也是基本功能:邊緩存變播放、播放隊(duì)列、淡入淡出、音頻焦點(diǎn)、后臺(tái)播放,該如何比較好的實(shí)現(xiàn)吶?在具體實(shí)踐之前我們先來學(xué)習(xí)分析下uamp這個(gè)google開源的音頻播放器是如何架構(gòu)的,看看在數(shù)據(jù)源設(shè)置以及播放管理方面是否可以學(xué)習(xí)借鑒。
感謝你的閱讀
下一篇我們繼續(xù)學(xué)習(xí)實(shí)踐ExoPlayer,分析uamp的設(shè)計(jì)與實(shí)現(xiàn),歡迎關(guān)注公眾號(hào)“音視頻開發(fā)之旅”,一起學(xué)習(xí)成長。
歡迎交流