MediaPlayer 概括
Android多媒體框架支持多種公共媒體類型的播放,你可以很容易的將音頻,視頻,圖片等多媒體資源整合進你的應用中。
使用MediaPlayer APIs,你可以播放位于應用資源文件,獨立的文件系統的文件或者網絡連接的數據流的視頻或音頻文件。
該文檔展示如何編寫用戶與系統交互的多媒體應用,以獲得良好的性能和愉快的用戶體驗
*注意:目前,你只能在移動設備的揚聲器或藍牙耳機等標準的輸出設備中播放音頻數據。在通話期間,你無法在談話中播放聲音文件
基礎
- MediaPlayer
提供用于播放音視頻的API - AudioManager
提供管理音頻源和輸出
清單聲明
在你的應用中使用MediaPlayer前,確保在你的清單文件中聲明相關feature的使用權
- 網絡權限 - 如果你使用MediaPlayer播放基于網絡的內容,需要聲明一下權限
<uses-permission android:name="android.permission.INTERNET" />
- 睡眠鎖權限 - 如果你的播放應用需要保持屏幕亮度或禁止進程休眠,或需要使用 MediaPlayer.setScreenOnWhilePlaying()和 MediaPlayer.setWakeMode()
兩個API,你需要請求以下權限
<uses-permission android:name="android.permission.WAKE_LOCK" />
MediaPlayer 使用
MediaPlayer是多媒體框架中最重要的組件之一。它可以用最小的設置來獲取,解碼并播放音視頻,支持一下幾種不同的媒體資源
- 本地資源
- 內部URIs,比如從ContentResolver中獲取
- 外部URLs(流)
了解Android支持的媒體格式列表,請訪問格式支持頁面
以下是播放本地本地原生資源(保存在應用res/raw目錄)的例子
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // 無需調用preare()方法,create()方法中已調用
這種方式,原生資源是系統不用特殊方法解析的一種文件。但是,資源不能是原始的音頻資源。他應該是支持的格式中的一種適當的編碼格式。
以下是使用系統本地的URI(比如, Content Resolver)的實例
Uri myUri = ....; // initialize Uri here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();
播放遠程URL比如HTTP 流
String url = "http://........";
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // might take long!
mediaPlayer.start();
- 注意:如果你解析網上的媒體文件URL為流,該文件必須支持漸進式下載
- 注意:setDataSource()需要捕獲或傳遞IllegalArgumentException 和 IOException,因為文件可能不存在
異步準備
原則上,MediaPlayer的使用很簡單。然而,一定要記住在典型的Android應用中正確整合的一些注意事項。比如,prepare()方法的調用會花一些時間,因為它可能涉及獲取和解碼媒體數據,因此,與任何可能需要很長時間才能執行的方法一樣,不能在UI線程調用該方法。這樣會導致UI線程掛起直到該方法返回,
這會導致糟糕的用戶體驗并引發ANR錯誤。即使你期望你的資源加載很快。記住在UI中響應時間超過十分之一秒的任何內容都會導致顯著的卡頓,并給用戶應用運行緩慢的印象。
為了避免UI線程掛起,在另一個線程準備MediaPlayer并當它完成時通知UI線程。但是,雖然你可以書寫自己的線程邏輯,但這種模式在使用MediaPlayer時非常常見,框架提供了另一個方便的方法prepareAsync()去完成這個任務。該方法在后臺準備媒體并且立即返回。當媒體準備完成,通過setOnPreparedListener設置的
MediaPlayer.OnPrepareListener.onPrepared()的方法被調用。
狀態管理
另一方面你應該記住,MediaPlayer是基于狀態的。這是說,MediaPlayer擁有內部狀態,當你寫代碼時,你需要時刻知道當前的狀態,因為特殊的操作僅在播放器在特殊的狀態下有效。當你在錯誤的狀態下執行操作,系統可能會拋出異常或者導致另一些不可取的行為。
MediaPlayer文檔展示了一個完整的狀態圖來說明一些使MediaPlayer狀態轉換的方法。比如,當你創建一個新的MediaPlayer,它處于空閑狀態。之后,你調用prepare()
或prepareAsync()方法準備播放器,當MediaPlayer準備完畢,它進入準備狀態,然后,可以調用start()方法去播放媒體。在這個時候,就如圖所顯示的,你可以調用
start(),pause(),seekTo()包括其他的方法在開始,暫停和播放完畢等狀態中切換。但是,注意你不應該再次調用start()方法知道你再次準備MediaPlayer。
在你編寫與MediaPlayer交互的代碼時要時刻記住狀態圖,因為在錯誤的狀態下調用方法是導致bug的共性問題。
釋放MediaPlayer
MediaPlayer占用系統資源。因此,你應該經常需要額外注意保證在不需要MediaPlayer實例時不再持有它。當你使用完它后,你應該調用release()方法保證所有分配給它的系統資源正確釋放。比如,當使用MediaPlayer的Activity回調onStop()時,你必須釋放該MediaPlayer,因為當你的activity不再與用戶交互后再持有它基本是沒有意義的(除非你在后臺播放媒體,這將在下一小節討論)。當然,當你的activity恢復或重啟時,你需要再創建一個MediaPlayer并在開始播放前再次準備它。
以下是你如何釋放并置空MediaPlayer
mediaPlayer.release();
mediaPlayer = null;
作為一個例子,考慮這個問題,當activity已停止,但你忘記釋放MediaPlayer,但是activity重啟后你又再次創建了一個實例。就如你可能知道的,當用戶旋轉屏幕(或者使用其他方法改變了設備配置),系統捕獲通過重啟activity(默認)來捕獲該行為,所以當用戶來回操作設備你可能很快消耗所有的系統資源,因為每次的方向轉換,你都會創建一個新的永遠不會釋放的MediaPlayer。(更多關于運行時重啟問題,詳見運行時處理改變)
你可能想知道,即使用戶離開activity,如果你想繼續播放背景媒體時會發生什么,這與內置音樂應用程序的行為非常相似。在這種情況下,你需要通過Service控制播放,我們在下一節討論,
在Service中使用MediaPlayer
如果你想在后臺播放媒體,即使你的應用已不再屏幕上顯示。也就是說,你希望當用戶在和其他應用交互式時,你的應用能夠繼續播放媒體。
那樣的話,你必須啟動一個Service去控制該MediaPlayer實例。你需要在MediaBrowserServiceCompat service中嵌入MediaPlayer并在另一個activity中與MediaBrowserCompat交互。
你應該注意這個客戶端/服務器設置。這里有一些關于在后臺服務中運行的播放器如何與剩余系統交互的期望。如果你的應用沒有旅行這些期望,用戶可能會有糟糕的體驗。更多細節請查看構建音頻應用
這一小節描述了一些關于在后臺服務中管理MediaPlayer的特殊命令。
異步運行
首先,類似與activity,service的所有工作默認在一個單獨的線程中進行。事實上,如果你在相同的應用中運行activity和service,他們默認使用相同的線程(主線程)。因此,服務需要快速處理傳入的意圖,并且在響應時不要執行冗長的計算。如果預期有大量的工作或阻塞調用,你必須在異步任務中完成這些操作。可以自行創建線程或者使用框架中異步操作的工具。
例如,在主線程中使用MediaPlayer,你應該調用prepareAysnc()而不是
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 = ... // 初始化
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.prepareAsync(); // 異步的準備不會阻塞UI線程
}
}
/** MediaPlayer準備完畢后調用 */
public void onPrepared(MediaPlayer player) {
player.start();
}
}
處理異步錯誤
在異步操作中,錯誤通常以異常或一個錯誤碼的方式標記,但是無論何時你使用異步資源時,你應該保證你的應用有合適的錯誤提醒。基于這一點,你應該實現MediaPlayer.OnErrorListener并且用MediaPlayer實例設置它來完成。
public class MyService extends Service implements MediaPlayer.OnErrorListener {
MediaPlayer mMediaPlayer;
public void initMediaPlayer() {
// 初始化MediaPlayer
mMediaPlayer.setOnErrorListener(this);
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// ... react appropriately ...
// MediaPlayer異常,請重置
}
}
一定要記住,當MediaPlayer出現錯誤,即MediaPlayer轉換為錯誤狀態(詳見文檔中的狀態圖),你必須在在此使用它之前重置。
使用睡眠鎖
設計在后臺播放媒體的應用,當你的服務在運行期時,設備可能會休眠。因為Android系統嘗試在設備休眠時節電,系統會嘗試關閉手機的不必要的功能,
包括CPU和WIFI硬件模塊。然而,如果你在運行播放服務或在播放音樂,則需要放置系統干擾播放。
為了保證你的服務在這種情況下繼續運行,你必須使用休眠鎖。休眠鎖是一種向系統發出信號的方式,即你的應用正在使用某些功能,即使手機處于空閑狀態也應保持可用。
*注意:你應該保守的使用休眠鎖并在真正需要時使用,因為它會顯著的降低電池壽命。
為了保證當你的MediaPlayer在播放時保持CPU繼續運行,當你初始化MediaPlayer時調用[setWakeMode()](https://developer.android.google.cn/reference/android/media/MediaPlayer.html#setWakeMode(android.content.Context, int))
一旦你這樣做,MediaPlayer會在播放時保持鎖定,在暫停和停止后釋放鎖。
mMediaPlayer = new MediaPlayer();
// ... other initialization here ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
然而,以上例子中獲取的睡眠鎖只能保證CPU保持喚醒狀態。如果你在使用wifi播放網絡流媒體,你可能也想要保持WifiLock,你必須手動獲取和釋放。所以,當你啟動使用遠程URL資源
的MediaPlayer,你應該創建并獲取WIFI 鎖,例如
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");
wifiLock.acquire();
當你暫停或停止媒體,或者當你不再使用網絡時,你應該釋放鎖
wifiLock.release();
執行清理
之前有提到過,MediaPlayer對象會消耗大量的系統資源,所以你應該僅僅在使用它時保持并在使用完后調用release()釋放。手動調用清理方法而不是依賴系統垃圾收集器是很重要的,因為在垃圾收集器會花一些時間去回收MediaPlayer,因為它只是對內存需求敏感,而不是缺少其他與媒體相關的資源。所以,基于這個原因,當你使用service時,你應該重寫onDestroy()
方法并保證釋放MediaPlayer
public class MyService extends Service {
MediaPlayer mMediaPlayer;
// ...
@Override
public void onDestroy() {
if (mMediaPlayer != null) mMediaPlayer.release();
}
}
除了在關閉時釋放它,你也應該會經常看到其他釋放MediaPlayer的時機。比如,你期望在一段較長的時間內不再播放媒體(比如,失去播放焦點),你應該毫無疑問的釋放存在的MediaPlayer,然后稍后在創建它。另一方面,如果你僅在一小段時間內停止播放,你獲取應該繼續持有MediaPlayer避免再次創建和準備它的開銷
數字版本管理
從Android8.0開始,MediaPlayer加入了支持播放受DRM保護的素材的API。它和MediaDrm提供的低級別API相似,但是它在一個更高級別進行操作,不會暴露底層的提取器,drm和加密對象。
雖然MediaPlayer DRM API并不支持完整的MediaDrm功能,但它提供了最常見的用例。當前實現可以處理以下內容類型:
- Widevine-protected 本地媒體文件
- Widevine-protected 遠程或流媒體文件
以下的代碼片段演示了如何在簡單的同步實現中使用新的DRM MediaPlayer方法。
要管理DRM控制的媒體,您需要將新方法與通常的MediaPlayer調用流一起包括在內,如下所示:
setDataSource();
setOnDrmConfigHelper(); // 可選,自定義配置
prepare();
if (getDrmInfo() != null) {
prepareDrm();
getKeyRequest();
provideKeyResponse();
}
// MediaPlayer已準備好,可以使用
start();
// ...play/pause/resume...
stop();
releaseDrm();
像平常一樣,從初始化MediaPlayer對象并使用setDataSource()方法設置源開始,然后,使用以下步驟,使用DRM。
- 如果你想app執行自定義的配置,定義OnDrmConfigHelper接口,
并使用setOnDrmConfigHelper()
關聯播放器。 - 調用prepare()
- 調用getDrmInfo(),如果視頻源包含DRM內容,方法會返回非空的MediaPlayer.DrmInfo值
如果MediaPlayer.DrmInfo存在:
- 檢查UUID的映射并選擇一個
- 調用prepareDrm()給當前的視頻源準備DRM配置
- 如果你創建并注冊了OnDrmConfigHelper回調,當prepareDrm()執行時會回調。這會讓你在打開DRM session前執行自定義的DRM屬性配置。回調會在調用prepareDrm()的線程同步的調用。
使用getDrmPropertyString()和setDrmPropertyString() 來訪問DRM屬性。避免執行冗余的操作 - 如果尚未設置設備,則prepareDrm()還會訪問配置服務器以配置設備。 這可能需要不同的時間,具體取決于網絡連接
- 調用getKeyRequest()來獲取要發送到許可證服務器的不透明密鑰請求字節數組
- 調用[provideKeyResponse()](https://developer.android.google.cn/reference/android/media/MediaPlayer.html#provideKeyResponse(byte[], byte[]))來提醒DRM引擎已收到來自許可證服務器的響應,
返回值取決于請求的key的類型
- 如果響應是一個脫機秘鑰請求,則結果是一個秘鑰集標識符。你可以將次秘鑰集標識符與restoreKeys()一起使用,以將秘鑰還原到新會話
- 如果響應是針對流式傳輸或釋放請求,則結果為null。
異步執行prepareDrm()
默認的,prepareDrm()同步執行,阻塞知道準備完成。然而,在一個新設備上首次DRM準備需要配置,使用prepareDrm()響應處理可能會因為涉及網絡操作的原因導致耗時。你可以通過定義和配置MediaPlayer.OnDrmPreparedListener
避免使用阻塞式的prepareDrm()
當你設置了OnDrmPreparedListener,parepareDrm()會在后臺執行設置(如果需要的話)和準備,當設置和準備完成后,監聽器會被回調。你不應該對調用序列或偵聽器運行的線程做出任何假設(除非監聽器在handler線程注冊)。該監聽器會在prepareDrm()返回前后被調用。
異步設置DRM
你可以通過創建和注冊MediaPlayer.OnDrmInfoListener準備DRM來異步的初始化DRM并使用MediaPlayer.OnDrmPreparedListener來啟動播放器,他們與prepareAsync()一起使用,如下:
setOnPreparedListener();
setOnDrmInfoListener();
setDataSource();
prepareAsync();
// ....
// 如果數據源內容被保護,你會收到onDrmInfo()的回調.
onDrmInfo() {
prepareDrm();
getKeyRequest();
provideKeyResponse();
}
// 當prepareAsync()調用完畢,你會收到onPrepared()回調。
// 如果視頻源手DRM保護,onDrmInfo會在該回調之前設置它,所以你可以直接啟動播放器
onPrepared() {
start();
}
處理加密媒體
從Android 8.0(API級別26)開始,MediaPlayer還可以為基本流類型H.264和AAC解密公共加密方案(CENC)和HLS樣本級加密媒體(METHOD = SAMPLE-AES)。 之前支持全段加密媒體(METHOD = AES-128)。
從ContentResolver中獲取媒體
MediaPlayer的另一個有用的功能點是獲取用戶設備中的音樂。你可以通過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使用ContentResolver,如下:
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...
示例代碼
android- SimpleMediaPlayer展示如何構建獨立的播放器。android-BasicMediaDecoder和 android-DeviceOwner深入展示了本頁面所涉及到的API
更多
以下頁面的話題設計到錄音,攝像,播放音視頻