Android 解讀開源項目UniversalMusicPlayer(播放控制層)

版權聲明:本文爲博主原創文章,未經博主容許不得轉載
源碼:AnliaLee/android-UniversalMusicPlayer
你們要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論java

前言

因爲工做的緣由,很久沒更新博客了,以前說要寫UniversalMusicPlayer(後面統一簡稱UAMP)的源碼分析,雖然代碼中許多關鍵的地方都已經寫好了註釋,同時爲了方便你們閱讀也把Google原有的一些註釋翻譯了,但一直抽不出太多時間去寫博客,只能是像擠牙膏似的天天抽一個小模塊出來分析的樣子_(:з」∠)_。因此若是有急需這個項目資料的童鞋能夠關注一下我fork那個項目,通常我都會先在那寫好註釋而後再整理成博客,你們經過註釋應該也能夠將項目理清,就不須要再等個人龜速更新了~android

回到項目中來,我打算按照UAMP項目各個大模塊的劃分來寫,所以可能會寫好幾篇博客湊成一個系列。這幾篇博客沒有特定的順序,你們按需選擇某個模塊來看就行。另外UAMP播放器是基於MediaSession框架的,相關資料可參考Android 媒體播放框架MediaSession分析與實踐,下面就開始正文吧git

參考資料
googlesamples/android-UniversalMusicPlayergithub


項目簡介

UAMP播放器做爲Google的官方demo展現瞭如何去開發一款音頻媒體應用,該應用可跨多種外接設備使用,併爲Android手機,平板電腦,Android Auto,Android Wear,Android TV和Google Cast設備提供一致的用戶體驗安全

項目按照標準的MVC架構管理各個模塊,模塊結構以下圖所示bash

其中modeluiplayback模塊分別表明MVC架構中的model層、view層以及controller層。此外,UAMP項目中深度使用了MediaSession框架實現了數據管理、播放控制、UI更新等功能,本系列博客將從各個模塊入手,分析其源碼及重要功能的實現邏輯,這期主要講的是播放控制這塊的內容微信


播放控制模塊

在分析MediaSession框架的博客中咱們講到在客戶端使用MediaController發送指令,而後調用MediaBrowserService中重寫的回調接口控制播放器進行播放的工做,這樣就實現了從用戶操做界面到控制音頻播放的過程。分析這個過程咱們能夠得知播放器是運行在Service層的,而爲了將Service層和控制層進行解耦,UAMP項目中將播放器的控制邏輯放到了Playback的實例中,而後使用PlaybackManager做爲中間者管理ServiceMediaSession以及Playback之間的交互。它們之間的關聯與交互主要是經過各個回調方法來完成的:session

MediaBrowserService與PlaybackManager的關聯

  • PlaybackManager中定義回調接口PlaybackServiceCallbackMusicService(繼承自MediaBrowserService)實現了接口中的方法,同時也持有PlaybackManager的實例
//PlaybackManager.java
public interface PlaybackServiceCallback {
  void onPlaybackStart();
  void onNotificationRequired();
  void onPlaybackStop();
  void onPlaybackStateUpdated(PlaybackStateCompat newState);
}
複製代碼
//MusicService.java
public class MusicService extends MediaBrowserServiceCompat implements PlaybackManager.PlaybackServiceCallback 複製代碼
  • PlaybackManager的構造方法中須要傳入實現了PlaybackServiceCallback的實例,所以在MusicService中會將自身做爲參數構造PlaybackManager實例,此時MusicServicePlaybackManager之間完成了關聯,能夠相互調用回調方法用以傳達指令狀態
//PlaybackManager.java
public PlaybackManager(PlaybackServiceCallback serviceCallback, Resources resources, MusicProvider musicProvider, QueueManager queueManager, Playback playback) {
    ...
    mServiceCallback = serviceCallback;
}
複製代碼
//MusicService.java
private PlaybackManager mPlaybackManager;

@Override
public void onCreate() {
    ...
    mPlaybackManager = new PlaybackManager(this, getResources(), mMusicProvider, queueManager,playback);
}
複製代碼

PlaybackManager與MediaSession的關聯

  • PlaybackManager中實現了MediaSession的回調MediaSessionCallback,在MusicService配置MediaSession時能夠用PlaybackManager.getMediaSessionCallback拿到這個回調,而後調用MediaSession.setCallback傳入回調。此時PlaybackManagerMediaSession之間完成了關聯,後續使用MediaController發送指令時,指令經過上述回調最終會傳達至PlaybackManager
//PlaybackManager.java
private MediaSessionCallback mMediaSessionCallback;
public PlaybackManager(PlaybackServiceCallback serviceCallback, Resources resources, MusicProvider musicProvider, QueueManager queueManager, Playback playback) {
    ...
    mMediaSessionCallback = new MediaSessionCallback();
}

public MediaSessionCompat.Callback getMediaSessionCallback() {
    return mMediaSessionCallback;
}

private class MediaSessionCallback extends MediaSessionCompat.Callback {
    ...
}
複製代碼
//MusicService.java
@Override
public void onCreate() {
    mSession.setCallback(mPlaybackManager.getMediaSessionCallback());
}
複製代碼

PlaybackManager與Playback的關聯

  • Playback中定義了回調接口CallbackPlaybackManager實現了這個接口中的方法,同時持有Playback的實例(Playback自己也是接口,因此此處持有的是Playback的實例,默認爲LocalPlayback,其做爲參數在MusicService構造PlaybackManager實例時傳入)
//Playback.java
public interface Playback {
    ...
    interface Callback {
        /** * 當前音樂播放完成時調用 */
        void onCompletion();
        /** * 在播放狀態改變時調用 * 啓用該回調方法能夠更新MediaSession上的播放狀態 */
        void onPlaybackStatusChanged(int state);

        /** * @param error to be added to the PlaybackState */
        void onError(String error);

        /** * @param mediaId being currently played */
        void setCurrentMediaId(String mediaId);
    }
}
複製代碼
//PlaybackManager.java
public class PlaybackManager implements Playback.Callback 複製代碼
//LocalPlayback.java
public final class LocalPlayback implements Playback 複製代碼
//MusicService.java
@Override
public void onCreate() {
    ...
    LocalPlayback playback = new LocalPlayback(this, mMusicProvider);
    mPlaybackManager = new PlaybackManager(this, getResources(), mMusicProvider, queueManager, playback);
}
複製代碼
  • PlaybackManager的構造方法中拿到Playback的實例後,調用Playback.setCallback將自身做爲參數傳入,此時PlaybackManagerPlayback之間完成了關聯,能夠相互調用回調方法用以傳達指令狀態
//Playback.java
public interface Playback {
    ...
    void setCallback(Callback callback);
}
複製代碼
//PlaybackManager.java
public PlaybackManager(PlaybackServiceCallback serviceCallback, Resources resources, MusicProvider musicProvider, QueueManager queueManager, Playback playback) {
    ...
    mPlayback.setCallback(this);
}
複製代碼

簡單總結一下,UAMP播放控制流程能夠分爲指令下發狀態回傳兩個過程:架構

  • 指令下發能夠理解爲從客戶端UI層Playback層每一層經過調用下一層的實例的方法控制指令一直傳達到播放器,從而達到UI組件控制播放器播放音樂的功能
  • 狀態回傳則是指下層經過上層實現的回調播放狀態一路回傳到UI層中,用以更新UI組件的顯示

瞭解播放控制流程的設計思路以後,下面咱們開始分析一些具體功能的實現框架


與播放器的交互

前面咱們提到播放器的具體實現是放在Playback層的,那麼就先看看Playback類提供了哪些接口

public interface Playback {
    /** * Start/setup the playback. * Resources/listeners would be allocated by implementations. */
    void start();

    /** * Stop the playback. All resources can be de-allocated by implementations here. * @param notifyListeners if true and a callback has been set by setCallback, * callback.onPlaybackStatusChanged will be called after changing * the state. */
    void stop(boolean notifyListeners);

    /** * Set the latest playback state as determined by the caller. */
    void setState(int state);

    /** * Get the current {@link android.media.session.PlaybackState#getState()} */
    int getState();

    /** * @return boolean that indicates that this is ready to be used. */
    boolean isConnected();

    /** * @return boolean indicating whether the player is playing or is supposed to be * playing when we gain audio focus. */
    boolean isPlaying();

    /** * @return pos if currently playing an item */
    long getCurrentStreamPosition();

    /** * Queries the underlying stream and update the internal last known stream position. */
    void updateLastKnownStreamPosition();
    void play(QueueItem item);
    void pause();
    void seekTo(long position);
    void setCurrentMediaId(String mediaId);
    String getCurrentMediaId();
    void setCallback(Callback callback);
}
複製代碼

UAMP經過指令下發的流程將用戶點擊UI控件所發送的指令一路傳遞到Playback的方法中,以點擊播放按鈕爲例,播放指令傳遞過程當中調用的方法順序大體以下:

OnClickListener.onClick → MediaController.getTransportControls().play
→ MediaSession.Callback.onPlay → Playback.play
複製代碼

Playback類的具體實現是LocalPlayback,那麼咱們看下LocalPlayback.play方法都作了些什麼

//LocalPlayback.java
@Override
public void play(QueueItem item) {
    mPlayOnFocusGain = true;
    ...
    if (mExoPlayer == null) {
        mExoPlayer =
                ExoPlayerFactory.newSimpleInstance(
                        mContext, new DefaultTrackSelector(), new DefaultLoadControl());
        mExoPlayer.addListener(mEventListener);
    }
    ...
    mExoPlayer.prepare(mediaSource);
    ...
    configurePlayerState();
}

private void configurePlayerState() {
    ...
    if (mPlayOnFocusGain) {
        mExoPlayer.setPlayWhenReady(true);
        mPlayOnFocusGain = false;
    }
}
複製代碼

能夠看到這裏初始化了ExoPlayer播放器,並調用ExoPlayer相應的方法播放音頻

那麼和播放器交互的分析就到這,至於ExoPlayer的操做就不細說了,你們能夠對照着源碼中的註釋以及ExoPlayer的文檔理解其中的實現邏輯便可


耳機插拔的處理邏輯

當咱們插着線控耳機或者連着藍牙耳機聽歌時,有時可能會忽然發生意外的情況,形成耳機與設備斷連了(線被拔掉或者藍牙中斷了),爲了在公共場合下避免沒必要要的尷尬,此時播放程序通常都會自動暫停音樂的播放。這個功能不是系統幫咱們實現的,這須要咱們本身完成相應邏輯的開發

Android系統中有着音頻輸出通道的概念,例如當咱們使用線控耳機收聽音樂時,音樂是從Headset通道出來的,拔掉耳機後,音頻輸出的通道則會切換至Speaker通道,此時系統會發出AudioManager.ACTION_AUDIO_BECOMING_NOISY這一廣播告知咱們耳機被拔掉了。UAMP中正是經過監聽此廣播實現了耳機插拔的邏輯處理,以前咱們也提到了這功能是在LocalPlayback中實現的:

public final class LocalPlayback implements Playback {
    ...
    private boolean mAudioNoisyReceiverRegistered;
    private final IntentFilter mAudioNoisyIntentFilter =
            new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);

    private final BroadcastReceiver mAudioNoisyReceiver =
            new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
                        LogHelper.d(TAG, "Headphones disconnected.");
                        //當音樂正在播放中,通知Service暫停播放音樂(在Service.onStartCommand中處理此命令)
                        if (isPlaying()) {
                            Intent i = new Intent(context, MusicService.class);
                            i.setAction(MusicService.ACTION_CMD);
                            i.putExtra(MusicService.CMD_NAME, MusicService.CMD_PAUSE);
                            mContext.startService(i);
                        }
                    }
                }
            };

    private void registerAudioNoisyReceiver() {
        //註銷耳機插拔、藍牙耳機斷連的廣播接收者
        if (!mAudioNoisyReceiverRegistered) {
            mContext.registerReceiver(mAudioNoisyReceiver, mAudioNoisyIntentFilter);
            mAudioNoisyReceiverRegistered = true;
        }
    }

    private void unregisterAudioNoisyReceiver() {
        //註銷耳機插拔的廣播接收者
        if (mAudioNoisyReceiverRegistered) {
            mContext.unregisterReceiver(mAudioNoisyReceiver);
            mAudioNoisyReceiverRegistered = false;
        }
    }
}
複製代碼

有關接收到廣播後的操做已經在代碼的註釋中說明,就很少贅述了。此外,爲了防止內存泄漏,咱們須要在適當的時機註冊和註銷BroadcastReceiver,通常的邏輯就是開始播放音樂時註冊,暫停或中止播放時註銷,LocalPlayback中一樣遵循着這一邏輯,具體的你們看下源碼註冊和註銷兩個方法何時被調用就能夠了


有關音頻焦點的控制

在分析源碼以前,咱們先簡單瞭解一下什麼是音頻焦點。在Android系統中,設備全部發出的聲音統稱爲音頻流,這其中包括應用播放的音樂按鍵聲通知鈴聲電話的聲音等等。因爲Android是多任務系統,那麼這些聲音就存在同時播放的可能,咱們可能就會由於正在播放的音樂聲而錯過某些重要的提示音。系統雖然不會區分哪些聲音對咱們來講是更重要的,但它提供了一套機制讓開發者能夠本身處理多個音頻流同時播放的問題

Android 2.2以後引入了音頻焦點機制,各個應用能夠經過這個機制協商各自音頻輸出的優先級。這套機制提供了請求和放棄音頻焦點的方法,以及通知咱們音頻焦點狀態改變的監聽器。當咱們須要播放音頻時,就能夠嘗試請求獲取音頻焦點綁定狀態監聽器。如有其餘應用的音頻流忽然插手競爭音頻焦點時,系統會根據這個插手的音頻流的類型經過監聽器通知咱們音頻焦點狀態的改變。這個改變後的狀態其實也是系統對於如何處理當前播放音頻的一種建議,狀態類型以下:

  • AUDIOFOCUS_GAIN:獲得音頻焦點時觸發的狀態,請求獲得的音頻焦點通常會長期佔有
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:失去音頻焦點時觸發的狀態,在該狀態的時候不須要暫停音頻,可是咱們須要下降音頻的聲音
  • AUDIOFOCUS_LOSS_TRANSIENT:失去音頻焦點時觸發的狀態,可是該狀態不會長時間保持,此時咱們應該暫停音頻,且當從新獲取音頻焦點的時候繼續播放
  • AUDIOFOCUS_LOSS:失去音頻焦點時觸發的狀態,且這個狀態有可能會長期保持,此時應當暫停音頻並釋放音頻相關的資源

瞭解這些概念以後,咱們來看下在UAMP項目中官方給出的有關音頻焦點的實現示例。有關音頻焦點的實如今LocalPlayback類中,首先是定義須要用到的常量音頻焦點狀態監聽器

public final class LocalPlayback implements Playback {
    ...
    //當音頻失去焦點,且不須要中止播放,只須要減少音量時,咱們設置的媒體播放器音量大小
    //例如微信的提示音響起,咱們只須要減少當前音樂的播放音量便可
    public static final float VOLUME_DUCK = 0.2f;
    //當咱們獲取音頻焦點時設置的播放音量大小
    public static final float VOLUME_NORMAL = 1.0f;

    //沒有獲取到音頻焦點,也不容許duck狀態
    private static final int AUDIO_NO_FOCUS_NO_DUCK = 0;
    //沒有獲取到音頻焦點,但容許duck狀態
    private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1;
    //徹底獲取音頻焦點
    private static final int AUDIO_FOCUSED = 2;
    private boolean mPlayOnFocusGain;
    //當前音頻焦點的狀態
    private int mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;

    /** * 根據音頻焦點的設置從新配置播放器 以及 啓動/從新啓動 播放器。調用這個方法 啓動/從新啓動 播放器實例取決於當前音頻焦點的狀態。 * 所以若是咱們持有音頻焦點,則正常播放音頻;若是咱們失去音頻焦點,播放器將暫停播放或者設置爲低音量,這取決於當前焦點設置容許哪一種設置 */
    private void configurePlayerState() {
        LogHelper.d(TAG, "configurePlayerState. mCurrentAudioFocusState=", mCurrentAudioFocusState);
        if (mCurrentAudioFocusState == AUDIO_NO_FOCUS_NO_DUCK) {
            // We don't have audio focus and can't duck, so we have to pause
            pause();
        } else {
            registerAudioNoisyReceiver();

            if (mCurrentAudioFocusState == AUDIO_NO_FOCUS_CAN_DUCK) {
                // We're permitted to play, but only if we 'duck', ie: play softly
                mExoPlayer.setVolume(VOLUME_DUCK);
            } else {
                mExoPlayer.setVolume(VOLUME_NORMAL);
            }

            // If we were playing when we lost focus, we need to resume playing.
            if (mPlayOnFocusGain) {
                //播放的過程當中因失去焦點而暫停播放,短暫暫停以後仍須要繼續播放時會進入這裏執行相應的操做
                mExoPlayer.setPlayWhenReady(true);
                mPlayOnFocusGain = false;
            }
        }
    }

    /** * 請求音頻焦點成功以後監聽其狀態的Listener */
    private final AudioManager.OnAudioFocusChangeListener mOnAudioFocusChangeListener =
            new AudioManager.OnAudioFocusChangeListener() {
                @Override
                public void onAudioFocusChange(int focusChange) {
                    LogHelper.d(TAG, "onAudioFocusChange. focusChange=", focusChange);
                    switch (focusChange) {
                        case AudioManager.AUDIOFOCUS_GAIN:
                            mCurrentAudioFocusState = AUDIO_FOCUSED;
                            break;
                        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                            // Audio focus was lost, but it's possible to duck (i.e.: play quietly)
                            mCurrentAudioFocusState = AUDIO_NO_FOCUS_CAN_DUCK;
                            break;
                        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                            // Lost audio focus, but will gain it back (shortly), so note whether
                            // playback should resume
                            mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
                            mPlayOnFocusGain = mExoPlayer != null && mExoPlayer.getPlayWhenReady();
                            break;
                        case AudioManager.AUDIOFOCUS_LOSS:
                            // Lost audio focus, probably "permanently"
                            mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
                            break;
                    }

                    if (mExoPlayer != null) {
                        // Update the player state based on the change
                        configurePlayerState();
                    }
                }
            };
}
複製代碼

接着定義請求與放棄音頻焦點的方法

public final class LocalPlayback implements Playback {
    ...
    /** * 嘗試獲取音頻焦點 * requestAudioFocus(OnAudioFocusChangeListener l, int streamType, int durationHint) * OnAudioFocusChangeListener l:音頻焦點狀態監聽器 * int streamType:請求焦點的音頻類型 * int durationHint:請求焦點音頻持續性的指示 * AUDIOFOCUS_GAIN:指示申請獲得的音頻焦點不知道會持續多久,通常是長期佔有 * AUDIOFOCUS_GAIN_TRANSIENT:指示要申請的音頻焦點是暫時性的,會很快用完釋放的 * AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:指示要申請的音頻焦點是暫時性的,同時還指示當前正在使用焦點的音頻能夠繼續播放,只是要「duck」一下(下降音量) */
    private void tryToGetAudioFocus() {
        LogHelper.d(TAG, "tryToGetAudioFocus");
        int result =
                mAudioManager.requestAudioFocus(
                        mOnAudioFocusChangeListener,//狀態監聽器
                        AudioManager.STREAM_MUSIC,//
                        AudioManager.AUDIOFOCUS_GAIN);
        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            mCurrentAudioFocusState = AUDIO_FOCUSED;
        } else {
            mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
        }
    }

    /** * 放棄音頻焦點 */
    private void giveUpAudioFocus() {
        LogHelper.d(TAG, "giveUpAudioFocus");
        //申請放棄音頻焦點
        if (mAudioManager.abandonAudioFocus(mOnAudioFocusChangeListener)
                == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            //AudioManager.AUDIOFOCUS_REQUEST_GRANTED 申請成功
            mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
        }
    }
}
複製代碼

那麼什麼時候該請求(放棄)音頻焦點呢?舉個例子,播放器開始播放(中止)音樂時,就須要請求(放棄)焦點了

public final class LocalPlayback implements Playback {
    ...
    @Override
    public void stop(boolean notifyListeners) {
        giveUpAudioFocus();//放棄音頻焦點
        ...
    }

    @Override
    public void play(QueueItem item) {
        mPlayOnFocusGain = true;
        tryToGetAudioFocus();
        ...
        configurePlayerState();
    }
}
複製代碼

播放隊列控制

以前咱們講到了UAMP項目中數據層播放控制層是分離開來的,且正如使用PlaybackManager做爲中間者管理播放器,這裏一樣使用了QueueManager這個類做爲中間者連通數據層播放控制層Service層,並提供了隊列形式的存儲容器(這個隊列是線程安全的)以及能夠管理音樂層級關係的方法

那麼首先來看看如何初始化QueueManagerQueueManager中提供了一個對外的回調接口,重寫接口中的方法便可在QueueManager中操做外面的方法

public class QueueManager {
    ...
    /** * @param musicProvider 數據源提供者 * @param resources 系統資源 * @param listener 播放數據更新的回調接口 */
    public QueueManager(@NonNull MusicProvider musicProvider, @NonNull Resources resources, @NonNull MetadataUpdateListener listener) {
        ...
    }
    
    public interface MetadataUpdateListener {
        void onMetadataChanged(MediaMetadataCompat metadata);//媒體數據變動時調用
        void onMetadataRetrieveError();//媒體數據檢索失敗時調用
        void onCurrentQueueIndexUpdated(int queueIndex);//當前播放索引變動時調用
        void onQueueUpdated(String title, List<MediaSessionCompat.QueueItem> newQueue);//當前播放隊列變動時調用
    }
}
複製代碼

重寫這些回調方法是在MusicService建立時完成的,細心的小夥伴應該發現了以前在Service中就有將建立好的QueueManager做爲參數構造PlaybackManager類,咱們來看源碼

public class MusicService extends MediaBrowserServiceCompat implements PlaybackManager.PlaybackServiceCallback {
   ...
   @Override
   public void onCreate() {
       ...          
       QueueManager queueManager = new QueueManager(mMusicProvider, getResources(),
               new QueueManager.MetadataUpdateListener() {
                   @Override
                   public void onMetadataChanged(MediaMetadataCompat metadata) {
                       mSession.setMetadata(metadata);
                   }

                   @Override
                   public void onMetadataRetrieveError() {
                       mPlaybackManager.updatePlaybackState(
                               getString(R.string.error_no_metadata));
                   }

                   @Override
                   public void onCurrentQueueIndexUpdated(int queueIndex) {
                       mPlaybackManager.handlePlayRequest();
                   }

                   @Override
                   public void onQueueUpdated(String title, List<MediaSessionCompat.QueueItem> newQueue) {
                       mSession.setQueue(newQueue);
                       mSession.setQueueTitle(title);
                   }
               });
       mPlaybackManager = new PlaybackManager(this, getResources(), mMusicProvider, queueManager, playback);
   }
}
複製代碼

此時播放控制層就成功連上了QueueManager,以後就能夠調用其提供的方法找到當前要播放的音頻了,具體的你們能夠參照博主在源碼中的註釋,這裏就不一一拿出來細講了

這篇博客就先到這了,若是這個模塊還有什麼須要補充的,我會直接在這進行更新。如有什麼遺漏或者建議的歡迎留言評論,若是以爲博主寫得還不錯麻煩點個贊,大家的支持是我最大的動力~

相關文章
相關標籤/搜索