文章集合:
Universal Music Player 源碼解析(一)--MediaSession框架
Univeral Music Player 源碼解析 -- 讓人頭疼的playback
Universal Music Player 源碼解析(二)--MusicService 和 MediaController
Universal Music Player 源碼分析 (三)-- 其他類分析
這篇文章主要承接上文,說明這幾個問題:
- 如何獲得音樂數(shù)據(jù)?
- UI 層如何和playback層交互?
- 令人困惑的MediaId是什么? 他是怎么傳到
MediaPlayerActivity
中的?
關于音樂數(shù)據(jù)的獲得:
在RemoteJSONSource
中,發(fā)起了一個http請求,解析成為一個JSONObject
private JSONObject fetchJSONFromUrl(String urlString)
之后:
private MediaMetadataCompat
buildFromJSON(JSONObject json, String basePath){
...
String id = String.valueOf(source.hashCode());
....
return new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
.putString(MusicProviderSource.CUSTOM_METADATA_TRACK_SOURCE, source)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
.putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, iconUrl)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, trackNumber)
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, totalTrackCount)
.build();
....
}
使用Builder模式,將拿到的數(shù)據(jù),初始化了一個MediaMetaData
對象.
這個地方引入了一個新的概念:MediaMetaData
是什么?
同樣,還有后面會出現(xiàn)的概念MediaItem
,MediaDescription
等 ,我一起總結(jié)一下:
MediaMetaData
---音樂元數(shù)據(jù),包含的是很多剛才從RemoteJSONSource
中拿到的數(shù)據(jù).
通過MediaMetaData
getMediaDescription()
可以很容易得到一個MediaDescription
對象
MediaItem
是MediaBrowser
的子類
主要用來表征這個item是browsable
orplayable
另外,這里有個很重要的信息: getMediaID()
,同樣具體意義暫且不表,后面會詳細解釋.
很容易可以看出,MediaItem
MediaDescription
作為MediaBrowser的子類的聯(lián)系很強,同樣getDescription()
也可以獲得MediaDescription
所以綜上所述,MediaItem
是對音樂數(shù)據(jù)的一個終極封裝,但是詳細的信息,比如:
getMediaUri()
getSubTitle/ title()
只能通過MediaItem.getDescription()
再拿一次
音樂數(shù)據(jù)的播放:
剛才的RemoteJSONSource
是繼承自MusicProviderSource
類的,實現(xiàn)了其中的iterator()
@Override
public Iterator<MediaMetadataCompat> iterator() {
.....
ArrayList<MediaMetadataCompat> tracks = new ArrayList<>();
if (jsonObj != null) {
JSONArray jsonTracks = jsonObj.getJSONArray(JSON_MUSIC);
if (jsonTracks != null) {
for (int j = 0; j < jsonTracks.length(); j++) {
tracks.add(buildFromJSON(jsonTracks.getJSONObject(j), path));
}
}
}
....
}
這個方法將會在MusicProvider
中被使用:
private synchronized void retrieveMedia() {
try {
if (mCurrentState == State.NON_INITIALIZED) {
mCurrentState = State.INITIALIZING;
Iterator<MediaMetadataCompat> tracks = mSource.iterator();
while (tracks.hasNext()) {
MediaMetadataCompat item = tracks.next();
String musicId = item.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID);
mMusicListById.put(musicId, new MutableMediaMetadata(musicId, item));
}
buildListsByGenre();
mCurrentState = State.INITIALIZED;
}
} finally {
if (mCurrentState != State.INITIALIZED) {
// Something bad happened, so we reset state to NON_INITIALIZED to allow
// retries (eg if the network connection is temporary unavailable)
mCurrentState = State.NON_INITIALIZED;
}
}
}
于是我們獲得了一個map,value是根據(jù)當時的jsonObject獲取的hashcode,key是一個固定的 string
鋪墊的部分已經(jīng)結(jié)束了,正式來看一下音樂是如何播放的:
如果用戶在界面上點了暫停/播放 這個是怎么實現(xiàn)的呢?
音樂播放的邏輯是結(jié)合MusicService
還有Playback
層實現(xiàn)的,具體可以參考一下我這篇博客
如果為了增強閱讀連貫性的讀者可以看看我的總結(jié),迅速上手:
還有關于handlePlayRequest和MusicService這幾個類的聯(lián)系:
播放的控制是由PlaybackControlsFragment
管理的 ,當點擊播放/暫停的時候:
private final View.OnClickListener mButtonListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
MediaControllerCompat controller = MediaControllerCompat.getMediaController(getActivity());
PlaybackStateCompat stateObj = controller.getPlaybackState();
final int state = stateObj == null ?
PlaybackStateCompat.STATE_NONE : stateObj.getState();
LogHelper.d(TAG, "Button pressed, in state " + state);
switch (v.getId()) {
case R.id.play_pause:
LogHelper.d(TAG, "Play button pressed, in state " + state);
if (state == PlaybackStateCompat.STATE_PAUSED ||
state == PlaybackStateCompat.STATE_STOPPED ||
state == PlaybackStateCompat.STATE_NONE) {
playMedia();
} else if (state == PlaybackStateCompat.STATE_PLAYING ||
state == PlaybackStateCompat.STATE_BUFFERING ||
state == PlaybackStateCompat.STATE_CONNECTING) {
pauseMedia();
}
break;
}
}
};
通過conntroller對象獲取到playbackState,調(diào)用playMedia()
private void playMedia() {
MediaControllerCompat controller = MediaControllerCompat.getMediaController(getActivity());
if (controller != null) {
controller.getTransportControls().play();
}
}
出乎意料的簡潔有咩有!
我們知道MediaController
是根據(jù)MediaSession.Token
創(chuàng)建的,并且綁定在了context上,所以調(diào)用play()
就可以控制播放,就會fire MediaSession中的回調(diào),不信請看:
由于在MusicService 中
mSession.setCallback(mPlaybackManager.getMediaSessionCallback());
看看Callback
private class MediaSessionCallback extends MediaSessionCompat.Callback {
@Override
public void onPlay() {
LogHelper.d(TAG, "play");
if (mQueueManager.getCurrentMusic() == null) {
mQueueManager.setRandomQueue();
}
handlePlayRequest();
}
....
}
最終還是調(diào)用handlexxxRequest()
同樣的,跳到下一首:
@Override
public void onSkipToQueueItem(long queueId) {
LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId);
mQueueManager.setCurrentQueueItem(queueId);
mQueueManager.updateMetadata();
}
最終都會調(diào)用handlePlayRequest()
MediaID的獲取
先看一下MusicService:
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
Bundle rootHints) {
LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName,
"; clientUid=" + clientUid + " ; rootHints=", rootHints);
// To ensure you are not allowing any arbitrary app to browse your app's contents, you
// need to check the origin:
if (!mPackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) {
// If the request comes from an untrusted package, return an empty browser root.
// If you return null, then the media browser will not be able to connect and
// no further calls will be made to other media browsing methods.
LogHelper.i(TAG, "OnGetRoot: Browsing NOT ALLOWED for unknown caller. "
+ "Returning empty browser root so all apps can use MediaController."
+ clientPackageName);
return new MediaBrowserServiceCompat.BrowserRoot(MEDIA_ID_EMPTY_ROOT, null);
}
//noinspection StatementWithEmptyBody
return new BrowserRoot(MEDIA_ID_ROOT, null);
}
@Override
public void onLoadChildren(@NonNull final String parentMediaId,
@NonNull final Result<List<MediaItem>> result) {
LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId);
if (MEDIA_ID_EMPTY_ROOT.equals(parentMediaId)) {
result.sendResult(new ArrayList<MediaItem>());
} else if (mMusicProvider.isInitialized()) {
// if music library is ready, return immediately
result.sendResult(mMusicProvider.getChildren(parentMediaId, getResources()));
} else {
// otherwise, only return results when the music library is retrieved
result.detach();
mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() {
@Override
public void onMusicCatalogReady(boolean success) {
result.sendResult(mMusicProvider.getChildren(parentMediaId, getResources()));
}
});
}
}
MusicService 繼承MediaBrowserServiceCompat
并且實現(xiàn)兩個方法:onGetRoot()
onLoadChildren()
通過onGetRoot()可以返回一個BrowserRoot實例,需要檢查其他的應用有沒有權(quán)限獲取這個我們的應用中的數(shù)據(jù),
onLoadChildren()
使用了一種特殊的機制返回返回值,這個函數(shù)的返回值是void , 我們通過在MediaBrowseFragment 中subscribe
mMediaFragmentListener.getMediaBrowser().unsubscribe(mMediaId);
mMediaFragmentListener.getMediaBrowser().subscribe(mMediaId, mSubscriptionCallback);
看一下mSubscriptionCallback
private final MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback =
//media description:contains the metadata of a song
//media item 構(gòu)造函數(shù) (MediaDescription,int flags)
new MediaBrowserCompat.SubscriptionCallback() {
@Override
public void onChildrenLoaded(@NonNull String parentId,
@NonNull List<MediaBrowserCompat.MediaItem> children) {
try {
LogHelper.d(TAG, "fragment onChildrenLoaded, parentId=" + parentId +
" count=" + children.size());
checkForUserVisibleErrors(children.isEmpty());
mBrowserAdapter.clear();
for (MediaBrowserCompat.MediaItem item : children) {
mBrowserAdapter.add(item);
}
mBrowserAdapter.notifyDataSetChanged();
} catch (Throwable t) {
LogHelper.e(TAG, "Error on childrenloaded", t);
}
}
@Override
public void onError(@NonNull String id) {
LogHelper.e(TAG, "browse fragment subscription onError, id=" + id);
Toast.makeText(getActivity(), R.string.error_loading_media, Toast.LENGTH_LONG).show();
checkForUserVisibleErrors(true);
}
};
向adapter中添加內(nèi)容,所以我們才可以瀏覽