文章集合:
Universal Music Player 源码解析(一)--MediaSession框架
Univeral Music Player 源码解析 -- 让人头疼的playback
Universal Music Player 源码解析(二)--MusicService 和 MediaController
Universal Music Player 源码分析 (三)-- 其他类分析
这篇文章主要承接上文,说明这几个问题:
- 如何获得音乐数据?
- UI 层如何和playback层交互?
- 令人困惑的MediaId是什么? 他是怎么传到
MediaPlayerActivity
中的?
关于音乐数据的获得:
在RemoteJSONSource
中,发起了一个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模式,将拿到的数据,初始化了一个MediaMetaData
对象.
这个地方引入了一个新的概念:MediaMetaData
是什么?
同样,还有后面会出现的概念MediaItem
,MediaDescription
等 ,我一起总结一下:
MediaMetaData
---音乐元数据,包含的是很多刚才从RemoteJSONSource
中拿到的数据.
通过MediaMetaData
getMediaDescription()
可以很容易得到一个MediaDescription
对象
MediaItem
是MediaBrowser
的子类
主要用来表征这个item是browsable
orplayable
另外,这里有个很重要的信息: getMediaID()
,同样具体意义暂且不表,后面会详细解释.
很容易可以看出,MediaItem
MediaDescription
作为MediaBrowser的子类的联系很强,同样getDescription()
也可以获得MediaDescription
所以综上所述,MediaItem
是对音乐数据的一个终极封装,但是详细的信息,比如:
getMediaUri()
getSubTitle/ title()
只能通过MediaItem.getDescription()
再拿一次
音乐数据的播放:
刚才的RemoteJSONSource
是继承自MusicProviderSource
类的,实现了其中的iterator()
@Override
public Iterator iterator() {
.....
ArrayList 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 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是根据当时的jsonObject获取的hashcode,key是一个固定的 string
铺垫的部分已经结束了,正式来看一下音乐是如何播放的:
如果用户在界面上点了暂停/播放 这个是怎么实现的呢?
音乐播放的逻辑是结合MusicService
还有Playback
层实现的,具体可以参考一下我这篇博客
如果为了增强阅读连贯性的读者可以看看我的总结,迅速上手:
还有关于handlePlayRequest和MusicService这几个类的联系:
播放的控制是由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,调用playMedia()
private void playMedia() {
MediaControllerCompat controller = MediaControllerCompat.getMediaController(getActivity());
if (controller != null) {
controller.getTransportControls().play();
}
}
出乎意料的简洁有咩有!
我们知道MediaController
是根据MediaSession.Token
创建的,并且绑定在了context上,所以调用play()
就可以控制播放,就会fire MediaSession中的回调,不信请看:
由于在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();
}
....
}
最终还是调用handlexxxRequest()
同样的,跳到下一首:
@Override
public void onSkipToQueueItem(long queueId) {
LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId);
mQueueManager.setCurrentQueueItem(queueId);
mQueueManager.updateMetadata();
}
最终都会调用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> result) {
LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId);
if (MEDIA_ID_EMPTY_ROOT.equals(parentMediaId)) {
result.sendResult(new ArrayList());
} 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
并且实现两个方法:onGetRoot()
onLoadChildren()
通过onGetRoot()可以返回一个BrowserRoot实例,需要检查其他的应用有没有权限获取这个我们的应用中的数据,
onLoadChildren()
使用了一种特殊的机制返回返回值,这个函数的返回值是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 构造函数 (MediaDescription,int flags)
new MediaBrowserCompat.SubscriptionCallback() {
@Override
public void onChildrenLoaded(@NonNull String parentId,
@NonNull List 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中添加内容,所以我们才可以浏览