Android 音頻開發之 MediaPlayer

雖然直播落幕,但 Android 的音、視頻技術仍然倍受關注!react

1、引言

Android 提供了常見的音頻、視頻的編碼、解碼機制。藉助於多媒體類 MediaPlayer 的支持,開發人員能夠很方便地在應用中播放音頻、視頻。只不過使用 MediaPlayer 播放視頻時,沒有提供圖像輸出界面。android

2、MediaPlayer 概述

Android 框架中使用如下類播放音頻和視頻:api

MediaPlayer : 這個類是播放音頻和視頻的主要API網絡

AudioManager : 該類管理設備上的音頻源和音頻輸出app

Android 下對於音頻、視頻的支持均須要使用到 MediaPlayer,它主要用來控制 Android 下播放文件或流的類。MediaPlayer 處於 Android 多媒體包下 「android.media.MediaPlayer」,僅有一個無參的構造函數,雖然僅爲咱們提供了一個無參的構造函數,但爲了開發方便,還爲咱們提供了幾個靜態的 create() 方法用於完成MediaPlayer 初始化的工做。框架

static MediaPlayer create(Context context,int resid):經過音頻資源的 Id 來建立一個 MediaPlayer 實例
static MediaPlayer create(Context context,Uri uri):經過一個音頻資源的 Uri 地址來建立一個 MediaPlayer 實例異步

MediaPlayer 除了經過上面兩個 create() 方法在初始化的時候指定媒體資源,還能夠經過 MediaPlayer.setDataSource() 方法爲初始化後的 MediaPlayer 設置媒體資源,setDataSource() 具備多個重載函數,適用於不一樣的媒體資源來源,如下講解幾個經常使用的,其餘的能夠查閱官方文檔。async

  • void setDataSource(String path):經過一個媒體資源的地址指定 MediaPlayer 的數據源,這裏的 path 能夠是一個本地路徑,也能夠是網絡路徑
  • void setDataSource(Context context,Uri uri):經過一個 Uri 指定 MediaPlayer 的數據源,這裏的 Uri 能夠是網絡路徑或這一個內容提供者的 Uri
  • void setDataSource(FileDescriptor fd):經過一個 FileDescriptor 指定一個 MediaPlayer 的數據源

MediaPlayer 支持的數據源有:本地文件、內部的 Uri(內容提供者)、外部 Uri。ide

3、清單聲明

在使用 MediaPlayer 對應用程序進行開發以前,請確保清單中有適當的聲明,容許使用相關特性。函數

Internet 權限——若是您正在使用 MediaPlayer 來播放流基於網絡的內容,那麼應用程序必須請求網絡訪問。

<uses-permission android:name="android.permission.INTERNET" />

Wake Lock 權限——若是您的播放器應用程序須要阻止屏幕變暗或處理器休眠,或 MediaPlayer.setWakeMode() 方法,必須請求此權限。

<uses-permission android:name="android.permission.WAKE_LOCK" />

4、MediaPlayer 狀態管理

MediaPlayer 狀態機圖

MediaPlayer 類中的文檔顯示了一個完整的狀態機,它闡明瞭哪些方法將 MediaPlayer 從一個狀態移動到另外一個狀態。例如,當您建立一個新的 MediaPlayer 時,它處於空閒狀態。這時,您應該經過調用 setDataSource() 來初始化它,使它處於初始化狀態。以後,您必須使用 prepare() 或 prepareAsync() 方法來準備它。當 MediaPlayer 完成準備工做時,它進入準備狀態,這意味着您能夠調用 start() 來讓它播放媒體。此時,您能夠經過調用 start()、pause() 和 seekTo() 等方法在 start、pause() 和 PlaybackCompleted 狀態之間切換。可是,當您調用 stop() 時,請注意,在從新準備 MediaPlayer 以前,您不能再次調用 start()。

MediaPlayer 是基於狀態的。也就是說,MediaPlayer 有一個內部狀態,與 MediaPlayer 對象交互的代碼時,必定要記住狀態圖,在編寫代碼時必須始終注意到,由於只有當 player 處於特定狀態時,某些操做纔有效。若是在錯誤的狀態下執行操做,系統可能會拋出異常或引起其餘不但願看到的行爲。

5、使用 MediaPlayer 播放音樂

MediaPlayer 實際上是一個封裝的很好的音頻、視頻流媒體操做類,其內部是調用的 native 方法,因此它實際上是有 C++ 實現的。既然是一個流媒體操做類,那麼必然涉及到,播放、暫停、中止等操做,實際上 MediaPlayer 也爲咱們提供了相應的方法來直接操做流媒體。

  • void start():開始或恢復播放
  • void stop():中止播放
  • void pause():暫停播放

經過上面三個方法,只要設定好流媒體數據源,便可在應用中播放流媒體資源,爲了更好的操做流媒體,MediaPlayer 還爲咱們提供了一些其餘的方法,這裏列出一些經常使用的,詳細內容參閱官方文檔。

  • int getDuration():獲取流媒體的總播放時長,單位是毫秒
  • int getCurrentPosition():獲取當前流媒體的播放的位置,單位是毫秒
  • void seekTo(int msec):設置當前MediaPlayer的播放位置,單位是毫秒
  • void setLooping(boolean looping):設置是否循環播放
  • boolean isLooping():判斷是否循環播放
  • boolean isPlaying():判斷是否正在播放
  • void prepare():同步的方式裝載流媒體文件
  • void prepareAsync():異步的方式裝載流媒體文件
  • void release ():回收流媒體資源
  • void setAudioStreamType(int streamtype):設置播放流媒體類型
  • void setWakeMode(Context context, int mode):設置 CPU 喚醒的狀態
  • setNextMediaPlayer(MediaPlayer next):設置當前流媒體播放完畢,下一個播放的 MediaPlayer

大部分方法看方法名就能夠理解,可是有幾個方法須要單獨說明一下。

在使用 MediaPlayer 播放一段流媒體的時候,須要使用 prepare() 或 prepareAsync() 方法把流媒體裝載進 MediaPlayer,才能夠調用 start() 方法播放流媒體。

setAudioStreamType() 方法用於指定播放流媒體的類型,它傳遞的是一個 int 類型的數據,均以常量定義在 AudioManager 類中, 通常咱們播放音頻文件,設置爲AudioManager.STREAM_MUSIC 便可。

除了上面介紹的一些方法外,MediaPlayer 還提供了一些事件的回調函數,這裏介紹幾個經常使用的:

  • setOnCompletionListener(MediaPlayer.OnCompletionListener listener):當流媒體播放完畢的時候回調
  • setOnErrorListener(MediaPlayer.OnErrorListener listener):當播放中發生錯誤的時候回調
  • setOnPreparedListener(MediaPlayer.OnPreparedListener listener):當裝載流媒體完畢的時候回調
  • setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener listener):當使用 seekTo() 設置播放位置的時候回調

6、使用 MediaPlayer

媒體框架最重要的組件之一是 MediaPlayer 類,這個類的對象可使用最少的設置獲取、解碼和播放音頻和視頻。它支持幾種不一樣的媒體來源,如:

  • 本地資源
  • 內部 uri,例如您可能從 contentProvider 得到的 uri
  • 外部 url(流)

有關 Android 支持的媒體格式列表,請參閱支持的媒體格式頁面。

下面是如何播放本地音頻資源(保存在您的應用程序的 res/raw/目錄中):

MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // no need to call prepare(); create() does that for you

在本例中,「raw」 資源是系統不嘗試以任何特定方式解析的文件。然而,這個資源的內容不該該是原始音頻。它應該是一個以支持的格式之一適當編碼和格式化的媒體文件。

6.1 同步方式

下面是您如何從系統中本地可用的 URI(例如,您經過內容解析器得到的 URI)進行播放:

Uri myUri = ....; // initialize Uri here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();

經過 HTTP 流媒體從遠程 URL 播放以下:

String url = "http://........"; // your URL here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // might take long! (for buffering, etc)
mediaPlayer.start();

注意:

  • 若是要經過一個 URL 來傳輸流媒體在線文件,該文件必須可以逐步下載
  • 在使用 setDataSource() 時,您必須捕獲或傳遞 IllegalArgumentException 和 IOException,由於您引用的文件可能不存在

6.2 異步的準備

使用 MediaPlayer 原則上很簡單。可是,須要記住的是,要將它正確地集成到典型的 Android 應用程序中,還須要作一些其餘的事情。例如,prepare() 的調用可能須要很長時間執行,由於它可能涉及到獲取和解碼媒體數據。所以,就像任何須要很長時間才能執行的方法同樣,永遠不要從應用程序的UI線程調用它。這樣作會致使 UI 掛起,直到方法返回,這是一種很是糟糕的用戶體驗,並可能致使 ANR(應用程序沒有響應)錯誤。即便您但願您的資源可以快速加載,也要記住,在 UI 中任何須要超過十分之一秒才能響應的內容都會引發明顯的暫停,並給用戶留下您的應用程序很慢的印象。

爲了不掛起UI線程,生成另外一個線程來準備 MediaPlayer,並在完成時通知主線程。然而,雖然您能夠本身編寫線程邏輯,可是在使用 MediaPlayer 時,這種模式很是常見,所以框架提供了一種方便的方法來經過使用 prepareAsync() 方法來完成此任務。該方法開始在後臺準備媒體並當即返回。當媒體完成準備工做時,將調用 經過 setOnPreparedListener() 配置的 MediaPlayer.OnPreparedListener() 的 onPrepared() 方法。

6.3 釋放媒體播放器

MediaPlayer 可能會消耗有價值的系統資源。所以,您應該始終採起額外的預防措施,以確保您沒有過多地依賴 MediaPlayer 實例。處理完它以後,應該始終調用 release(),以確保分配給它的任何系統資源都被正確釋放。例如,若是您使用的是一個媒體播放器和活動接收 onStop() 調用,您必須釋放媒體播放器,由於當你的活動不與用戶進行交互,繼續持有實例毫無心義(除非你是在後臺播放媒體)。當您的活動恢復或從新啓動時,固然,您須要建立一個新的 MediaPlayer,並在恢復回放以前從新準備。

下面是應該如何釋放並取消 MediaPlayer:

mediaPlayer.release();
mediaPlayer = null;

做爲一個例子,考慮一下若是您在活動中止時忘記釋放 MediaPlayer,而在活動從新開始時建立一個新的,可能會發生的問題。正如你可能知道的,當用戶更改屏幕的方向(或更改設備配置以另外一種方式),系統處理,經過從新啓動活動(默認狀況下),因此你可能會很快消耗掉全部系統資源的用戶旋轉設備之間來回的肖像和風景,由於在每個方向變化,您建立一個新的媒體播放器,你永遠不會釋放。

7、在服務中使用 MediaPlayer

若是您想要您的媒體在後臺播放,即便您的應用程序不是在屏幕上——也就是說,您想要它在用戶與其餘應用程序交互時繼續播放——那麼您必須啓動一個服務並從那裏控制 MediaPlayer 實例。您須要將 MediaPlayer 嵌入到 MediaBrowserServiceCompat 服務中,並讓它與另外一個活動中的 MediaBrowserCompat 交互。

要當心這個 client/server 設置。人們對在後臺服務中運行的播放器如何與系統的其餘部分進行交互抱有指望。若是您的應用程序沒有知足這些指望,用戶可能會有一個糟糕的體驗。閱讀創建一個音頻應用程序的完整細節。

7.1 異步運行

首先,與活動同樣,服務中的全部工做在默認狀況下都是在單個線程中完成的——事實上,若是您從同一個應用程序運行活動和服務,默認狀況下它們使用相同的線程(「主線程」)。所以,服務須要快速處理傳入意圖,而且在響應它們時從不執行冗長的計算。若是預期有任何繁重的工做或阻塞調用,您必須異步執行這些任務:要麼從另外一個您本身實現的線程執行,要麼使用框架的許多異步處理工具。

例如,在使用主線程中的 MediaPlayer 時,應該調用 prepareAsync() 而不是 prepare(),並實現 MediaPlayer.OnPreparedListener 目的是在準備完成後開始播放時獲得通知。例如:

public class MyService extends Service implements MediaPlayer.OnPreparedListener {
    private static final String ACTION_PLAY = "com.example.action.PLAY";
    MediaPlayer mMediaPlayer = null;

    public int onStartCommand(Intent intent, int flags, int startId) {
        ...
        if (intent.getAction().equals(ACTION_PLAY)) {
            mMediaPlayer = ... // initialize it here
            mMediaPlayer.setOnPreparedListener(this);
            mMediaPlayer.prepareAsync(); // prepare async to not block main thread
        }
    }

    /** Called when MediaPlayer is ready */
    public void onPrepared(MediaPlayer player) {
        player.start();
    }
}

7.2 處理異步錯誤

在同步操做中,錯誤一般會以異常或錯誤代碼發出信號,但不管什麼時候使用異步資源,都應該確保將錯誤通知給應用程序。對於 MediaPlayer,您能夠經過實現MediaPlayer.OnErrorListener 並將其設置到 MediaPlayer 實例中來解決該問題。

public class MyService extends Service implements MediaPlayer.OnErrorListener {
    MediaPlayer mMediaPlayer;

    public void initMediaPlayer() {
        // ...initialize the MediaPlayer here...
        mMediaPlayer.setOnErrorListener(this);
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        // ... react appropriately ...
        // The MediaPlayer has moved to the Error state, must be reset!
    }
}

要記住,當發生錯誤時,MediaPlayer 將切換到錯誤狀態,必須在再次使用它以前重置它。

7.3 使用‘喚醒鎖 wake locks’

當設計在後臺播放媒體的應用程序時,設備可能會在服務運行時休眠。因爲 Android 系統試圖在設備處於休眠狀態時節省電池,因此係統試圖關閉手機的任何沒必要要的功能,包括 CPU 和WiFi 硬件。然而,若是您的服務正在播放或流媒體音樂,您但願防止系統干擾您的播放。
爲了確保您的服務在這些條件下繼續運行,您必須使用「喚醒鎖」。喚醒鎖是一種向系統發出信號的方式,即您的應用程序正在使用某些特性,即便手機處於空閒狀態,這些特性也應該保持可用。

注意:你應該儘可能少用喚醒鎖,而且只在必要的時候使用它們,由於它們會大大減小設備的電池壽命。

要確保在 MediaPlayer 播放時 CPU 繼續運行,在初始化 MediaPlayer 時調用 setWakeMode() 方法。一旦你這樣作了,MediaPlayer 會在播放時持有指定的鎖,並在暫停或中止時釋放鎖:

mMediaPlayer = new MediaPlayer(); // ... other initialization here ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

可是,在本例中得到的喚醒鎖只保證 CPU 保持清醒。若是你是經過網絡流媒體,使用的是 Wi-Fi,你可能也想要一個 WifiLock,你必須手動獲取和釋放它。所以,當您開始使用遠程 URL 準備 MediaPlayer 時,您應該建立並得到 Wi-Fi 鎖。例如:

WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
    .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");
wifiLock.acquire();

當你暫停或中止你的媒體,或當你再也不須要網絡,你應該釋放鎖:

wifiLock.release();

7.4 執行清理

如前所述,MediaPlayer 對象可能會消耗大量的系統資源,因此您應該只在須要的時候使用它,而且在使用以後調用 release()。顯式調用這種清理方法而不是依賴於系統垃圾收集是很重要的,由於垃圾收集器從新聲明 MediaPlayer 可能須要一些時間,由於它只對內存需求敏感,而不缺少其餘與媒體相關的資源。所以,在使用服務時,您應該老是重寫 onDestroy() 方法,以確保釋放 MediaPlayer:

public class MyService extends Service {
   MediaPlayer mMediaPlayer;
   // ...

   @Override
   public void onDestroy() {
       super.onDestroy()
       if (mMediaPlayer != null) mMediaPlayer.release();
   }
}

8、MediaPlayer 使用技巧

在使用 MediaPlayer 的使用過程當中,有些小技巧須要說明一下:

8.1 在使用 start() 播放流媒體以前,須要裝載流媒體資源

這裏最好使用 prepareAsync() 用異步的方式裝載流媒體資源。由於流媒體資源的裝載是會消耗系統資源的,在一些硬件不理想的設備上,若是使用 prepare() 同步的方式裝載資源,可能會形成 UI 界面的卡頓,這是很是影響用於體驗的。由於推薦使用異步裝載的方式,爲了不尚未裝載完成就調用 start() 而報錯的問題,須要綁定MediaPlayer.setOnPreparedListener() 事件,它將在異步裝載完成以後回調。異步裝載還有一個好處就是避免裝載超時引起 ANR((Application Not Responding)錯誤。

mediaPlayer = new MediaPlayer();
    mediaPlayer.setDataSource(path);
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); // 經過異步的方式裝載媒體資源
    mediaPlayer.prepareAsync();
    mediaPlayer.setOnPreparedListener(new OnPreparedListener() { 
         @Override
        publicvoid onPrepared(MediaPlayer mp) {
            mediaPlayer.start();
         }
     });

8.2 使用完 MediaPlayer 須要回收資源

MediaPlayer 是很消耗系統資源的,因此在使用完 MediaPlayer,不要等待系統自動回收,最好是主動回收資源。

if (mediaPlayer != null && mediaPlayer.isPlaying()) {
    mediaPlayer.stop();
    mediaPlayer.release();
    mediaPlayer = null;
}

8.3 使用 MediaPlayer 最好使用一個Service來使用

實際上,就算是直接使用 Activity 承載 MediaPlayer,也最好在銷燬的時候判斷一下 MediaPlayer 是否被回收,若是未被回收,回收其資源,由於底層調用的 native 方法,若是不銷燬仍是會在底層繼續播放,而承載的組件已經被銷燬了,這個時候就沒法獲取到這個 MediaPlayer 進而控制它。

@Override
protectedvoid onDestroy() {
    if (mediaPlayer != null && mediaPlayer.isPlaying()) {
        mediaPlayer.stop();
        mediaPlayer.release();
        mediaPlayer = null;
    }

    super.onDestroy();
}

8.4 單曲循環之類的操做

對於單曲循環之類的操做,除了可使用 setLooping() 方法進行設置以外,還能夠爲 MediaPlayer 註冊回調函數,MediaPlayer.setOnCompletionListener(),它會在 MediaPlayer 播放完畢被回調。

// 設置循環播放
// mediaPlayer.setLooping(true);
mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
     @Override
    publicvoid onCompletion(MediaPlayer mp) {
        // 在播放完畢被回調
        play();
    }
});

8.5 流媒體播放異常處理

由於 MediaPlayer 一直操做的是一個流媒體,因此無可避免的可能一段流媒體資源,前半段能夠正常播放,而中間一段由於解析或者源文件錯誤等問題,形成中間一段沒法播放問題,須要咱們處理這個錯誤,不然會影響 UX(用戶體驗)。能夠爲 MediaPlayer 註冊回調函數 setOnErrorListener() 來設置出錯以後的解決辦法,通常從新播放或者播放下一個流媒體便可。

mediaPlayer.setOnErrorListener(new OnErrorListener() {
    @Override
    publicboolean onError(MediaPlayer mp, int what, int extra) {
        play();
        returnfalse;
    }
});

9、總結

Android 多媒體框架支持播放各類常見媒體類型,所以您能夠輕鬆地將音頻、視頻和圖像集成到應用程序中。您可使用 MediaPlayer api 從存儲在應用程序資源(原始資源)中的媒體文件、文件系統中的獨立文件或經過網絡鏈接到達的數據流中播放音頻或視頻。

10、關於我

更多信息能夠點擊關於我 , 很是但願和你們一塊兒交流 , 共同進步

相關文章
相關標籤/搜索