Univeral Music Player 源碼解析 -- 讓人頭疼的playback

文章集合:
Universal Music Player 源碼解析(一)--MediaSession框架

Univeral Music Player 源碼解析 -- 讓人頭疼的playback

Universal Music Player 源碼解析(二)--MusicService 和 MediaController

Universal Music Player 源碼分析 (三)-- 其他類分析

項目中的playback層是非常令人頭痛的,因為他的回調(diào)之多,不亞于MediaSession框架使用的回調(diào),而且這里是播放器主要的功能的看點所在,所以這里的理解也要比其他地方更加深刻才可以,下面,我從項目目錄入手剖析一下:

看完這篇文章你將會知道這樣幾個部分:

  • LocalPlayback 和 PlaybackManager 的關系
  • QueueManager的介紹
  • MusicService 和 MediaSession的關系
image.png

除了castplayback其他的都需要仔細理解,因為這個是用于電視上播放需要的.
PlaybackManager 實現(xiàn)了Playback的callback接口,
LocalPlayback是本地音樂的播放類,實現(xiàn)了Playback接口,瞄一眼Playback接口的structure:

image.png

下面從LocalPlayback切入,跟著源碼分析一下:

LocalPlayback

它的重要之處和難以理解之處主要在下面幾個方面:LocalPlayback所需要實現(xiàn)的play方法:

 @Override
    public void play(QueueItem item) {
        mPlayOnFocusGain = true;
        tryToGetAudioFocus();
        registerAudioNoisyReceiver();
        String mediaId = item.getDescription().getMediaId();
        boolean mediaHasChanged = !TextUtils.equals(mediaId, mCurrentMediaId);
        if (mediaHasChanged) {
            mCurrentMediaId = mediaId;
        }

//通過解析 MediaId中的hashcode 獲取對應的 MetaData
 MediaMetadataCompat track =
                    mMusicProvider.getMusic(
                            MediaIDHelper.extractMusicIDFromMediaID(
                                    item.getDescription().getMediaId()));

//source 獲取是一個url
            String source = track.getString(MusicProviderSource.CUSTOM_METADATA_TRACK_SOURCE);
            if (source != null) {
                source = source.replaceAll(" ", "%20"); // Escape spaces for URLs
            }

            if (mExoPlayer == null) {
                mExoPlayer =
                        ExoPlayerFactory.newSimpleInstance(
                                mContext, new DefaultTrackSelector(), new DefaultLoadControl());
                mExoPlayer.addListener(mEventListener);
            }

   
            final AudioAttributes audioAttributes = new AudioAttributes.Builder()
                    .setContentType(CONTENT_TYPE_MUSIC)
                    .setUsage(USAGE_MEDIA)
                    .build();
            mExoPlayer.setAudioAttributes(audioAttributes);

            // Produces DataSource instances through which media data is loaded
            // dataSource 是一個接口通過DefaultDataSourceFactory給予默認的實現(xiàn)
            DataSource.Factory dataSourceFactory =
                    new DefaultDataSourceFactory(
                            //Utils.getUserAgent返回一哥他包含包名version的字符串
                            mContext, Util.getUserAgent(mContext, "uamp"),
                            null);
            // Produces Extractor instances for parsing the media data.
            ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
            // The MediaSource represents the media to be played.
            MediaSource mediaSource =
                    new ExtractorMediaSource(
                            Uri.parse(source), dataSourceFactory, extractorsFactory, null, null);

            // Prepares media to play (happens on background thread) and triggers
            // {@code onPlayerStateChanged} callback when the stream is ready to play.
            mExoPlayer.prepare(mediaSource);
      }
 configurePlayerState();
}

解釋一下各個類的作用

DataSource.Factory 數(shù)據(jù)可以被讀取的接口
 Extractor : 從所屬的格式中解析出媒體文件的數(shù)據(jù)

使用mExoPlayer 會觸發(fā) 這個回調(diào):

  @Override
        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
            switch (playbackState) {
                case ExoPlayer.STATE_IDLE:
                case ExoPlayer.STATE_BUFFERING:
                case ExoPlayer.STATE_READY:
                    if (mCallback != null) {
                        mCallback.onPlaybackStatusChanged(getState());
                    }
                    break;
...從
}

這個mCallback是一個PlaybackManager的實例,函數(shù)的實現(xiàn)如下:


//updatePlaybackState()就是對state的一個管理 沒有我們關心的關于播放的源碼
    @Override
    public void onPlaybackStatusChanged(int state) {
        updatePlaybackState(null);
    }

上面說過mExoPlayer.prepare()ok之后會回調(diào)函數(shù),但是在上面的代碼片段的下面的configurePlayerState(); 才是關鍵,我們呢看看這個是什么:

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

其中有很多奇怪的名詞,比如noisyReceiver DUCK 這些我會一一介紹,先讓我們關注音樂的播放這里:

在上面的代碼片段中,唯一和音樂播放扯得上關系的就是

//當getPlaybackState() == STATE_READY 時候繼續(xù)播放
mExoPlayer.setPlayWhenReady(true);

事實上,ExoPlayer和我們想象的是由差別的,其中并沒有play()方法,根據(jù)這篇博客來看只要調(diào)用

prepare()
setPlayWhenReady()

之后當player準備好了,就可以播放.

繼續(xù)解釋一下上面奇怪的名詞:

noisyReceiver


    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.");
                        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);
                        }
                    }
                }
            };

關鍵在于AudioManager.ACTION_AUDIO_BECOMING_NOISY這個action指的是當耳機被拔下來的時候會觸發(fā),這個也符合我們生活中的體驗,當耳機被拔下來的時候會有zizizi的聲音,如果是QQ音樂或者是網(wǎng)易云音樂的話,當耳機拔出是不會繼續(xù)播放的,這個時候要交給MusicService繼續(xù)判斷一下,我們來看一下MusicService如何處理是否繼續(xù)播放:

以下代碼出自MusicService;

 @Override
    public int onStartCommand(Intent startIntent, int flags, int startId) {
        Log.d("start", "onStartCommand: ");
        if (startIntent != null) {
            String action = startIntent.getAction();
            String command = startIntent.getStringExtra(CMD_NAME);
            if (ACTION_CMD.equals(action)) {
                if (CMD_PAUSE.equals(command)) {
                    mPlaybackManager.handlePauseRequest();
                } else if (CMD_STOP_CASTING.equals(command)) {
                    CastContext.getSharedInstance(this).getSessionManager().endCurrentSession(true);
                }
            } else {
                // Try to handle the intent as a media button event wrapped by MediaButtonReceiver
                MediaButtonReceiver.handleIntent(mSession, startIntent);
            }
        }
....
}

我們傳遞的狀態(tài)MusicService.CMD_PAUSE是暫停,符合預期,音樂播放也會暫停.

好,繼續(xù)解釋下一個名詞:

DUCK

duck其實值得不是鴨子,換句話說,當AudioFocus資源被競爭的時候,需要降低一下音量,也就是"duck"一下

和DUCK相關的代碼:

// The volume we set the media player to when we lose audio focus, but are
    // allowed to reduce the volume instead of stopping playback.
    public static final float VOLUME_DUCK = 0.2f;
    // The volume we set the media player when we have audio focus.
    public static final float VOLUME_NORMAL = 1.0f;

    // we don't have audio focus, and can't duck (play at a low volume)
    private static final int AUDIO_NO_FOCUS_NO_DUCK = 0;
    // we don't have focus, but can duck (play at a low volume)
    private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1;
    // we have full audio focus
    private static final int AUDIO_FOCUSED = 2;

這些代碼在OnAudioChangedListener的回調(diào)中被處理,我們就看一下前面的幾個case:

 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的焦點有一個連續(xù)的失去的是哦后,但是開發(fā)者希望通過減小音量的方式而不讓用戶體驗很差
                            mCurrentAudioFocusState = AUDIO_NO_FOCUS_CAN_DUCK;
                            break;
....//省略一些case


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

在configurePlayerState()中:

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

我們根據(jù)LocalPlayback來分析UAM對AudioFoucs的處理:


    @Override
    public void play(QueueItem item) {
 ...
        tryToGetAudioFocus();
        registerAudioNoisyReceiver();
...
}

LocalPlayback 對Playback的實現(xiàn)方法play中使用了tryToGetAudioFocus(); 同時也把剛才實現(xiàn)的onAudioFocusChangeListener作為requestAudioFocus的一個參數(shù)

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

同樣在stop()中要及時的丟棄Audio focus,避免系統(tǒng)通知服務再次播放音頻,比如在音頻播放完成之后的某個時間段內(nèi),電話也結束了,此時沒有丟棄音頻焦點,系統(tǒng)又會通知自動播放


PlaybackManager

我們再來看一下PlayBackManager是做什么的:
開始的時候我不能理解為什么要PlaybackManager實現(xiàn)Playback.Callback接口,而讓Playback實現(xiàn)Playback接口,其實仔細看一下Playback的方法命名,這個是很有道理的:

Playback中是play() pause() stop()這些比較具體的的抽象,理應應該交給一個具體的類去實現(xiàn),而PlayBackManager中實現(xiàn)的Playback.Callback是比較抽象的方法,就像公司中的Manger只是發(fā)布命令,完成任務的是其他的員工這樣.

我們來看看一下PlaybackManager:

 public PlaybackManager(PlaybackServiceCallback serviceCallback, Resources resources,
                           MusicProvider musicProvider, QueueManager queueManager,
                           Playback playback) {
        mMusicProvider = musicProvider;
        mServiceCallback = serviceCallback;
        mResources = resources;
        mQueueManager = queueManager;
        mMediaSessionCallback = new MediaSessionCallback();
        mPlayback = playback;
        mPlayback.setCallback(this);
    }

因為MusicService 中實現(xiàn)了PlayerServiceCallback接口,所以這個callback是一個MusicService對象,MusicService實現(xiàn)的回調(diào)其中的

 @Override
    public void onPlaybackStart() {
        mSession.setActive(true);
        startService(new Intent(getApplicationContext(), MusicService.class));
    }

是最關鍵的,開啟了一個service,service的onCreate()方法,繼續(xù)看看service oncreate方法.

注意

這里有一個問題,之前說過在BaseActivity中的 mediaBrowser.connect()會bind啟動 service,這個地方應該不會再次調(diào)用onCreate()方法了吧?

使得確實不會,這個地方和前文中沒有描述只是為了邏輯需要,請大家注意下,MusicSerivce早就已經(jīng)onCreate過(在BaseActivity創(chuàng)建的時候),這個地方會直接執(zhí)行onStartCommand()

image.png

繼續(xù)看onCreate()方法,如果你仔細觀察你就會發(fā)現(xiàn)MusicService和MediaSession 聯(lián)系比較緊密,而在QueueManager的回調(diào)中大量的使用了有關于MediaSession的方法,我們以深度優(yōu)先的方法看一下QueueManager內(nèi)部的實現(xiàn):

  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);
                    }
                });
    mSession.setMetadata(metadata);

    mSession.setQueue(newQueue);
    mSession.setQueueTitle(title);

以下代碼出自QueueManager中:

這個是Metadata的獲取,

public void updateMetadata() {
        MediaSessionCompat.QueueItem currentMusic = getCurrentMusic();
        if (currentMusic == null) {
            mListener.onMetadataRetrieveError();
            return;
        }
        final String musicId = MediaIDHelper.extractMusicIDFromMediaID(
                currentMusic.getDescription().getMediaId());
        MediaMetadataCompat metadata = mMusicProvider.getMusic(musicId);
        if (metadata == null) {
            throw new IllegalArgumentException("Invalid musicId " + musicId);
        }

        mListener.onMetadataChanged(metadata);
.....
        }
    }

容易看出,metaData來自之前分析過的MeidaProvider定義的map中,

  mSession.setQueue(newQueue);
    mSession.setQueueTitle(title);

也是如法炮制的.

我們回到MusicService中QueueManger,下面還有一個函數(shù)

   QueueManager queueManager = new QueueManager(mMusicProvider, getResources(),
                new QueueManager.MetadataUpdateListener() {
               ....
 
                    @Override
                    public void onCurrentQueueIndexUpdated(int queueIndex) {
                        mPlaybackManager.handlePlayRequest();
                    }
               ...
                });

調(diào)用了PlayManager的handlePlayRequest()

public void handlePlayRequest() {
      LogHelper.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState());
      MediaSessionCompat.QueueItem currentMusic = mQueueManager.getCurrentMusic();
      if (currentMusic != null) {
          mServiceCallback.onPlaybackStart();
          mPlayback.play(currentMusic);
      }
  }

在這里QueueManger交出了QueueItem 以讓音樂播放,所以總結一下:

image.png

還有以handlePlayRequest()為核心分析的PlaybackManager,LocalPlayback ,MusicService的聯(lián)系

image.png

相關代碼:
ExoPlayerListener

private final class ExoPlayerEventListener implements ExoPlayer.EventListener {
        @Override
        public void onTimelineChanged(Timeline timeline, Object manifest) {
            // Nothing to do.
        }

        @Override
        public void onTracksChanged(
                TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
            // Nothing to do.
        }

        @Override
        public void onLoadingChanged(boolean isLoading) {
            // Nothing to do.
        }

        @Override
        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
            switch (playbackState) {
                case ExoPlayer.STATE_IDLE:
                case ExoPlayer.STATE_BUFFERING:
                case ExoPlayer.STATE_READY:
                    if (mCallback != null) {
                        mCallback.onPlaybackStatusChanged(getState());
                    }
                    break;
                case ExoPlayer.STATE_ENDED:
                    // The media player finished playing the current song.
                    if (mCallback != null) {
                        mCallback.  onCompletion();
                    }
                    break;
            }
        }

        @Override
        public void onPlayerError(ExoPlaybackException error) {
            final String what;
            switch (error.type) {
                case ExoPlaybackException.TYPE_SOURCE:
                    what = error.getSourceException().getMessage();
                    break;
                case ExoPlaybackException.TYPE_RENDERER:
                    what = error.getRendererException().getMessage();
                    break;
                case ExoPlaybackException.TYPE_UNEXPECTED:
                    what = error.getUnexpectedException().getMessage();
                    break;
                default:
                    what = "Unknown: " + error;
            }

            LogHelper.e(TAG, "ExoPlayer error: what=" + what);
            if (mCallback != null) {
                mCallback.onError("ExoPlayer error " + what);
            }
        }

        @Override
        public void onPositionDiscontinuity() {
            // Nothing to do.
        }

        @Override
        public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
            // Nothing to do.
        }

        @Override
        public void onRepeatModeChanged(int repeatMode) {
            // Nothing to do.
        }
    }

PlaybackManager實現(xiàn)的Playback.Callback中的onCompletion

@Override
    public void onCompletion() {
        // The media player finished playing the current song, so we go ahead
        // and start the next.
        if (mQueueManager.skipQueuePosition(1)) {
            handlePlayRequest();
            mQueueManager.updateMetadata();
        } else {
            // If skipping was not possible, we stop and release the resources:
            handleStopRequest(null);
        }
    }

在上面的代碼中的onPlayerStateChanged中被調(diào)用

MusicService.setCallback()

  mSession.setCallback(mPlaybackManager.getMediaSessionCallback());

links
Android Audio Focus

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子泥技,更是在濱河造成了極大的恐慌症杏,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件誉察,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機稚铣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來墅垮,“玉大人惕医,你說我怎么就攤上這事∷闵” “怎么了抬伺?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長灾梦。 經(jīng)常有香客問我峡钓,道長,這世上最難降的妖魔是什么若河? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任能岩,我火速辦了婚禮,結果婚禮上萧福,老公的妹妹穿的比我還像新娘拉鹃。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布膏燕。 她就那樣靜靜地躺著钥屈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪坝辫。 梳的紋絲不亂的頭發(fā)上篷就,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機與錄音近忙,去河邊找鬼腻脏。 笑死,一個胖子當著我的面吹牛银锻,可吹牛的內(nèi)容都是我干的永品。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼击纬,長吁一口氣:“原來是場噩夢啊……” “哼鼎姐!你這毒婦竟也來了?” 一聲冷哼從身側響起更振,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤炕桨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后肯腕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體献宫,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年实撒,在試婚紗的時候發(fā)現(xiàn)自己被綠了姊途。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡知态,死狀恐怖捷兰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情负敏,我是刑警寧澤贡茅,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站其做,受9級特大地震影響顶考,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜妖泄,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一驹沿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧浮庐,春花似錦甚负、人聲如沸柬焕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽斑举。三九已至,卻和暖如春病涨,著一層夾襖步出監(jiān)牢的瞬間富玷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工既穆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赎懦,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓幻工,卻偏偏與公主長得像励两,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子囊颅,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容