MusicLibrary- a rich audio player SDK.

GitHub: github.com/lizixian18/…

In daily development, if the project needs to add audio playback function, is a very troublesome thing. Generally need to deal with things like audio service encapsulation, player encapsulation, notification bar management, interactive system media center, audio focus acquisition, playlist maintenance, various API method writing and so on… If perfect, also need to use IPC to achieve. So there’s a lot to deal with.

So MusicLibrary was written, and its goal is to help you do all the audio stuff so you can focus on other things.

What MusicLibrary can do:

  1. Realize audio service based on IPC, reduce app memory peak, avoid OOM.
  2. Integrating and calling the API is so simple that you can integrate the audio functionality in almost one sentence.
  3. Provides rich API methods, easy to achieve a variety of functions.
  4. One sentence integrated notification bar, you can customize the control of the notification bar.
  5. There are two integrated players, ExoPlayer and MediaPlayer. The default is ExoPlayer. You can switch between ExoPlayer and MediaPlayer.
  6. And so on…

NiceMusic – a practical application for MusicLibrary

To reflect the actual application of MusicLibrary, I wrote a simple music player NiceMusic.

GitHub: github.com/lizixian18/…

For basic usage of MusicLibrary, refer to the implementation in this project. In NiceMusic, you can learn the following:

  1. A good MVP structure encapsulation, combined with RxJava, life cycle and Activity binding, and using annotations to instantiate Presenter, more decoupled.
  2. The encapsulation of the Retrofit framework and how to use interceptors to add public parameters and headers to all interfaces.
  3. And so on…

Here are some screenshots:

Structure diagram and code analysis of MusicLibrary key classes:

About IPC and AIDL usage and principle no longer, if you do not understand, please refer to the information. As you can see, PlayControl is a Binder that connects clients and servers.

QueueManager

QueueManager is a playlist management class that maintains the current playlist and the current audio index. Playlists are stored in an ArrayList. The audio index defaults to 0:

public QueueManager(MetadataUpdateListener listener, PlayMode playMode) {
    mPlayingQueue = Collections.synchronizedList(new ArrayList<SongInfo>());
    mCurrentIndex = 0; . }Copy the code

When you call the playlist API, you are actually calling the setCurrentQueue method, and each time the playlist is cleared and then assigned:

public void setCurrentQueue(List<SongInfo> newQueue, int currentIndex) {
    int index = 0;
    if(currentIndex ! = -1) {
        index = currentIndex;
    }
    mCurrentIndex = Math.max(index, 0);
    mPlayingQueue.clear();
    mPlayingQueue.addAll(newQueue);
    // Notify that the playlist has been updated
    List<MediaSessionCompat.QueueItem> queueItems = QueueHelper.getQueueItems(mPlayingQueue);
    if(mListener ! =null) { mListener.onQueueUpdated(queueItems, mPlayingQueue); }}Copy the code

When the playlist is updated, the playlist is encapsulated as a QueueItem list that is called back to the MediaSessionManager for media-related lockscreen operations.

To get the currently playing music, to play the specified music, and so on, is to actually manipulate the audio index mCurrentIndex, and then extract the corresponding audio information from the list according to the index.

This method uses the mod algorithm to calculate the index of the previous and next entries instead of adding or subtracting one. One of the benefits of this method is that the array is not out of bounds or is easier to calculate:

public boolean skipQueuePosition(int amount) {
    int index = mCurrentIndex + amount;
    if (index < 0) {
        // Jump back before the first song, leaving you on the first song
        index = 0;
    } else {
        // When the last song is clicked on the next song, the first song is returned
        index %= mPlayingQueue.size();
    }
    if(! QueueHelper.isIndexPlayable(index, mPlayingQueue)) {return false;
    }
    mCurrentIndex = index;
    return true;
}
Copy the code

The amount parameter is the dimension, and you can see that if you pass 1 you’ll take the next one, if you pass -1 you’ll take the last one, and you can actually take any one, as long as the dimensions are different.

When the music is played, the setCurrentQueueIndex method is called to set the audio index and then the callback is given to PlaybackManager to do the actual playback.

private void setCurrentQueueIndex(int index, boolean isJustPlay, boolean isSwitchMusic) {
    if (index >= 0 && index < mPlayingQueue.size()) {
        mCurrentIndex = index;
        if(mListener ! =null) { mListener.onCurrentQueueIndexUpdated(mCurrentIndex, isJustPlay, isSwitchMusic); }}}Copy the code

QueueManager needs to explain these feelings, other if interested can clone the code after a detailed look.

PlaybackManager

PlaybackManager is the playback management class, which is responsible for playing, pausing, and so on. It implements the Playback.Callback interface, and Playback is the interface that defines the operations associated with the player. Specific player implementations such as ExoPlayer and MediaPlayer implement Playback interfaces, while PlaybackManager centrally manages player operations through Playback. So, if you want to add another player, simply implement the Playback interface.

Play:

public void handlePlayRequest(a) {
    SongInfo currentMusic = mQueueManager.getCurrentMusic();
    if(currentMusic ! =null) {
        String mediaId = currentMusic.getSongId();
        booleanmediaHasChanged = ! TextUtils.equals(mediaId, mCurrentMediaId);if (mediaHasChanged) {
            mCurrentMediaId = mediaId;
            notifyPlaybackSwitch(currentMusic);
        }
        / / play
        mPlayback.play(currentMusic);
        // Update media information
        mQueueManager.updateMetadata();
        updatePlaybackState(null); }}Copy the code

The playback method has several steps:

  1. Take out the audio message to play.
  2. According to the audio ID comparison to determine whether the callback song cutting method. If the ids are different, the songs need to be cut.
  3. And then callmPlayback.play(currentMusic)Hand it over to the player.
  4. Then update the audio information for the media operation (that is, the player when the screen is locked).
  5. Callback play status Status.

Suspended:

public void handlePauseRequest(a) {
    if (mPlayback.isPlaying()) {
        mPlayback.pause();
        updatePlaybackState(null); }}Copy the code

Pause is directly handed over to the specific player to pause, and then callback the play state state.

Stop:

public void handleStopRequest(String withError) {
    mPlayback.stop(true);
    updatePlaybackState(withError);
}
Copy the code

The same goes for stopping.

Basically everything in PlaybackManager is done around these three methods, the rest is wrapped up and called back. The specific player implementation is based on Google’s official example android-UniversalMusicPlayer, which is really good.

MediaSessionManager

This class is mainly used to manage MediaSessionCompat. It is written in a fixed way. You can refer to the introduction of linkage system media center in this article or the official example of Google

MediaNotificationManager

This class encapsulates the actions associated with notification bars. Customizing a Notification bar can be quite complicated, and involves much more than just a new Notification. (Maybe I’m still a rookie)

Classification of notification bar

NotificationCompat. Builder inside the setContentView method a total of two, One is setCustomContentView() and the other is setCustomBigContentView(). The difference is the size of the RemoteView and the other is BigRemoteView

On different phones, some of the notification bar backgrounds are white, some are transparent, some are black (meizu, Mi, etc.), and you need to display different styles according to the background (unless you write the background color in the layout, which is really ugly), so there are four layouts required for the notification bar:

  1. ContentView on a white background
  2. BigContentView on white background
  3. ContentView against a black background
  4. BigContentView on a black background

Set the ContentView as follows:

.if (Build.VERSION.SDK_INT >= 24) {
    notificationBuilder.setCustomContentView(mRemoteView);
    if(mBigRemoteView ! =null) { notificationBuilder.setCustomBigContentView(mBigRemoteView); }}... Notification notification;if (Build.VERSION.SDK_INT >= 16) {
    notification = notificationBuilder.build();
} else {
    notification = notificationBuilder.getNotification();
}
if (Build.VERSION.SDK_INT < 24) {
    notification.contentView = mRemoteView;
    if (Build.VERSION.SDK_INT >= 16&& mBigRemoteView ! =null) { notification.bigContentView = mBigRemoteView; }}...Copy the code

When configuring the notification bar, the most important thing is how to obtain the corresponding resource file and layout control, is to use the Resources#getIdentifier method to obtain:

private Resources res;
private String packageName;

public MediaNotificationManager(a){
     packageName = mService.getApplicationContext().getPackageName();
     res = mService.getApplicationContext().getResources();
}

private int getResourceId(String name, String className) {
    return res.getIdentifier(name, className, packageName);
}
Copy the code

Because it needs to be configured dynamically, you need to have a convention for naming the resources and ids associated with the notification bar. Let’s say I want to get the layout file of the ContentView on a white background and assign it to RemoteView:

RemoteViews remoteView = new RemoteViews(packageName, 
                                         getResourceId("view_notify_light_play"."layout"));
Copy the code

As long as your layout file is named view_notify_light_play.xml it will get it right. So different layouts and different resource retrievals are all retrievable using the getResourceId method.

Update notification bar UI

Updating the UI is divided into the following three steps:

  1. Create a new RemoteView to replace the old RemoteView
  2. The new RemoteView assigned to Notification. ContentView and Notification. BigContentView
  3. Update the RemoteView UI
  4. Call the NotificationManager. Notify (NOTIFICATION_ID mNotification); To refresh.

Update the play/pause button UI when playing:

public void updateViewStateAtStart(a) {
    if(mNotification ! =null) {
        boolean isDark = NotificationColorUtils.isDarkNotificationBar(mService);
        mRemoteView = createRemoteViews(isDark, false);
        mBigRemoteView = createRemoteViews(isDark, true);
        if (Build.VERSION.SDK_INT >= 16) {
            mNotification.bigContentView = mBigRemoteView;
        }
        mNotification.contentView = mRemoteView;
        if(mRemoteView ! =null) {
            mRemoteView.setImageViewResource(getResourceId(ID_IMG_NOTIFY_PLAY_OR_PAUSE, "id"),
                    getResourceId(isDark ? DRAWABLE_NOTIFY_BTN_DARK_PAUSE_SELECTOR :
                            DRAWABLE_NOTIFY_BTN_LIGHT_PAUSE_SELECTOR, "drawable"));
            
            if(mBigRemoteView ! =null) {
                mBigRemoteView.setImageViewResource(getResourceId(ID_IMG_NOTIFY_PLAY_OR_PAUSE, "id"),
                        getResourceId(isDark ? DRAWABLE_NOTIFY_BTN_DARK_PAUSE_SELECTOR :
                                DRAWABLE_NOTIFY_BTN_LIGHT_PAUSE_SELECTOR, "drawable")); } mNotificationManager.notify(NOTIFICATION_ID, mNotification); }}}Copy the code
Notification bar click event

Click event is through RemoteView. SetOnClickPendingIntent (PendingIntent PendingIntent) method to implement. If dynamic configuration is possible, the key is to configure PendingIntent. If a PendingIntent is passed in, use the intent. Otherwise, use the default intent.

private PendingIntent startOrPauseIntent;

public MediaNotificationManager(a){
    setStartOrPausePendingIntent(creater.getStartOrPauseIntent());  
}
 
private RemoteViews createRemoteViews(a){
    if(startOrPauseIntent ! =null) {
         remoteView.setOnClickPendingIntent(getResourceId(ID_IMG_NOTIFY_PLAY_OR_PAUSE, "id"), startOrPauseIntent); }}private void setStartOrPausePendingIntent(PendingIntent pendingIntent) {
    startOrPauseIntent = pendingIntent == null ? getPendingIntent(ACTION_PLAY_PAUSE) : pendingIntent;
}

private PendingIntent getPendingIntent(String action) {
    Intent intent = new Intent(action);
    intent.setClass(mService, PlayerReceiver.class);
    return PendingIntent.getBroadcast(mService, 0, intent, 0);
}
Copy the code

As you can see, the complete code as shown above, when the creater. GetStartOrPauseIntent () is not null, use creater. GetStartOrPauseIntent () or use the default.

I hope you enjoy it! ^_^