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

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

GitHub地址:github.com/lizixian18/…java

在平常開發中,若是項目中須要添加音頻播放功能,是一件很麻煩的事情。通常須要處理的事情大概有音頻服務的封裝,播放器的封裝,通知欄管理,聯動系統媒體中心,音頻焦點的獲取,播放列表維護,各類API方法的編寫等等...若是完善一點,還須要用到IPC去實現。 可見須要處理的事情很是多。android

因此 MusicLibrary 就這樣編寫出來了,它的目標是幫你所有實現好因此音頻相關的事情,讓你能夠專一於其餘事情。git

MusicLibrary 能作什麼:

  1. 基於 IPC 實現音頻服務,減小app的內存峯值,避免OOM。
  2. 集成和調用 API 很是簡單,幾乎一句話就能夠把音頻功能集成好了。
  3. 提供了豐富的 API 方法,輕鬆實現各類功能。
  4. 一句話集成通知欄,能夠自定義對通知欄的控制。
  5. 內部集成了兩個播放器,ExoPlayer 和 MediaPlayer,默認使用 ExoPlayer,可隨意切換。
  6. 還有其餘等等...

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

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

GitHub地址:github.com/lizixian18/…算法

MusicLibrary 的基本用法,能夠參考這個項目中的實現。
在 NiceMusic 中,你能夠學到下面的東西:數組

  1. 一種比較好的 MVP 結構封裝,結合了RxJava,生命週期跟 Activity 綁定,並且經過註解的方式實例化 Presenter ,比較解耦。
  2. Retrofit 框架的封裝以及如何用攔截器去給全部接口添加公共參數和頭信息等。
  3. 其餘等等...

放上幾張截圖:
markdown

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

關於 IPC 和 AIDL 等用法和原理再也不講,若是不瞭解請本身查閱資料。
能夠看到,PlayControl實際上是一個Binder,鏈接着客戶端和服務端。app

QueueManager

QueueManager 是播放列表管理類,裏面維護着當前的播放列表和當前的音頻索引。
播放列表存儲在一個 ArrayList 裏面,音頻索引默認是 0:框架

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

當調用設置播放列表相關API的時候,其實是調用了裏面的setCurrentQueue方法,每次播放列表都會先清空,再賦值:oop

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()
不然用默認的。

但願你們喜歡! ^_^

相關文章
相關標籤/搜索