音視頻開發(fā)之旅(45)-ExoPlayer 音頻播放器實踐(一)

通過上一篇的學習實踐蔼夜,我們了解了ExoPlayer的優(yōu)缺點以及基本用法,今天我們進入ExoPlayer的音頻播放實踐压昼,我們來一起實現(xiàn)一個簡單的音頻播放器求冷。


目錄

  1. 媒體播放框架MediaSession
  2. MediaSession框架+ExoPlayer 簡單音樂播放器實踐
  • 播放網絡音樂
  • 播放/暫停
  • 歌曲切換
  • 倍速播放
  1. 資料
  2. 收獲

一、媒體播放框架MediaSession

音頻播放器并不總是需要使其UI可見窍霞。一旦開始播放音頻匠题,播放器就可以作為后臺任務運行。用戶可以切換到另一個應用程序但金,并繼續(xù)聽韭山。
要在Android中實現(xiàn)這一設計,您可以使用兩個組件構建一個音頻應用程序: activity(展示所用) 和播放器service冷溃。如果用戶切換到另一個應用程序钱磅,則該service可以在后臺運行。通過將音頻應用程序的兩個部分分解為單獨的組件似枕,每個組件可以獨立運行盖淡。與播放器相比,UI通常是短暫的菠净,可能會在沒有UI的情況下運行很長時間禁舷。

在設計音樂播放器APP架構時彪杉,有幾種常用的做法
方案一

  1. 注冊Service,用于數(shù)據(jù)設置牵咙、音樂控制派近,在Service中自定義播放器的一些狀態(tài)值和回調接口用于流程控制
  2. 通過廣播、aidl等實現(xiàn)和頁面層邏輯的通信洁桌,使得用戶可以通過界面控制音樂的播放渴丸、暫停、切換另凌、seek等操作
  3. 使用RemoteControlClient(低版本)或者MediaSession(>5.0或者MediaSessionCompat)進行多端設備或者跨APP媒體會話

方案二
Android5.0時推出的MediaSession框架(Supprot包中MediaSessionCompat也對低版本做了支持)谱轨,專門用來解決媒體播放時界面和Service通信的問題,在結構低耦合方面的設計做的比較好

支持庫提供了兩個類來實現(xiàn)此客戶端/服務器方法:MediaBrowserService和MediaBrowser吠谢。該服務組件被實現(xiàn)為包含媒體會話及其播放器的MediaBrowserService的子類土童。使用UI和媒體控制器的活動應包括與MediaBrowserService進行通信的MediaBrowser。
使用MediaBrowserService可以讓隨身設備(如Android Auto and Wear)輕松發(fā)現(xiàn)您的應用工坊,連接到它献汗,瀏覽內容和控制播放,而無需訪問您的Activity

我們今天的學習實踐是基于方案二的MediaSession的框架


圖片來自 媒體應用架構概覽

MediaBrowser
用來連接MediaBrowserService和訂閱數(shù)據(jù)王污,通過他的回調可以獲取和Service的連接狀態(tài)以及獲取在Service中異步獲取的音樂數(shù)據(jù)(這個一般不在Service中進行獲取罢吃,因為涉及到的是具體的業(yè)務邏輯)

MediaBrowserService
是一個Service,封裝了媒體相關的一些功能昭齐,通過onGetRoot的返回值決定是否允許客戶端連接尿招。onLoadChildren回調在Sercive中異步獲取的數(shù)據(jù)給到MediaBrowser。也包含媒體播放器實例(比如我們本篇實踐的ExoPlayer)

MediaSession
一般在MediaBrowserService的onCreate中創(chuàng)建阱驾,通過MediaSession.CallBack回調接收MediaController發(fā)來的指令就谜,觸發(fā)對應的播放器相關的操作

MediaController
MediaContoller的創(chuàng)建需要MediaSession的配對令牌,在MediaBrowser連接服務成功之后創(chuàng)建里覆。MediaController可以主動的發(fā)送指令或者被動的接收MediaController.Callback回調來改變播放狀態(tài)和界面刷新吁伺。

更詳細的介紹請參考官方文檔或者Android 媒體播放框架MediaSession分析與實踐

二、 簡單實踐

下面我們看下如何使用MediaSession框架實現(xiàn)簡單的音頻播放

2.1 Server端實現(xiàn)

首先我們繼承MediaBrowserServiceCompat實現(xiàn)和注冊Service

public class MusicService extends MediaBrowserServiceCompat {

    private static final String TAG = "MusicService";
    private SimpleExoPlayer exoPlayer;
    private MediaSessionCompat mediaSession;

    /**
     * 當服務收到onCreate()生命周期回調方法時租谈,它應該執(zhí)行以下步驟:
     * 1. 創(chuàng)建并初始化media session
     * 2. 設置media session回調
     * 3. 設置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. 設置mediaSession回調
        mediaSession.setCallback(new MyMediaSessionCallBack());

        //3. 設置mediaSessionToken
       setSessionToken(mediaSession.getSessionToken());

        //創(chuàng)建播放器實例
        exoPlayer = new SimpleExoPlayer.Builder(getApplicationContext()).build();
    }
}

MediaSessionCompat.Callback的回調用于接收業(yè)務成通過mediaController.getTransportControls進行播放相關操作(播放篮奄、暫停、seek割去、倍速等等)的回調

 /**
     * 用于接收由MediaControl觸發(fā)的改變窟却,內部封裝實現(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有兩個回調方法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);
    }

    //獲取音視頻信息(這個更應該是在業(yè)務層處理事情)
    @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));
        }
        //當設置多首歌曲組成隊列時報錯
        // IllegalStateException: sendResult() called when either sendResult() or sendError() had already been called for: media_root_id
        //原因咖城,之前在for處理了茬腿,應該在設置好mediaItems列表后呼奢,統(tǒng)一設置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)改變(播放器內部發(fā)生狀態(tài)變化的回調,
                // 包括
                // 1. 用戶觸發(fā)的  比如: 手動切歌曲切平、暫停握础、播放、seek等悴品;
                // 2. 播放器內部觸發(fā) 比如: 播放結束禀综、自動切歌曲等)

                //該如何通知給ui業(yè)務層吶?苔严?好些只能通過回調
                //那有該如何 --》查看源碼得知通過setPlaybackState設置
                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è)務層注冊的MediaControllerCompat.Callback進行回調

                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端實現(xiàn)

下面我們再來看下Client端的實現(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連接請求 創(chuàng)建MusicService,收到onGetRoot回調值不為空說明建立連接成功--》然后觸發(fā)MyConnectionCallback的回調onConnected
        mediaBrowser.connect();
//        subscribe();
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.i(TAG, "onStop: ");
        mediaBrowser.disconnect();
    }
}

MediaBrowserCompat.ConnectionCallback用于接收與Server端連接的狀態(tài)回調

    public class MyConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
        @Override
        public void onConnected() {
            super.onConnected();
            Log.i(TAG, "onConnected: MyConnectionCallback");

            //MediaBrowser和MediaBrowerService建立連接之后會回調該方法
            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) {
                //這個callback 是Controller的callback届氢,即用戶觸發(fā)了播放欠窒、暫停,后發(fā)生狀態(tài)變化的回調退子。
                //像播放結束贱迟、自動切歌,則無法收到該回調(那該如何處理吶絮供?)

                mMediaControllerCallback = new MediaControllerCompat.Callback() {

                    //這里的回調,只有用戶觸發(fā)的才會有相應的回調茶敏。
                    //播放結束 這里沒有
                    //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 基本功能

歌曲播放播放暫停
當用戶點擊了播放/暫停按鈕后壤靶,獲取當前的播放狀態(tài),通過mediaController.getTransportControls給到通過Binder給到mediaSession惊搏,在service中MediaSessionCompat.Callback改變Exoplayer的播放狀態(tài)贮乳,exoplayer的onPlaybackStateChanged收到播放狀態(tài)改變的通知后觸發(fā),給mediasession設置mediaSession.setPlaybackState

對應關鍵代碼如下:

 client端用戶點擊事件處理
 
 //ExoSimpleAudioPlayerActivity.java
    
 PlaybackStateCompat playbackState = mediaController.getPlaybackState();
            int state = playbackState.getState();
            Log.i(TAG, "onClick: state=" + state);
            //通過 mediaController.getTransportControls 觸發(fā)MediaSessionCompat.Callback回調--》進行播放控制
            if (state == PlaybackStateCompat.STATE_PLAYING) {
                mediaController.getTransportControls().pause();
            } else {
                mediaController.getTransportControls().play();
            }

//Server端MediasessionCallback實現(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)改變(播放器內部發(fā)生狀態(tài)變化的回調向拆,
                // 包括
                // 1. 用戶觸發(fā)的  比如: 手動切歌曲、暫停酪耳、播放浓恳、seek等;
                // 2. 播放器內部觸發(fā) 比如: 播放結束碗暗、自動切歌曲等)

                //該如何通知給ui業(yè)務層吶颈将??好些只能通過回調
                //那有該如何 --》查看源碼得知通過setPlaybackState設置
                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è)務層注冊的MediaControllerCompat.Callback進行回調

                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());
    }

雖然知道了怎么使用晴圾,但是整個流程是怎樣的吶?
其中用到了Handler和Binder的線程和進程通信相關的知識噪奄,后續(xù)我們專題單獨深入學習實踐下死姚,這里我們先順著流程畫下播放/暫停的流程圖人乓,從用戶按下按鈕到播放器開始播放以及頁面更新的整個流程是怎樣的。


上一首下一首切換
歌曲切換流程個上面的播放流程基本上一致都毒,

//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的播放回調色罚,需要再sessionCallback中調用exoplayer的next/prev進行歌曲切換,并且設置新的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收到回調温鸽,根據(jù)狀態(tài)進行

   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++];
    }

然后再MediaSessionCallBack中實現(xiàn)onSetPlaybackSpeed回調保屯,進行播放倍速設置以及mession的設置

//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);
            //重新設置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ù)據(jù)如何交互涤垫,我們看到Activity直接和Service通過MediaSession框架中的各種回調進行通信姑尺,播放器ExoPlayer封裝在Service內,數(shù)據(jù)的獲取也在Service中蝠猬。這明顯和真實的場景有差異切蟋。
另外播放管理相關的沒有分離,播放隊列的維護榆芦,播放狀態(tài)的管理等等沒有統(tǒng)一的管理柄粹,不利于擴展擴展更換播放器等。

下一篇我們來分析umap的實現(xiàn)匆绣,它是如何進行架構的藻三,如何解決上面的問題的。

完整代碼已上傳至 github https://github.com/ayyb1988/mediajourney

三盅粪、資料

ExoPlayer

  1. Android開發(fā)之ExoPlayer的學習和使用(音頻)講解
  2. Media streaming with ExoPlayer
  3. ExoPlayer blog
  4. ExoPlayer developer guide
  5. Easy Audio Focus with ExoPlayer

UAMP相關

  1. Android 解讀開源項目UniversalMusicPlayer(播放控制層)
  2. Android 媒體播放框架MediaSession分析與實踐
  3. Android媒體應用(一)
  4. 音頻應用概覽
  5. 打造基于MediaSessionCompat的音樂播放(一)
  6. 打造基于MediaSessionCompat的音樂播放(二)

音頻播放器相關開源項目

  1. uamp
  2. 音頻可視化-audio-visualizer-android
  3. ListenerMusicPlayer
  4. Music-Player
  5. Timber
  6. Music-Cover-View

其他

  1. android 禁用和開啟四大組件的方法(setComponentEnabledSetting )
  2. Android 通知渠道Notification Channel

網絡接口以及歌曲來源

來自google官方的uamp開源項目

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

四咏删、收獲

通過本篇的學習實踐,

  1. 了解媒體播放框架MediaSession
  2. 使用MediaSession框架實現(xiàn)簡單的音頻播放器(播放/暫停拣凹、切歌森爽、倍速)
  3. 了解原理、具體實踐以及流程分析嚣镜,我們基本了解MediaSession的框架以及ExoPlayer簡單實用爬迟。
    但是一個音頻播放器以下功能也是基本功能:邊緩存變播放、播放隊列菊匿、淡入淡出付呕、音頻焦點、后臺播放跌捆,該如何比較好的實現(xiàn)吶凡涩?在具體實踐之前我們先來學習分析下uamp這個google開源的音頻播放器是如何架構的,看看在數(shù)據(jù)源設置以及播放管理方面是否可以學習借鑒疹蛉。

感謝你的閱讀

下一篇我們繼續(xù)學習實踐ExoPlayer活箕,分析uamp的設計與實現(xiàn),歡迎關注公眾號“音視頻開發(fā)之旅”,一起學習成長育韩。
歡迎交流

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末克蚂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子筋讨,更是在濱河造成了極大的恐慌埃叭,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悉罕,死亡現(xiàn)場離奇詭異赤屋,居然都是意外死亡,警方通過查閱死者的電腦和手機壁袄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門类早,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嗜逻,你說我怎么就攤上這事涩僻。” “怎么了栈顷?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵逆日,是天一觀的道長。 經常有香客問我萄凤,道長室抽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任靡努,我火速辦了婚禮坪圾,結果婚禮上,老公的妹妹穿的比我還像新娘颤难。我一直安慰自己,他們只是感情好已维,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布行嗤。 她就那樣靜靜地躺著,像睡著了一般垛耳。 火紅的嫁衣襯著肌膚如雪栅屏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天堂鲜,我揣著相機與錄音栈雳,去河邊找鬼。 笑死缔莲,一個胖子當著我的面吹牛哥纫,可吹牛的內容都是我干的。 我是一名探鬼主播痴奏,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼蛀骇,長吁一口氣:“原來是場噩夢啊……” “哼厌秒!你這毒婦竟也來了?” 一聲冷哼從身側響起擅憔,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鸵闪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后暑诸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚌讼,經...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年个榕,在試婚紗的時候發(fā)現(xiàn)自己被綠了篡石。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡笛洛,死狀恐怖夏志,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情苛让,我是刑警寧澤沟蔑,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站狱杰,受9級特大地震影響瘦材,放射性物質發(fā)生泄漏。R本人自食惡果不足惜仿畸,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一食棕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧错沽,春花似錦簿晓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至放可,卻和暖如春谒臼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背耀里。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工蜈缤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人冯挎。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓底哥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子叠艳,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內容