MusicLibrary-一個豐富的音訊播放SDK。

L_Xian發表於2019-03-03

MusicLibrary-一個豐富的音訊播放SDK。

GitHub地址:github.com/lizixian18/…

在日常開發中,如果專案中需要新增音訊播放功能,是一件很麻煩的事情。一般需要處理的事情大概有音訊服務的封裝,播放器的封裝,通知欄管理,聯動系統媒體中心,音訊焦點的獲取,播放列表維護,各種API方法的編寫等等...如果完善一點,還需要用到IPC去實現。 可見需要處理的事情非常多。

所以 MusicLibrary 就這樣編寫出來了,它的目標是幫你全部實現好所以音訊相關的事情,讓你可以專注於其他事情。

MusicLibrary 能做什麼:

  1. 基於 IPC 實現音訊服務,減少app的記憶體峰值,避免OOM。
  2. 整合和呼叫 API 非常簡單,幾乎一句話就可以把音訊功能整合好了。
  3. 提供了豐富的 API 方法,輕鬆實現各種功能。
  4. 一句話整合通知欄,可以自定義對通知欄的控制。
  5. 內部整合了兩個播放器,ExoPlayer 和 MediaPlayer,預設使用 ExoPlayer,可隨意切換。
  6. 還有其他等等...

NiceMusic - 一個 MusicLibrary 的實際應用 App 例子

為體現 MusicLibrary 在實際上的應用,編寫了一個簡單的音樂播放器 NiceMusic。

GitHub地址:github.com/lizixian18/…

MusicLibrary 的基本用法,可以參考這個專案中的實現。
在 NiceMusic 中,你可以學到下面的東西:

  1. 一種比較好的 MVP 結構封裝,結合了RxJava,生命週期跟 Activity 繫結,而且通過註解的方式例項化 Presenter ,比較解耦。
  2. Retrofit 框架的封裝以及如何用攔截器去給所有介面新增公共引數和頭資訊等。
  3. 其他等等...

放上幾張截圖:
MusicLibrary-一個豐富的音訊播放SDK。 MusicLibrary-一個豐富的音訊播放SDK。 MusicLibrary-一個豐富的音訊播放SDK。 MusicLibrary-一個豐富的音訊播放SDK。MusicLibrary-一個豐富的音訊播放SDK。

MusicLibrary 關鍵類的結構圖以及程式碼分析:

MusicLibrary-一個豐富的音訊播放SDK。

關於 IPC 和 AIDL 等用法和原理不再講,如果不瞭解請自己查閱資料。
可以看到,PlayControl其實是一個Binder,連線著客戶端和服務端。

QueueManager

QueueManager 是播放列表管理類,裡面維護著當前的播放列表和當前的音訊索引。
播放列表儲存在一個 ArrayList 裡面,音訊索引預設是 0:

public QueueManager(MetadataUpdateListener listener, PlayMode playMode) {
    mPlayingQueue = Collections.synchronizedList(new ArrayList<SongInfo>());
    mCurrentIndex = 0;
    ...
}
複製程式碼

當呼叫設定播放列表相關API的時候,實際上是呼叫了裡面的setCurrentQueue方法,每次播放列表都會先清空,再賦值:

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);
    //通知播放列表更新了
    List<MediaSessionCompat.QueueItem> queueItems = QueueHelper.getQueueItems(mPlayingQueue);
    if (mListener != null) {
        mListener.onQueueUpdated(queueItems, mPlayingQueue);
    }
}
複製程式碼

當播放列表更新後,會把播放列表封裝成一個 QueueItem 列表回撥給 MediaSessionManager 做鎖屏的時候媒體相關的操作。

得到當前播放音樂,播放指定音樂等操作實際上是操作音訊索引mCurrentIndex,然後根據索引取出列表中對應的音訊資訊。

上一首下一首等操作,實際上是呼叫了skipQueuePosition方法,這個方法中採用了取餘的演算法來計算上一首下一首的索引,
而不是加一或者減一,這樣的一個好處是避免了陣列越界或者說計算更方便:

public boolean skipQueuePosition(int amount) {
    int index = mCurrentIndex + amount;
    if (index < 0) {
        // 在第一首歌曲之前向後跳,讓你在第一首歌曲上
        index = 0;
    } else {
        //當在最後一首歌時點下一首將返回第一首個
        index %= mPlayingQueue.size();
    }
    if (!QueueHelper.isIndexPlayable(index, mPlayingQueue)) {
        return false;
    }
    mCurrentIndex = index;
    return true;
}
複製程式碼

引數 amount 是維度的意思,可以看到,傳 1 則會取下一首,傳 -1 則會取上一首,事實上可以取到任何一首音訊,
只要維度不一樣就可以。

播放音樂時,先是呼叫了setCurrentQueueIndex方法設定好音訊索引後再通過回撥交給PlaybackManager去做真正的播放處理。

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);
        }
    }
}
複製程式碼

QueueManager 需要說明的感覺就這些,其他如果有興趣可以clone程式碼後再具體細看。

PlaybackManager

PlaybackManager 是播放管理類,負責操作播放,暫停等播放控制操作。
它實現了 Playback.Callback 介面,而 Playback 是定義了播放器相關操作的介面。
具體的播放器 ExoPlayer、MediaPlayer 的實現均實現了 Playback 介面,而 PlaybackManager 則是通過 Playback
來統一管理播放器的相關操作。 所以,如果想再新增一個播放器,只需要實現 Playback 介面即可。

播放:

public void handlePlayRequest() {
    SongInfo currentMusic = mQueueManager.getCurrentMusic();
    if (currentMusic != null) {
        String mediaId = currentMusic.getSongId();
        boolean mediaHasChanged = !TextUtils.equals(mediaId, mCurrentMediaId);
        if (mediaHasChanged) {
            mCurrentMediaId = mediaId;
            notifyPlaybackSwitch(currentMusic);
        }
        //播放
        mPlayback.play(currentMusic);
        //更新媒體資訊
        mQueueManager.updateMetadata();
        updatePlaybackState(null);
    }
}
複製程式碼

播放方法有幾個步驟:

  1. 取出要播放的音訊資訊。
  2. 根據音訊 id 的對比來判斷是否回撥切歌方法。如果 id 不一樣,則代表需要切歌。
  3. 然後再呼叫mPlayback.play(currentMusic)交給具體播放器去播放。
  4. 然後更新媒體操作的音訊資訊(就是鎖屏時的播放器)。
  5. 回撥播放狀態狀態。

暫停:

public void handlePauseRequest() {
    if (mPlayback.isPlaying()) {
        mPlayback.pause();
        updatePlaybackState(null);
    }
}
複製程式碼

暫停是直接交給具體播放器去暫停,然後回撥播放狀態狀態。

停止:

public void handleStopRequest(String withError) {
    mPlayback.stop(true);
    updatePlaybackState(withError);
}
複製程式碼

停止也是同樣道理。

基本上PlaybackManager裡面的操作都是圍繞著這三個方法進行,其他則是一些封裝和回撥的處理。
具體的播放器實現參考的是Google的官方例子 android-UniversalMusicPlayer 這專案真的非常不錯。

MediaSessionManager

這個類主要是管理媒體資訊MediaSessionCompat,他的寫法是比較固定的,可以參考這篇文章中的聯動系統媒體中心 的介紹
也可以參考 Google的官方例子

MediaNotificationManager

這個類是封裝了通知欄的相關操作。自定義通知欄的情況可算是非常複雜了,遠不止是 new 一個 Notification。(可能我還是菜鳥)

通知欄的分類

NotificationCompat.Builder 裡面 setContentView 的方法一共有兩個,一個是 setCustomContentView()
一個是 setCustomBigContentView() 可知道區別就是大小的區別吧,對應的 RemoteView 也是兩個:RemoteView 和 BigRemoteView

而不同的手機,有的通知欄背景是白色的,有的是透明或者黑色的(如魅族,小米等),這時候你就需要根據不同的背景顯示不同的樣式(除非你在佈局裡面寫死背景色,但是那樣真的很醜)
所以通知欄總共需要的佈局有四個:

  1. 白色背景下 ContentView
  2. 白色背景下 BigContentView
  3. 黑色背景下 ContentView
  4. 黑色背景下 BigContentView

設定 ContentView 如下所示:

...
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;
    }
}
...
複製程式碼

在配置通知欄的時候,最重要的就是如何獲取到對應的資原始檔和佈局裡面相關的控制元件,是通過 Resources#getIdentifier 方法去獲取:

private Resources res;
private String packageName;

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

private int getResourceId(String name, String className) {
    return res.getIdentifier(name, className, packageName);
}
複製程式碼

因為需要能動態配置,所以對通知欄的相關資源和id等命名就需要制定好約定了。比如我要獲取
白色背景下ContentView的佈局檔案賦值給RemoteView:

RemoteViews remoteView = new RemoteViews(packageName, 
                                         getResourceId("view_notify_light_play", "layout"));
複製程式碼

只要你的佈局檔案命名為 view_notify_light_play.xml 就能正確獲取了。
所以不同的佈局和不同的資源獲取全部都是通過 getResourceId 方法獲取。

更新通知欄UI

更新UI分為下面3個步驟:

  1. 重新新建 RemoteView 替換舊的 RemoteView
  2. 將新的 RemoteView 賦值給 Notification.contentView 和 Notification.bigContentView
  3. 更新 RemoteView 的 UI
  4. 呼叫 NotificationManager.notify(NOTIFICATION_ID, mNotification); 去重新整理。

更新開始播放的時候播放/暫停按鈕UI:

public void updateViewStateAtStart() {
    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);
        }
    }
}
複製程式碼
通知欄點選事件

點選事件通過的就是 RemoteView.setOnClickPendingIntent(PendingIntent pendingIntent) 方法去實現的。
如果能夠動態配置,關鍵就是配置 PendingIntent 就可以了。
思路就是:如果外部有傳PendingIntent進來,就用傳進來的PendingIntent,否則就用預設的PendingIntent

private PendingIntent startOrPauseIntent;

public MediaNotificationManager(){
    setStartOrPausePendingIntent(creater.getStartOrPauseIntent());  
}
 
private RemoteViews createRemoteViews(){
    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);
}
複製程式碼

可以看到,完整程式碼如上所示,當 creater.getStartOrPauseIntent() 不為空時,就用 creater.getStartOrPauseIntent()
否則用預設的。

希望大家喜歡! ^_^

相關文章