前言
之前博客里已經將了MediaPlayer的簡單應用,如何使用MediaPlayer在Android應用中播放音頻。這篇博客在MediaPlayer使用的基礎上,講解一下MediaPlayer的一些高級功能的使用,以及它的狀態轉換。對MediaPlayer還不了解的朋友可以先看看之前那篇博客:Android--MP3播放器MediaPlayer。
本篇博客主要內容如下:
之前講到,使用MediaPlayer播放音頻,主要使用的是start()、pause()、stop()等方法操作MediaPlayer。但是除了開始、暫停、停止等,MediaPlayer還涉及到一些其他的狀態切換,有些狀態是可以雙向轉換的,有些只能單向環形轉換。如果在某狀態下,強行轉換狀態,會應發程序錯誤,例如在Preparing狀態下切換到Started狀態,是准備中強行開始播放,會出錯。下圖是官方文檔上的圖例,可以很清晰的表名MediaPlayer各個狀態的轉換情況。

上圖已經對MediaPlayer的各種狀態轉換有的清晰的介紹,這里不再詳細講解了,只是提一下需要注意的地方:
- Started(開始)/Paused(暫停)到Stopped(停止)是單向轉換,無法再從Stopped直接轉換到Started,需要經歷Prepared重新裝載才可以重新播放。
- Initialized(初始化)狀態需要裝載數據才可以進行start()播放,但是如果使用prepareAsync()方法異步准備,需要等待准備完成再開始播放,這里需要使用一個回調方法:setOnPreparedListener(),它會在異步裝載完成后調用。
- End(結束)狀態是游離在其他狀態之外的,在任何狀態皆可切換,一般在不需要繼續使用MediaPlayer的時候,才會使用release()回收資源。
- Error(錯誤)狀態是游離在其他狀態之外的,只有在MediaPlayer發生錯誤的時候才會轉換。為了保持應用的用戶體驗,一般我們回監聽setOnErrorListener()回調方法,它會在MediaPlayer發生錯誤的時候被回調。
一般使用MediaPlayer播放音頻流,推薦使用一個Service來承載MediaPlayer,而不是直接在Activity里使用。但是Android系統的功耗設計里,為了節約電池消耗,如果設備處於睡眠狀態,系統將試圖降低或者關閉一些沒設備必須的特性,包括CUP和Wifi硬件,然后,如果是一個后台播放音樂的應用,降低CUP可能導致在后台運行的時候干擾音頻的正常播放,關閉Wifi將可能導致網絡音頻流的獲取出現錯誤。
為了確保MediaPlayer的承載的服務在系統睡眠的時候繼續正常運行下去,Android為我們提供了一種喚醒鎖(wake locks)的機制。它可以在系統睡眠的,依然保持鎖定硬件的正常工作。
確保在MediaPlayer運行的時候,哪怕系統睡眠了CUP也能正常運行,需要使用MediaPlayer.setWakeMode()為MediaPlayer設定喚醒鎖。下面是setWakMode()的簽名:
setWakeMode(Context context, int mode)
第一個參數是當前上下文,第二個參數為需要加鎖的狀態,被設定為int類型的常量,定義在PowerManager這個final類中。PowerManager是專門用來管理Android功率消耗的鎖定狀態,與鎖定CUP相關的,有四種,分別設定CUP、屏幕、鍵盤等的各種保持喚醒的狀態,在這里只需要設定為PARTIAL_WAKE_LOCK即可。
1 mediaPlayer = new MediaPlayer(); 2 // 設定CUP鎖定 3 mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
一般對於鎖而言,鎖定了通常需要解鎖,但是這里的喚醒說與MediaPlayer關聯,所以只需要在使用完之后release()釋放MediaPlayer即可,無需顯式的為其解鎖。在使用setWakeMode設定喚醒鎖的時候,還必須為應用賦予相應的權限:
<uses-permission android:name="android.permission.WAKE_LOCK"/>
再來說說如何鎖定wifi硬件在系統睡眠的時候保持正常運行。wifi鎖通過WifiLock進行操作,而WifiLock通過WifiManager進行管理,通過WifiManager.createWifiLock()進行Wifi鎖定。
WifiManager.WifiLock createWifiLock(int lockType, String tag)
這個方法有多個重載,這里介紹的這個,第一個參數設定鎖的狀態,為一個int類型的常量,定義在Context類中,這里的應用場景一般設定為WIFI_MODE_FULL即可。第二個參數為WifiLock的標志,用於確定wifiLock的。
1 wifiLock= ((WifiManager) getSystemService(this.WIFI_SERVICE)) 2 .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock"); 3 wifiLock.acquire();
當然,在應用中把Wifi鎖定之后,還需要在MediaPlayer.release()的時候為wifi硬件解鎖,為避免意外關閉的情況,最好在Android組件的onDestory()里對其進行釋放,釋放Wifi鎖使用WifiLock.release()。
1 /** 2 * 停止播放 3 */ 4 protected void stop() { 5 if (mediaPlayer != null && mediaPlayer.isPlaying()) { 6 mediaPlayer.stop(); 7 mediaPlayer.release(); 8 mediaPlayer = null; 9 // 釋放wifi鎖 10 wifiLock.acquire(); 11 btn_play.setEnabled(true); 12 Toast.makeText(this, "停止播放", 0).show(); 13 } 14 15 } 16 17 @Override 18 protected void onDestroy() { 19 // 在activity結束的時候回收資源 20 if (mediaPlayer != null && mediaPlayer.isPlaying()) { 21 mediaPlayer.stop(); 22 mediaPlayer.release(); 23 mediaPlayer = null; 24 // 釋放wifi鎖 25 wifiLock.acquire(); 26 } 27 super.onDestroy(); 28 }
眾所周知,Android是一個多任務的操作系統,所以對於音頻的播放,也許有幾個不同的媒體服務會同時播放,這樣可能導致一個比較雜亂的聲音環境,而錯過一些重要的聲音提醒。在Android2.2之后,Android提供了一種應用協商使用設備音頻輸出的機制,這種機制稱為音頻焦點。
當應用程序需要輸出音頻或通知的時候,需要請求音頻焦點,當請求得到音頻焦點之后,監聽音頻焦點的變換,當音頻焦點變換了,根據返回回來的音頻焦點碼進行相應的處理。音頻焦點的注冊使用音頻管理器的AudioManager.requestAudioFocus()方法設定。它的簽名如下:
int requestAudioFocus(AudioManager.OnAudioFocusChangeListener l, int streamType, int durationHint)
這個方法的返回值是int類型,其含義被定義在AudioManager中以常量表示AUDIOFOCUS_REQUEST_FAILED(獲取音頻焦點成功)、AUDIOFOCUS_REQUEST_GRANTED(獲取音頻焦點失敗)。其中重要的是第一個參數,為音頻焦點變化的回調函數,在其中可以設定如果音頻焦點變換了,當前應用如何管理MediaPlayer,第二個參數為媒體流的類型,第三個參數為持續的狀態。
AudioManager.OnAudioFocusChangeListener為音頻焦點變換的監聽器,其中需要實現一個方法:onAudioFocusChange(int focusChange)在音頻焦點變換的時候回調。它有一個參數,為當前表示音頻焦點對於當前應用的狀態碼,通過這個狀態碼指定對應的操作,有些時候音頻狀態改變了,並不一定需要停止音頻的播放。
focusChange有一下幾種狀態碼:
- AUDIOFOCUS_GAIN:獲得音頻焦點。
- AUDIOFOCUS_LOSS:失去音頻焦點,並且會持續很長時間。這是我們需要停止MediaPlayer的播放。
- AUDIOFOCUS_LOSS_TRANSIENT:失去音頻焦點,但並不會持續很長時間,需要暫停MediaPlayer的播放,等待重新獲得音頻焦點。
- AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:暫時失去音頻焦點,但是無需停止播放,只需降低聲音方法。
1 audioManager = (AudioManager) getSystemService(this.AUDIO_SERVICE); 2 int result = audioManager.requestAudioFocus( 3 new OnAudioFocusChangeListener() { 4 5 @Override 6 public void onAudioFocusChange(int focusChange) { 7 switch (focusChange) { 8 case AudioManager.AUDIOFOCUS_GAIN: 9 // 獲得音頻焦點 10 if (!mediaPlayer.isPlaying()) { 11 mediaPlayer.start(); 12 } 13 // 還原音量 14 mediaPlayer.setVolume(1.0f, 1.0f); 15 break; 16 17 case AudioManager.AUDIOFOCUS_LOSS: 18 // 長久的失去音頻焦點,釋放MediaPlayer 19 if (mediaPlayer.isPlaying()) 20 mediaPlayer.stop(); 21 mediaPlayer.release(); 22 mediaPlayer = null; 23 break; 24 25 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 26 // 展示失去音頻焦點,暫停播放等待重新獲得音頻焦點 27 if (mediaPlayer.isPlaying()) 28 mediaPlayer.pause(); 29 break; 30 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 31 // 失去音頻焦點,無需停止播放,降低聲音即可 32 if (mediaPlayer.isPlaying()) { 33 mediaPlayer.setVolume(0.1f, 0.1f); 34 } 35 break; 36 } 37 } 38 }, AudioManager.STREAM_MUSIC, 39 AudioManager.AUDIOFOCUS_GAIN);
總結
以上就講解了MediaPlayer的一些高級的內容,在掌握了MediaPlayer的使用之后,開發有關音樂播放類的應用的時候就可以得心應手了。從用戶體驗的方面出發,如果真實開發一款播放器類的軟件,需要監聽AUDIO_BECOMING_NOISY的廣播,它會在音頻輸出源從其他輸出源變換到設備揚聲器的時候發出此廣播,監聽廣播在音頻輸出源改變到設備揚聲器的時候,停止播放,這樣確保在耳機或額外的音頻輸出硬件與設備斷開連接的時候,不至於重新從揚聲器繼續輸出音頻播放。

