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

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


目錄

  1. 媒體播放框架MediaSession
  2. MediaSession框架+ExoPlayer 簡單音樂播放器實(shí)踐
  • 播放網(wǎng)絡(luò)音樂
  • 播放/暫停
  • 歌曲切換
  • 倍速播放
  1. 資料
  2. 收獲

一、媒體播放框架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í),有幾種常用的做法
方案一

  1. 注冊(cè)Service,用于數(shù)據(jù)設(shè)置、音樂控制,在Service中自定義播放器的一些狀態(tài)值和回調(diào)接口用于流程控制
  2. 通過廣播、aidl等實(shí)現(xiàn)和頁面層邏輯的通信,使得用戶可以通過界面控制音樂的播放、暫停、切換、seek等操作
  3. 使用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

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

UAMP相關(guān)

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

音頻播放器相關(guān)開源項(xiàng)目

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

其他

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

網(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í)踐,

  1. 了解媒體播放框架MediaSession
  2. 使用MediaSession框架實(shí)現(xiàn)簡單的音頻播放器(播放/暫停、切歌、倍速)
  3. 了解原理、具體實(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í)成長。
歡迎交流

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容