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