Copyright: AnliaLee/BauzMusic Copyright: AnliaLee/BauzMusic
preface
Recently, I have been busy studying and researching music player. I found that MediaSession framework has very little information, but more source code and open source libraries. It is not very friendly for beginners and may be confusing when looking at it
Googlesamples /android-UniversalMusicPlayer Media Apps Overview
MediaSession Framework Introduction
Let’s take a look at how to design the architecture of a music player App. The traditional approach is as follows:
- Register a Service for asynchronously retrieving music library data, music control, etc. In a Service we may also need to define some state values and callback interfaces for flow control
- The communication between the Activity and the Service is realized through broadcasting (other methods, such as interface and Messenger), so that users can control music playing, pausing, dragging the progress bar and other operations through the interface components
If our music player also needs to support the notification bar to quickly control music playback, then we need to add a set of radio and corresponding interface to respond to the notification bar button events
If you need to support multiple controls (TV, watch, headset, etc.) to control the same player, the entire system architecture can become very complex, and we need to spend a lot of time and effort to design and optimize the structure of the code. So what are some ways to save all this work, increase our productivity, and still gracefully implement all of these features?
Google added MediaSession framework in Android 5.0 (in support-V4 also provides corresponding compatible package, related classes end with Compat, Api is basically the same), specially used to solve the problem of interface and Service communication during media playback. The process is intended to standardize these functions. Using this framework we can reduce some process complex development work, such as the use of various broadcasts to control the player, and its code readability, structure coupling degree is very well controlled, so I recommend you to try this framework. Let’s start by introducing the core members of the MediaSession framework and its usage process
Use of MediaSession framework
Overview of common member classes
There are four commonly used member classes in the MediaSession framework that are the core of the overall process control
-
The MediaBrowser MediaBrowser is used to connect MediaBrowserService to subscription data. Its callback interface allows us to retrieve the connection status of the Service and the music library data asynchronously retrieved from the Service. Media browsers are usually created on the client side (which can be understood as the interface that each terminal is responsible for controlling music playback)
-
MediaBrowserService provides onGetRoot and onLoadChildren which are called when the media browser sends a data subscription to the Service. The MediaBrowserService also acts as a container for media players (MediaPlayer, ExoPlayer, etc.) and MediaSession
-
MediaSession media session, that is, the controlled end, by setting the MediaSessionCompat. Receiving media controller Callback callbacks MediaController send instructions, When a command is received, the corresponding Callback method of each command in Callback will be triggered (the Callback method will perform the corresponding operations of the player, such as play, pause, etc.). The Session is typically created in the service. onCreate method, and then the setSessionToken method is called to set the token used to pair with the controller and notify the browser that the connection to the Service is successful
-
MediaController media controller, developers can use the controller not only in the client sends commands to the Service of the controlled end, can also by setting the MediaControllerCompat. Callback Callback method receives is controlled the condition, Thus refresh the UI according to the corresponding state. The creation of MediaController requires the pairing token of the controlled side, so the creation is performed in the callback of the browser successfully connecting to the service
From the above introduction, we can see that there is a very clear division of labor and scope between the four members, which makes the whole code structure clear and easy to read. The relationship between them can be briefly summarized in the following diagram
In addition, MediaSession has other classes that are equally important. For example, PlaybackState, which encapsulates various playback states, and MediaMetadata, which stores media information through key and value pairs similar to Map, MediaItem for data interaction between MediaBrowser and MediaBrowserService, etc. Let’s see how this framework works by implementing a simple demo
Build a simple music player using the MediaSession framework
For example, our demo looks like this (see below), which only provides a simple play pause operation, and the music source is retrieved from the RAW resource folder
Following the workflow, let’s start by getting the music library data. First of all, add a RecyclerView at the top of the interface to display the acquired music list. We completed some initialization operations of RecyclerView in DemoActivity
public class DemoActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private List<MediaBrowserCompat.MediaItem> list;
private DemoAdapter demoAdapter;
private LinearLayoutManager layoutManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
list = new ArrayList<>();
layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
demoAdapter = new DemoAdapter(this,list); recyclerView = (RecyclerView) findViewById(R.id.recyclerView); recyclerView.setLayoutManager(layoutManager); recyclerView.setAdapter(demoAdapter); }}Copy the code
Pay attention to the type of List elements to MediaBrowserCompat MediaItem, because MediaBrowser every song music can be retrieved from the service encapsulation into MediaItem object. Next we create a MediaBrowser and perform the connection between the server and the subscription data
public class DemoActivity extends AppCompatActivity {...private MediaBrowserCompat mBrowser;
@Override
protected void onCreate(Bundle savedInstanceState) {... mBrowser =new MediaBrowserCompat(
this.new ComponentName(this, MusicService.class),// Bind browser services
BrowserConnectionCallback,// Set the connection callback
null
);
}
@Override
protected void onStart(a) {
super.onStart();
//Browser sends connection requests
mBrowser.connect();
}
@Override
protected void onStop(a) {
super.onStop();
mBrowser.disconnect();
}
/** * The onConnected() method is called when the connection is successful
private MediaBrowserCompat.ConnectionCallback BrowserConnectionCallback =
new MediaBrowserCompat.ConnectionCallback(){
@Override
public void onConnected(a) {
Log.e(TAG,"onConnected------");
// The subscription operation must be performed only if the connection is successful
if (mBrowser.isConnected()) {
/ / mediaId is MediaBrowserService onGetRoot return values
// If the Service allows clients to connect, the result is not null. The value is the root ID of the data content hierarchy
// If the connection is rejected, null is returned
String mediaId = mBrowser.getRoot();
// The Browser requests data from the Service via subscription. To initiate a subscription request, two parameters are required, one of which is mediaId
// If the mediaId is already subscribed by other Browser instances, the subscriber of mediaId needs to be unsubscribed before subscribing
// While subscribing to an already subscribed mediaId replaces Browser's subscription callback, the onChildrenLoaded callback cannot be triggered
// Ps: Although the basic concept is like this, Google has a comment in the official demo...
// This is temporary: A bug is being fixed that will make subscribe
// consistently call onChildrenLoaded initially, no matter if it is replacing an existing
// subscriber or not. Currently this only happens if the mediaID has no previous
// subscriber or if the media content changes on the service side, so we need to
// unsubscribe first.
// The onChildrenLoaded callback is triggered when a subscription request is sent
// So we need to unsubscribe before we can make a subscription request anyway
mBrowser.unsubscribe(mediaId);
// the subscription method takes one more parameter, which sets the SubscriptionCallback SubscriptionCallback
/ / as a Service to get the data after the data sent back, at this time will trigger SubscriptionCallback. OnChildrenLoaded callbackmBrowser.subscribe(mediaId, BrowserSubscriptionCallback); }}@Override
public void onConnectionFailed(a) {
Log.e(TAG,"Connection failed!"); }};/** * Callback interface to initiate data subscription requests to MediaBrowserService */
private final MediaBrowserCompat.SubscriptionCallback BrowserSubscriptionCallback =
new MediaBrowserCompat.SubscriptionCallback(){
@Override
public void onChildrenLoaded(@NonNull String parentId, @NonNull List
children)
{
Log.e(TAG,"onChildrenLoaded------");
// Children is the media data set sent back by the Service
for (MediaBrowserCompat.MediaItem item:children){
Log.e(TAG,item.getDescription().getTitle().toString());
list.add(item);
}
// Refresh the list UI in onChildrenLoadeddemoAdapter.notifyDataSetChanged(); }}; }Copy the code
From the above code and comments, it should be clear how MediaBrowser goes from connecting to a service to subscribing to it, which is a quick summary
Connect → onConnected → Subscribe → onChildrenLoadedCopy the code
What does the Service side do in this process? First we have to create the MusicService class by inheriting MediaBrowserService (which uses the support-V4 package’s classes here). MediaBrowserService inherits from Service, so remember to configure it in Androidmanifest.xml
<service
android:name=".demo.MusicService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
Copy the code
We need to complete the construction of MediaSession when the Service is initialized, and set up the corresponding flags, states, etc. The specific code is as follows
public class MusicService extends MediaBrowserServiceCompat {
private MediaSessionCompat mSession;
private PlaybackStateCompat mPlaybackState;
@Override
public void onCreate(a) {
super.onCreate();
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_NONE,0.1.0 f)
.build();
mSession = new MediaSessionCompat(this."MusicService");
mSession.setCallback(SessionCallback);// Set the callback
mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mSession.setPlaybackState(mPlaybackState);
/ / set the token after triggers MediaBrowserCompat. ConnectionCallback callback methods
// The connection between MediaBrowser and MediaBrowserService is successfulsetSessionToken(mSession.getSessionToken()); }}Copy the code
To explain some details, MediaSession. SetFlag is called to set the flag bit for the Session so that the Session can receive the command from the controller. And then the state of play Settings, need to call MediaSession. SetPlaybackState, then PlaybackState? We briefly introduced that PlaybackState is a class that encapsulates various states of play. We can control the behavior of each member by judging the current states of play. The PlaybackState class defines the specifications of various states for us. We also need to set up SessionCallback callback methods that are triggered when the client sends instructions using the controller to control the player
public class MusicService extends MediaBrowserServiceCompat {...private MediaPlayer mMediaPlayer;
@Override
public void onCreate(a) {... mMediaPlayer =new MediaPlayer();
mMediaPlayer.setOnPreparedListener(PreparedListener);
mMediaPlayer.setOnCompletionListener(CompletionListener);
}
/** * responds to the controller instruction callback */
private android.support.v4.media.session.MediaSessionCompat.Callback SessionCallback = new MediaSessionCompat.Callback(){
/ * * * response MediaController getTransportControls () play * /
@Override
public void onPlay(a) {
Log.e(TAG,"onPlay");
if(mPlaybackState.getState() == PlaybackStateCompat.STATE_PAUSED){
mMediaPlayer.start();
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING,0.1.0 f) .build(); mSession.setPlaybackState(mPlaybackState); }}/ * * * response MediaController getTransportControls () onPause * /
@Override
public void onPause(a) {
Log.e(TAG,"onPause");
if(mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING){
mMediaPlayer.pause();
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED,0.1.0 f) .build(); mSession.setPlaybackState(mPlaybackState); }}/ * * * response MediaController getTransportControls () playFromUri *@param uri
* @param extras
*/
@Override
public void onPlayFromUri(Uri uri, Bundle extras) {
Log.e(TAG,"onPlayFromUri");
try {
switch (mPlaybackState.getState()){
case PlaybackStateCompat.STATE_PLAYING:
case PlaybackStateCompat.STATE_PAUSED:
case PlaybackStateCompat.STATE_NONE:
mMediaPlayer.reset();
mMediaPlayer.setDataSource(MusicService.this,uri);
mMediaPlayer.prepare();// Prepare to synchronize
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_CONNECTING,0.1.0 f)
.build();
mSession.setPlaybackState(mPlaybackState);
// We can save the current music information, so that the client can refresh the UI
mSession.setMetadata(new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE,extras.getString("title"))
.build()
);
break; }}catch(IOException e){ e.printStackTrace(); }}@Override
public void onPlayFromSearch(String query, Bundle extras) {}};/** * Listen to mediaplayer.prepare () */
private MediaPlayer.OnPreparedListener PreparedListener = new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
mMediaPlayer.start();
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING,0.1.0 f) .build(); mSession.setPlaybackState(mPlaybackState); }};/** * listen for the event that ends playback */
private MediaPlayer.OnCompletionListener CompletionListener = new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
mPlaybackState = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_NONE,0.1.0 f) .build(); mSession.setPlaybackState(mPlaybackState); mMediaPlayer.reset(); }}; }Copy the code
MediaSessionCompat. There are also many Callback method in the Callback, you can cover can be rewritten as needed
After build good MediaSession remember call pairing setSessionToken save Session token, and invoke this method also can callback MediaBrowser. ConnectionCallback onConnected method, Tell the client that the Browser successfully connected to BrowserService, and MediaSession is created and initialized
We also talked about the Browser’s subscription relationship with BrowserService. In MediaBrowserService we need to override the onGetRoot and onLoadChildren methods
public class MusicService extends MediaBrowserServiceCompat {
@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
Log.e(TAG,"onGetRoot-----------");
return new BrowserRoot(MEDIA_ID_ROOT, null);
}
@Override
public void onLoadChildren(@NonNull String parentId, @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
Log.e(TAG,"onLoadChildren--------");
// Remove the information from the current thread, allowing subsequent calls to the sendResult method
result.detach();
// We simulate the process of retrieving data, which should be read asynchronously from the network or locally
MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ""+R.raw.jinglebells)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, "The Christmas Song")
.build();
ArrayList<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
mediaItems.add(createMediaItem(metadata));
// Send data to Browser
result.sendResult(mediaItems);
}
private MediaBrowserCompat.MediaItem createMediaItem(MediaMetadataCompat metadata){
return newMediaBrowserCompat.MediaItem( metadata.getDescription(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE ); }}Copy the code
Finally, back to the client side, the four members are left with the controller MediaController. The creation of MediaController relies on the Session’s pairing token, which Browser gets when it connects to BrowserService. Controller has been created, we can through the MediaController getTransportControls send broadcast instruction method, at the same time can also register MediaControllerCompat. Callback callbacks receiving state, to refresh the UI interface
public class DemoActivity extends AppCompatActivity {...private Button btnPlay;
private TextView textTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {... btnPlay = (Button) findViewById(R.id.btn_play); textTitle = (TextView) findViewById(R.id.text_title); }public void clickEvent(View view) {
switch (view.getId()) {
case R.id.btn_play:
if(mController! =null){
handlerPlayEvent();
}
break; }}/** * handles the play button event */
private void handlerPlayEvent(a){
switch (mController.getPlaybackState().getState()){
case PlaybackStateCompat.STATE_PLAYING:
mController.getTransportControls().pause();
break;
case PlaybackStateCompat.STATE_PAUSED:
mController.getTransportControls().play();
break;
default:
mController.getTransportControls().playFromSearch("".null);
break; }}/** * The onConnected() method is called when the connection is successful
private MediaBrowserCompat.ConnectionCallback BrowserConnectionCallback =
new MediaBrowserCompat.ConnectionCallback(){
@Override
public void onConnected(a) {
Log.e(TAG,"onConnected------");
if (mBrowser.isConnected()) {
...
try{
mController = new MediaControllerCompat(DemoActivity.this,mBrowser.getSessionToken());
// Register a callback
mController.registerCallback(ControllerCallback);
}catch(RemoteException e){ e.printStackTrace(); }}}@Override
public void onConnectionFailed(a) {
Log.e(TAG,"Connection failed!"); }};/** * The media controller controls the callback interface during playback, which can be used to update the UI according to the playback status */
private final MediaControllerCompat.Callback ControllerCallback =
new MediaControllerCompat.Callback() {
/*** * Music playback state change callback *@param state
*/
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
switch (state.getState()){
case PlaybackStateCompat.STATE_NONE:// There is no state
textTitle.setText("");
btnPlay.setText("Start");
break;
case PlaybackStateCompat.STATE_PAUSED:
btnPlay.setText("Start");
break;
case PlaybackStateCompat.STATE_PLAYING:
btnPlay.setText("Pause");
break; }}/** * Play the music change callback *@param metadata
*/
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) { textTitle.setText(metadata.getDescription().getTitle()); }};private Uri rawToUri(int id){
String uriStr = "android.resource://" + getPackageName() + "/" + id;
returnUri.parse(uriStr); }}Copy the code
We have analyzed the basic usage of MediaSession framework. We will analyze the source code of Google official demo UniversalMusicPlayer later. See how the play progress bar, play queue control, quick actions on the notification bar and so on are combined with the MediaSession framework
update
The UniversalMusicPlayer source code analysis has been updated:
- UniversalMusicPlayer for Android