We learned about the advantages and disadvantages of ExoPlayer and its basic usage. Today we’ll get into the audio player practice of ExoPlayer. We’ll implement a simple audio player.

directory

  1. Media playback framework MediaSession
  2. MediaSession framework +ExoPlayer simple music player practice
  • Play online music
  • Play/Pause
  • Switch song
  • Times the speed of playback
  1. data
  2. harvest

MediaSession is a framework for media playback

Audio players don’t always need to make their UI visible. Once the audio starts playing, the player can run as a background task. Users can switch to another app and continue listening. To implement this design in Android, you can build an audio application using two components: an activity (for presentation) and a player Service. If the user switches to another application, the service can run in the background. By splitting the two parts of an audio application into separate components, each component can run independently. Uis are typically short lived compared to players and can run for a long time without a UI.

In the design of music player APP architecture, there are several common practice scheme one

  1. Register Service for data setting, music control, custom player status values and callback interface for flow control
  2. Through radio, AIDL and other implementation of the page layer logic communication, so that the user can control the music through the interface to play, pause, switch, seek and other operations
  3. Use RemoteControlClient (lower version) or MediaSession (>5.0 or MediaSessionCompat) for multi-device or cross-app media sessions

The MediaSession framework (Supprot MediaSessionCompat) is designed to solve the problem of interface and Service communication during media playback. It is designed to do a good job in terms of low coupling structure

The support library provides two classes to implement this client/server method: MediaBrowserService and MediaBrowser. The service component is implemented as a subclass of The MediaBrowserService that contains the media session and its player. Activities using the UI and media controller should include a MediaBrowser that communicates with the MediaBrowserService. Using MediaBrowserService makes it easy for portable devices (such as Android Auto and Wear) to discover your app, connect to it, browse content, and control playback without having to access your Activity

Our learning practice today is based on the framework of Solution 2 for MediaSession

Image from Media Application Architecture Overview

MediaBrowser is used to connect to the MediaBrowserService and subscribe to data. Its callbacks allow it to retrieve the status of the connection to the Service and to retrieve music data asynchronously from the Service. Because there is concrete business logic involved)

MediaBrowserService is a Service that encapsulates media-related functions. The return value of onGetRoot determines whether to allow clients to connect. The onLoadChildren callback asynchronously retrieves data from the Sercive to MediaBrowser. It also includes media player instances (such as ExoPlayer, which we practice here)

The MediaSession is created in the onCreate of the MediaBrowserService. The MediaSession.CallBack receives commands from the MediaController and triggers actions related to the player

MediaController The creation of MediaContoller requires a pairing token for MediaSession, which is created after MediaBrowser successfully connects to the service. MediaController can either actively send instructions or passively receive MediaController.Callback to change the play state and refresh the interface.

For a more detailed introduction, please refer to the official documentation or analysis and practice of Android media playback framework MediaSession

Second, simple practice

Let’s see how we can use the MediaSession framework to implement simple audio playback

2.1 Server implementation

First we inherit MediaBrowserServiceCompat implementation and registration Service

public class MusicService extends MediaBrowserServiceCompat { private static final String TAG = "MusicService"; private SimpleExoPlayer exoPlayer; private MediaSessionCompat mediaSession; /** * When the service receives the onCreate () lifecycle callback method, it should perform the following steps: Create and initialize media Session * 2. Set media Session callback * 3. Public void onCreate() {log.i (TAG, "onCreate: "); super.onCreate(); //1. Create and initialize MediaSession = New MediaSessionCompat(getApplicationContext(), TAG); mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder() .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SEEK_TO) .build(); mediaSession.setPlaybackState(playbackState); MediaSession. SetCallback (new MyMediaSessionCallBack()); //2. / / 3. Set the mediaSessionToken setSessionToken (mediaSession getSessionToken ()); // Create a player instance exoPlayer = new SimpleExoPlayer.Builder(getApplicationContext()).build(); }}Copy the code

MediaSessionCompat. Callback callbacks for receiving business into. Through the mediaController getTransportControls play related operations (play, pause, seek, etc.) times the speed of the Callback

/** * to receive changes triggered by MediaControl, Internal packaging players and play state change * / private class MyMediaSessionCallBack extends MediaSessionCompat. Callback {@ Override public void onPlay() { super.onPlay(); Log.i(TAG, "onPlay: "); exoPlayer.play(); } @Override public void onPause() { super.onPause(); Log.i(TAG, "onPause: "); exoPlayer.pause(); } @Override public void onSeekTo(long pos) { super.onSeekTo(pos); Log.i(TAG, "onSeekTo: pos=" + pos); exoPlayer.seekTo(pos); }... }Copy the code

MediaBrowserServiceCompat onGetRoot and onLoadChildren has two callback method. OnGetRoot is used to tell MediaBrowser whether the connection is successful. OnLoadChildren loads audio and video data. The specific use is as follows:

@Nullable @Override public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) { Log.i(TAG, "onGetRoot: clientPackageName=" + clientPackageName + " clientUid=" + clientUid + " pid=" + Binder.getCallingPid() + " uid=" + Binder.getCallingUid()); Return new BrowserRoot("media_root_id", null); } @override public void onLoadChildren(@nonnull String parentId, @override public void onLoadChildren(@nonnull String parentId, @override) @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) { Log.i(TAG, "onLoadChildren: parentId=" + parentId); List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>(); if (TextUtils.equals("media_root_id", parentId)) { } ArrayList<MusicEntity> musicEntityList = getMusicEntityList(); for (int i = 0; i < musicEntityList.size(); i++) { MusicEntity musicEntity = musicEntityList.get(i); MediaMetadataCompat metadataCompat = buildMediaMetadata(musicEntity); if (i == 0) { mediaSession.setMetadata(metadataCompat); } mediaItems.add(new MediaBrowserCompat.MediaItem(metadataCompat.getDescription(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)); exoPlayer.addMediaItem(MediaItem.fromUri(musicEntity.source)); } // IllegalStateException: sendResult() called when either sendResult() or sendError() had already been called for: Media_root_id // Result result. SendResult (mediaItems); Log.i(TAG, "onLoadChildren: addMediaItem"); initExoPlayerListener(); exoPlayer.prepare(); Log.i(TAG, "onLoadChildren: prepare"); } private void initExoPlayerListener() { exoPlayer.addListener(new Player.EventListener() { @Override public void onPlaybackStateChanged(int state) { long currentPosition = exoPlayer.getCurrentPosition(); long duration = exoPlayer.getDuration(); // State change (a callback for a state change within the player, including // 1. User triggered such as: manually cut songs, pause, play, seek and so on; // 2. How to notify the UI service layer?? Set log. I (TAG, "onPlaybackStateChanged: ") currentPosition=" + currentPosition + " duration=" + duration + " state=" + state); int playbackState; switch (state) { default: case Player.STATE_IDLE: playbackState = PlaybackStateCompat.STATE_NONE; break; case Player.STATE_BUFFERING: playbackState = PlaybackStateCompat.STATE_BUFFERING; break; case Player.STATE_READY: if(exoPlayer.getPlayWhenReady()){ playbackState = PlaybackStateCompat.STATE_PLAYING; }else { playbackState = PlaybackStateCompat.STATE_PAUSED; } break; case Player.STATE_ENDED: playbackState = PlaybackStateCompat.STATE_STOPPED; break; } / / the state of the player, told the UI by mediasession MediaControllerCompat registered business layer. The Callback to Callback setPlaybackState (playbackState); } private void setPlaybackState(int playbackState) { float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed; mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build()); } @NotNull private ArrayList<MusicEntity> getMusicEntityList() { ArrayList<MusicEntity> list = new ArrayList<MusicEntity>(); . MusicEntity musicEntity2 = new MusicEntity(); musicEntity2.id = "wake_up_02"; musicEntity2.title = "Geisha"; musicEntity2.album = "Wake Up"; musicEntity2.artist = "Media Right Productions"; musicEntity2.genre = "Electronic"; musicEntity2.source = "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/02_-_Geisha.mp3"; musicEntity2.image = "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg"; musicEntity2.trackNumber = 2; musicEntity2.totalTrackCount = 13; musicEntity2.duration = 267; musicEntity2.site = "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"; list.add(musicEntity2); return list; }Copy the code

2.2 Client Implementation

Now let’s look at the implementation of the Client

public class ExoSimpleAudioPlayerActivity extends Activity implements View.OnClickListener { private MediaBrowserCompat mediaBrowser; private MediaBrowserCompat.ConnectionCallback mConnectionCallbacks = new MyConnectionCallback(); private MediaControllerCompat.Callback mMediaControllerCallback; private MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_simple_audio); . //mConnectionCallbacks is a callback for c-S connection mediaBrowser = new MediaBrowserCompat(this, new ComponentName(this, MusicService.class), mConnectionCallbacks, null); } @Override protected void onStart() { super.onStart(); Log.i(TAG, "onStart: "); OnConnected MediabrowSer.connect (); // Make a C-S connection request to create MusicService. If the onGetRoot callback value is not null, the connection is established. // subscribe(); } @Override protected void onStop() { super.onStop(); Log.i(TAG, "onStop: "); mediaBrowser.disconnect(); }}Copy the code

MediaBrowserCompat. ConnectionCallback used to receive callback connected to the Server side

public class MyConnectionCallback extends MediaBrowserCompat.ConnectionCallback { @Override public void onConnected() { super.onConnected(); Log.i(TAG, "onConnected: MyConnectionCallback"); / / MediaBrowser and MediaBrowerService after establishing a connection will callback MediaSessionCompat. This method Token sessionToken = MediaBrowser. GetSessionToken (); / / after the connection is established to create MediaController MediaController = new MediaControllerCompat (ExoSimpleAudioPlayerActivity. This, sessionToken); MediaControllerCompat.setMediaController(ExoSimpleAudioPlayerActivity.this, mediaController); subscribe(); //MediaController sends the command buildTransportControls(); If (mMediaControllerCallback == null) {if (mMediaControllerCallback == null) {// This callback is the Controller's callback. // If the playback ends and the song is automatically cut, the callback cannot be received. MMediaControllerCallback = new MediaControllerCompat. Callback () {/ / Callback here, only the user will have corresponding Callback. //ExoPlayer getDuration: https://stackoverflow.com/questions/35298125/exoplayer-getduration // Override public void onPlaybackStateChanged(PlaybackStateCompat state) { super.onPlaybackStateChanged(state); Log.i(TAG, "onPlaybackStateChanged: state=" + state.getState()); If (PlaybackStateCompat. STATE_PLAYING = = state. GetState ()) {playButton. SetText (" pause "); } else {playbutton.settext (" play "); } updatePlaybackState(state); MediaMetadataCompat metadata = mediaController.getMetadata(); updateDuration(metadata); } @Override public void onMetadataChanged(MediaMetadataCompat metadata) { super.onMetadataChanged(metadata); durationSet = false; Log.i(TAG, "onMetadataChanged: metadata=" + metadata.toString()); updateDuration(metadata); } } mediaController.registerCallback(mMediaControllerCallback); PlaybackStateCompat state = mediaController.getPlaybackState(); updatePlaybackState(state); updateProgress(); if (state ! = null && (state.getState() == PlaybackStateCompat.STATE_PLAYING || state.getState() == PlaybackStateCompat.STATE_BUFFERING)) { scheduleSeekbarUpdate(); } / / MediaMetadataCompat obtained through mediaController MediaMetadataCompat metadata = mediaController. For getMetadata (); updateDuration(metadata); } @Override public void onConnectionFailed() { super.onConnectionFailed(); }}Copy the code

2.3 Basic Functions

Songs play play pause When users click on the play/pause button, gets the current state of play, by mediaController. GetTransportControls to by Binder to mediaSession, In service MediaSessionCompat. Callback change Exoplayer state of play, Exoplayer onPlaybackStateChanged received state change notification, the trigger, To set mediasession mediasession setPlaybackState

The corresponding key codes are as follows:

Client end user click event handling / / ExoSimpleAudioPlayerActivity Java PlaybackStateCompat playbackState = mediaController.getPlaybackState(); int state = playbackState.getState(); Log.i(TAG, "onClick: state=" + state); / / by mediaController. GetTransportControls trigger MediaSessionCompat. Callback Callback - "playback controls the if (state = = PlaybackStateCompat.STATE_PLAYING) { mediaController.getTransportControls().pause(); } else { mediaController.getTransportControls().play(); }Copy the code

/ / the Server end MediasessionCallback implementation, receiving mediaController getTransportControls () event

//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack

       @Override
        public void onPlay() {
            super.onPlay();

            Log.i(TAG, "onPlay: ");
            exoPlayer.play();
        }

        @Override
        public void onPause() {
            super.onPause();

            Log.i(TAG, "onPause: ");
            exoPlayer.pause();
        }
Copy the code

// Listen for the status changes of the ExoPlayer on the server

//com.example.myplayer.audio.MusicService#initExoPlayerListener exoPlayer.addListener(new Player.EventListener() { @Override public void onPlaybackStateChanged(int state) { long currentPosition = exoPlayer.getCurrentPosition(); long duration = exoPlayer.getDuration(); // State change (a callback for a state change within the player, including // 1. User triggered such as: manually cut songs, pause, play, seek and so on; // 2. How to notify the UI service layer?? Set log. I (TAG, "onPlaybackStateChanged: ") currentPosition=" + currentPosition + " duration=" + duration + " state=" + state); int playbackState; switch (state) { default: case Player.STATE_IDLE: playbackState = PlaybackStateCompat.STATE_NONE; break; case Player.STATE_BUFFERING: playbackState = PlaybackStateCompat.STATE_BUFFERING; break; case Player.STATE_READY: if(exoPlayer.getPlayWhenReady()){ playbackState = PlaybackStateCompat.STATE_PLAYING; }else { playbackState = PlaybackStateCompat.STATE_PAUSED; } break; case Player.STATE_ENDED: playbackState = PlaybackStateCompat.STATE_STOPPED; break; } / / the state of the player, told the UI by mediasession MediaControllerCompat registered business layer. The Callback to Callback setPlaybackState (playbackState); } } private void setPlaybackState(int playbackState) { float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed; mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build()); }Copy the code

Although I know how to use it, what is the whole process? It uses the knowledge of Handler and Binder’s thread and process communication. In the future, we will study the practice in depth separately. Here we first draw the flow chart of play/pause along the process, from the user presses the button to the player starts to play and the page updates.

The switch between the previous and the next song is basically the same,

//com.example.myplayer.audio.ExoSimpleAudioPlayerActivity#onClick if (id == R.id.prev) { if (mediaController ! = null) { mediaController.getTransportControls().skipToPrevious(); } } else if (id == R.id.next) { if (mediaController ! = null) { mediaController.getTransportControls().skipToNext(); }}Copy the code

The difference is that there is no playback in the ExoPlayer. You need to call the Next /prev of the ExoPlayer in the sessionCallback to switch songs and set the new playstate to the mession

//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack
 
     @Override
        public void onSkipToNext() {
            super.onSkipToNext();
            Log.i(TAG, "onSkipToNext: ");
            exoPlayer.next();
            exoPlayer.setPlayWhenReady(true);
            setPlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_NEXT);
    mediaSession.setMetadata(getMediaMetadata(1));
        }

        @Override
        public void onSkipToPrevious() {
            super.onSkipToPrevious();
            Log.i(TAG, "onSkipToPrevious: ");
            exoPlayer.previous();
            exoPlayer.setPlayWhenReady(true);
            setPlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS);
    mediaSession.setMetadata(getMediaMetadata(0));

        }
Copy the code

Finally, the onPlaybackStateChanged of MediaControllerCallback receives a callback, depending on the state

public void onPlaybackStateChanged(PlaybackStateCompat state) { super.onPlaybackStateChanged(state); . if (state.getState() == PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS || state.getState() == PlaybackStateCompat.STATE_SKIPPING_TO_NEXT) { updateShowMediaInfo(description); } } private void updateShowMediaInfo(MediaDescriptionCompat description) { if (description == null) return; titleView.setText(description.getTitle()); artistView.setText(description.getSubtitle()); Glide.with(ExoSimpleAudioPlayerActivity.this).load(description.getIconUri().toString()).into(iconView); Uri mediaUri = description.getMediaUri(); Uri iconUri = description.getIconUri(); Log.i(TAG, "onChildrenLoaded: title=" + description.getTitle() + " subtitle=" + description.getSubtitle() + " mediaUri=" + mediaUri + " iconUri=" + iconUri); }Copy the code

Times the speed

//com.example.myplayer.audio.ExoSimpleAudioPlayerActivity#onClick if (id == R.id.speed) { if (mediaController ! = null) { float speed = getSpeed(); Speedview.settext (" double speed "+ speed); mediaController.getTransportControls().setPlaybackSpeed(speed); }} float[] speedArray = new float[]{0.5f, 1f, 1.5f, 2f}; int curSpeedIndex = 1; private float getSpeed() { if (curSpeedIndex > 3) { curSpeedIndex = 0; } return speedArray[curSpeedIndex++]; }Copy the code

Then implement the onSetPlaybackSpeed callback in the MediaSessionCallBack to set the play speed and mession

//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack @Override public void onSetPlaybackSpeed(float speed) {  super.onSetPlaybackSpeed(speed); Log.i(TAG, "onSetPlaybackSpeed: speed=" + speed); PlaybackParameters playParams = new PlaybackParameters(speed); exoPlayer.setPlaybackParameters(playParams); / / reset mediaSession setPlaybackState told listeners speed change setPlaybackState (exoPlayer. GetPlaybackState ()); } private void setPlaybackState(int playbackState) { float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed; mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build()); }Copy the code

Note the differences and associations between the MediaSession framework and ExoPlayer

//android.support.v4.media.session.PlaybackStateCompat
TATE_NONE, STATE_STOPPED, STATE_PAUSED, STATE_PLAYING, STATE_FAST_FORWARDING,
            STATE_REWINDING, STATE_BUFFERING, STATE_ERROR, STATE_CONNECTING,
            STATE_SKIPPING_TO_PREVIOUS, STATE_SKIPPING_TO_NEXT, STATE_SKIPPING_TO_QUEUE_ITEM
Copy the code

//com.google.android.exoplayer2.Player.State

STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED
Copy the code

2.4 Existing Problems

There are some problems with the above practice, such as how the data interacts. We see that the Activity communicates directly with the Service through various callbacks in the MediaSession framework. The player ExoPlayer is encapsulated in the Service, and the data is retrieved in the Service. This is obviously not a real scene. In addition, there is no separation of playback management, playback queue maintenance, playback state management and so on there is no unified management, is not conducive to the expansion of the replacement of players.

In the next article we will examine the implementation of UMAP, how it is structured, and how it addresses the above issues.

The full code has been uploaded to github github.com/ayyb1988/me…

Third, information

ExoPlayer

  1. Learn and use ExoPlayer for Android development (audio)
  2. Media streaming with ExoPlayer
  3. ExoPlayer blog
  4. ExoPlayer developer guide
  5. Easy Audio Focus with ExoPlayer

UAMP related

  1. UniversalMusicPlayer (Play Control Layer)
  2. Analysis and Practice of Android Media player Framework MediaSession
  3. Android Media (part 1)
  4. Overview of audio applications
  5. Creating Music Playback based on MediaSessionCompat (part 1)
  6. Creating Music Playback based on MediaSessionCompat (part 2)

Audio player related open source project

  1. uamp
  2. Audio visualizer- Audio visualizer- Android
  3. ListenerMusicPlayer
  4. Music-Player
  5. Timber
  6. Music-Cover-View

other

  1. Android is disabled and the method of open four components (setComponentEnabledSetting)
  2. Android Notification Channel

Internet interface and song source

From the official Google uamp open source projects at http://storage.googleapis.com/automotive-media/music.json https://storage.googleapis.com/uamp/catalog.json Music provided by the [Free Music Archive](http://freemusicarchive.org/). - [Irsen's Tale](http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/) by [Kai Engel](http://freemusicarchive.org/music/Kai_Engel/). - [Wake Up](http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/) by [The Kyoto Connection](http://freemusicarchive.org/music/The_Kyoto_Connection/). Long audio: https://v.typlog.com/oohomechat/8385162738_706123.mp3Copy the code

Four, harvest

Through this study and practice,

  1. Learn about MediaSession, the media playing framework
  2. Implement simple audio player (play/pause, cut songs, double speed) using MediaSession framework
  3. Understanding the principles, practices, and process analysis, we have a basic understanding of the framework of MediaSession and the simplicity and utility of ExoPlayer. But the following functions of an audio player are also basic functions: edge cache change playback, playback queue, fade in and out, audio focus, background playback, how to better achieve it? In the concrete practice before we first learn to analyze the UAMP this Google open source audio player is how the architecture, to see whether in the data source Settings and playback management can learn from.

Thank you for reading

In the next article, we will continue to learn and practice ExoPlayer and analyze the design and implementation of UAMP. Welcome to follow the public account “Audio and Video Development Journey” and learn and grow together. Welcome to communicate