android音視訊指南-MediaPlayer概述

DamonRen發表於2018-11-01

翻譯自MediaPlayer overview

Android多媒體框架支援播放各種常見媒體型別,因此您可以輕鬆地將音訊、視訊和影象整合到應用程式中。您可以使用MediaPlayer api從儲存在應用程式資源(原始資源)中的媒體檔案、檔案系統中的獨立檔案或通過網路連線到達的資料流中播放音訊或視訊。

本文向您展示瞭如何編寫與使用者和系統互動的媒體播放應用程式,以獲得良好的效能和愉快的使用者體驗。

注意:您只能將音訊資料回放到標準輸出裝置。目前,這是移動裝置揚聲器或藍芽耳機。您不能在通話期間播放通話音訊中的聲音檔案。

最基本的

在Android框架中使用以下類播放聲音和視訊:

MediaPlayer

這個類是播放聲音和視訊的主要API。
複製程式碼

AudioManager

該類管理裝置上的音訊源和音訊輸出。
複製程式碼

清單宣告

在使用MediaPlayer對應用程式進行開發之前,請確保清單中有適當的宣告,允許使用相關特性。

  • Internet許可權——如果您正在使用MediaPlayer來播放流基於網路的內容,那麼您的應用程式必須請求網路訪問。
<uses-permission android:name="android.permission.INTERNET" />
複製程式碼
<uses-permission android:name="android.permission.WAKE_LOCK" />
複製程式碼

使用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”資源是系統不嘗試以任何特定方式解析的檔案。然而,這個資源的內容不應該是原始音訊。它應該是一個以支援的格式之一適當編碼和格式化的媒體檔案。

下面是您如何從系統中本地可用的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,因為您引用的檔案可能不存在。

非同步的準備

使用MediaPlayer原則上很簡單。但是,需要記住的是,要將它正確地整合到典型的Android應用程式中,還需要做一些其他的事情。例如,prepare()的呼叫可能需要很長時間執行,因為它可能涉及到獲取和解碼媒體資料。因此,就像任何需要很長時間才能執行的方法一樣,永遠不要從應用程式的UI執行緒呼叫它。這樣做會導致UI掛起,直到方法返回,這是一種非常糟糕的使用者體驗,並可能導致ANR(應用程式沒有響應)錯誤。即使您希望您的資源能夠快速載入,也要記住,在UI中任何需要超過十分之一秒才能響應的內容都會引起明顯的暫停,並給使用者留下您的應用程式很慢的印象。

為了避免掛起UI執行緒,生成另一個執行緒來準備MediaPlayer,並在完成時通知主執行緒。然而,雖然您可以自己編寫執行緒邏輯,但是在使用MediaPlayer時,這種模式非常常見,因此框架提供了一種方便的方法來通過使用prepareAsync()方法來完成此任務。該方法開始在後臺準備媒體並立即返回。當媒體完成準備工作時,將呼叫 通過setOnPreparedListener()配置的MediaPlayer.OnPreparedListener()的onPrepared()方法。

管理狀態

您應該記住的MediaPlayer的另一個方面是,它是基於狀態的。也就是說,MediaPlayer有一個內部狀態,在編寫程式碼時必須始終注意到,因為只有當player處於特定狀態時,某些操作才有效。如果在錯誤的狀態下執行操作,系統可能會丟擲異常或引發其他不希望看到的行為。

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

在編寫與MediaPlayer物件互動的程式碼時,一定要記住狀態圖,因為從錯誤的狀態呼叫其方法是導致錯誤的常見原因。

MediaPlayer Diagram

釋放媒體播放器

MediaPlayer可能會消耗有價值的系統資源。因此,您應該始終採取額外的預防措施,以確保您沒有過多地依賴MediaPlayer例項。處理完它之後,應該始終呼叫release(),以確保分配給它的任何系統資源都被正確釋放。例如,如果您使用的是一個媒體播放器和活動接收onStop()呼叫,您必須釋放媒體播放器,因為當你的活動不與使用者進行互動,繼續持有例項毫無意義(除非你是在後臺播放媒體,這是在下一節中討論)。當您的活動恢復或重新啟動時,當然,您需要建立一個新的MediaPlayer,並在恢復回放之前重新準備。

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

mediaPlayer.release();
mediaPlayer = null;
複製程式碼

作為一個例子,考慮一下如果您在活動停止時忘記釋放MediaPlayer,而在活動重新開始時建立一個新的,可能會發生的問題。正如你可能知道的,當使用者更改螢幕的方向(或更改裝置配置以另一種方式),系統處理,通過重新啟動活動(預設情況下),所以你可能會很快消耗掉所有系統資源的使用者旋轉裝置之間來回的肖像和風景,因為在每一個方向變化,您建立一個新的媒體播放器,你永遠不會釋放。(有關執行時重新啟動的更多資訊,請參見處理執行時更改。)

您可能想知道,在使用者離開您的活動時如果您想繼續播放“背景媒體”,會發生什麼,這與內建音樂應用程式的表現非常類似。在這種情況下,您需要的是一個由服務控制的MediaPlayer,下一節將對此進行討論.

在服務中使用MediaPlayer

如果您想要您的媒體在後臺播放,即使您的應用程式不是在螢幕上——也就是說,您想要它在使用者與其他應用程式互動時繼續播放——那麼您必須啟動一個服務並從那裡控制MediaPlayer例項。您需要將MediaPlayer嵌入到MediaBrowserServiceCompat服務中,並讓它與另一個活動中的MediaBrowserCompat互動。

要小心這個client/server設定。人們對在後臺服務中執行的播放器如何與系統的其他部分進行互動抱有期望。如果您的應用程式沒有滿足這些期望,使用者可能會有一個糟糕的體驗。閱讀建立一個音訊應用程式的完整細節。

本節描述了在服務中實現MediaPlayer時管理它的特殊說明。

非同步執行

首先,與活動一樣,服務中的所有工作在預設情況下都是在單個執行緒中完成的——事實上,如果您從同一個應用程式執行活動和服務,預設情況下它們使用相同的執行緒(“主執行緒”)。因此,服務需要快速處理傳入意圖,並且在響應它們時從不執行冗長的計算。如果預期有任何繁重的工作或阻塞呼叫,您必須非同步執行這些任務:要麼從另一個您自己實現的執行緒執行,要麼使用框架的許多非同步處理工具。

例如,在使用主執行緒中的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();
    }
}
複製程式碼

處理非同步錯誤

在同步操作中,錯誤通常會以異常或錯誤程式碼發出訊號,但無論何時使用非同步資源,都應該確保將錯誤通知給應用程式。對於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將切換到錯誤狀態,您必須在再次使用它之前重置它。

使用‘喚醒鎖 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();
複製程式碼

執行清理

如前所述,MediaPlayer物件可能會消耗大量的系統資源,所以您應該只在需要的時候使用它,並且在使用之後呼叫release()。顯式呼叫這種清理方法而不是依賴於系統垃圾收集是很重要的,因為垃圾收集器重新宣告MediaPlayer可能需要一些時間,因為它只對記憶體需求敏感,而不缺乏其他與媒體相關的資源。因此,在使用服務時,您應該總是重寫onDestroy()方法,以確保釋放MediaPlayer:

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

   @Override
   public void onDestroy() {
       super.onDestroy()
       if (mMediaPlayer != null) mMediaPlayer.release();
   }
}
複製程式碼

數碼版權管理(DRM)

從Android 8.0 (API級別26)開始,MediaPlayer就包含了支援drm保護材料回放的API。它們類似於MediaDrm提供的低階API,但是它們在更高階別上操作,並且不公開底層提取器、drm和加密物件。

儘管MediaPlayer DRM API沒有提供MediaDrm的全部功能,但它支援最常見的用例。當前實現可以處理以下內容型別:

  • 受廣泛保護的本地媒體檔案
  • 寬頻保護遠端/流媒體檔案

下面的程式碼片段演示瞭如何在簡單的同步實現中使用新的DRM MediaPlayer方法。

要管理drm控制的媒體,您需要在通常的MediaPlayer呼叫流之外包括新的方法,如下所示:

setDataSource();
setOnDrmConfigHelper(); // optional, for custom configuration
prepare();
if (getDrmInfo() != null) {
  prepareDrm();
  getKeyRequest();
  provideKeyResponse();
}

// MediaPlayer is now ready to use
start();
// ...play/pause/resume...
stop();
releaseDrm();
複製程式碼

像往常一樣,初始化MediaPlayer物件並使用setDataSource()設定其源。然後,要使用DRM,執行以下步驟:

  1. 如果您希望應用程式執行自定義配置,請定義OnDrmConfigHelper介面,並使用setOnDrmConfigHelper()將其附加到播放器上。
  2. 呼叫prepare()。
  3. 呼叫getDrmInfo()。如果源具有DRM內容,該方法將返回一個非空MediaPlayer.DrmInfo價值。

如果MediaPlayer.DrmInfo存在:

  1. 檢查可用uuid的對映並選擇一個。
  2. 通過呼叫prepareDrm()為當前源程式準備DRM配置。
    • 如果您建立並註冊了一個OnDrmConfigHelper回撥,它將在prepareDrm()執行時被呼叫。這允許您在開啟DRM會話之前執行DRM屬性的自定義配置。在呼叫prepareDrm()的執行緒中同步呼叫回撥函式。要訪問DRM屬性,可以呼叫getDrmPropertyString()和setDrmPropertyString()。避免執行冗長的操作。
    • 如果裝置還沒有提供好,那麼prepareDrm()也會訪問配置伺服器來提供裝置。這可能需要可變的時間,這取決於網路連線。
  3. 要將不透明的鍵請求位元組陣列傳送到許可證伺服器,請呼叫getKeyRequest()
  4. 要將從許可證伺服器接收到的金鑰響應通知DRM引擎,請呼叫provideKeyResponse()。結果取決於鍵請求的型別:
    • 如果響應是離線鍵請求,則結果是鍵集識別符號。您可以通過restoreKeys()使用這個鍵集識別符號將鍵恢復到新會話。
    • 如果響應是流請求或釋出請求,則結果為空。

非同步執行prepareDrm()

預設情況下,prepareDrm()同步執行,阻塞直到準備工作完成。但是,在新裝置上進行的第一次DRM準備也可能需要進行準備,準備工作由prepareDrm()內部處理,由於涉及網路操作,可能需要一些時間才能完成。通過定義和設定MediaPlayer.OnDrmPreparedListener,可以避免在prepareDrm()上阻塞。

當您設定OnDrmPreparedListener時,prepareDrm()在後臺執行請求(如果需要)和準備。當drm準備就緒時,將呼叫偵聽器。您不應該對呼叫序列或偵聽器執行的執行緒做任何假設(除非偵聽器註冊到handler thread)。偵聽器在prepareDrm()返回之前或之後能被呼叫。

非同步設定DRM

您可以非同步地初始化DRM,通過建立和註冊MediaPlayer.OnDrmInfoListener用於DRM準備和MediaPlayer.OnDrmPreparedListener去啟動播放器。它們與prepareAsync()協同工作,如下所示:

setOnPreparedListener();
setOnDrmInfoListener();
setDataSource();
prepareAsync();
// ...

// If the data source content is protected you receive a call to the onDrmInfo() callback.
onDrmInfo() {
  prepareDrm();
  getKeyRequest();
  provideKeyResponse();
}

// When prepareAsync() finishes, you receive a call to the onPrepared() callback.
// If there is a DRM, onDrmInfo() sets it up before executing this callback,
// so you can start the player.
onPrepared() {

start();
}
複製程式碼

處理加密媒體

從Android 8.0 (API級別26)開始,MediaPlayer還可以為H.264和AAC的基本流型別解密通用加密方案(CENC)和HLS取樣級加密媒體(METHOD=SAMPLE-AES)。以前支援全段加密媒體(METHOD=AES-128)。

從ContentResolver檢索媒體

在媒體播放器應用程式中可能有用的另一個特性是檢索本地音樂。你可以通過查詢外部媒體的ContentResolver來實現:

ContentResolver contentResolver = getContentResolver();
Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
Cursor cursor = contentResolver.query(uri, null, null, null, null);
if (cursor == null) {
    // query failed, handle error.
} else if (!cursor.moveToFirst()) {
    // no media on the device
} else {
    int titleColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE);
    int idColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media._ID);
    do {
       long thisId = cursor.getLong(idColumn);
       String thisTitle = cursor.getString(titleColumn);
       // ...process entry...
    } while (cursor.moveToNext());
}
複製程式碼

在MediaPlayer中使用

long id = /* retrieve it from somewhere */;
Uri contentUri = ContentUris.withAppendedId(
        android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);

mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setDataSource(getApplicationContext(), contentUri);

// ...prepare and start...
複製程式碼

Sample

SimpleMediaPlayer程式碼示例展示瞭如何構建獨立播放器。android-BasicMediaDecoderandroid-DeviceOwner示例進一步演示了本頁所述api的使用。

瞭解更多

這些頁面涵蓋了有關錄音、儲存和回放音訊和視訊的主題。

支援的媒體格式

MediaRecorder

資料儲存

相關文章