Android 多媒體框架包含了支持播放的一系列常見多媒體類型,以此可以很容易地整合諸如音頻、視頻、圖片到你的應用程序。資源文件、本地和網(wǎng)絡中的視頻、音頻都可以通過Media Player播放。
本文展示如何寫一個高性能并且體驗良好的多媒體播放應用。
基礎知識
下邊是兩個Android中用來播放聲音、視頻的類
- Media Player
提供播放聲音、視頻的API。 - AudioManager
管理設備上的音頻源和音頻輸出
Manifest 聲明
使用Media Player 開發(fā)之前,確定已經(jīng)在清單文件Manifest 的正確位置聲明了所需要的權(quán)限。
- 如果需要播放網(wǎng)絡數(shù)據(jù),需要聲明網(wǎng)絡權(quán)限
<uses-permission android:name="android.permission.INTERNET" /> - 如果需要屏幕常亮,需要聲明
<uses-permission android:name="android.permission.WAKE_LOCK" />
使用Media Player
Media Player 是多媒體框架中的一個重要組件。這個類的實例可以通過最少的設置獲取、解碼以及播發(fā)音視頻,支持下面集中不同的播放源:
- 本地資源
- 內(nèi)部URI
- 外部URL(流)
- 播放本地資源文件(res/raw 目錄下):
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); //不需要調(diào)用prepare()方法,因為在create中已經(jīng)執(zhí)行了。
- 播放系統(tǒng)返回的Uri
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(); // 耗時操作
mediaPlayer.start();
如果您正在通過一個網(wǎng)址來流一個在線媒體文件,該文件必須是能夠進行下載的。
因為文件可能不存在,所以在setDataSource()時需要處理IOException和IllegalArgumentException
異步準備操作
原則上使用Media Player是簡單直接的,但是在將它整合進一個Android 應用的時需要謹記一些額外的東西。例如,因為prepare()會獲取并解碼多媒體數(shù)據(jù),是一個耗時操作,所以不能在UI線程調(diào)用,否則會造成線程阻塞,程序無響應,降低用戶體驗。即使加載資源文件特別快,UI響應耗時超過一秒就會有一個明顯的卡頓,給用戶一種程序運行卡慢的感覺。
為了避免線程阻塞,需要另開線程準備MediaPlayer,并在準備完成后通知主線程。prepareAsync()方法在后臺準備media并且立即返回。media準備完會調(diào)用MediaPlayer.OnPrepareListener 的onPrepared()方法,通過setOnPrepareListener()方法可以給MediaPlayer設置。
管理狀態(tài)
MediaPlayer另一個需要注意的方面是它是基于狀態(tài)的。這就是說,寫代碼的時候需要知道MediaPlayer有自己的內(nèi)部狀態(tài),因為指定的操作只有在player處于特定狀態(tài)的時候有效。如果在錯誤的狀態(tài)下執(zhí)行操作,系統(tǒng)會拋異?;蛘邔е缕渌豢深A期的行為。
MediaPlayer的API文檔展示了完整的狀態(tài)表。狀態(tài)表闡明了哪個方法把MediaPlayer從一個狀態(tài)改變成另一種狀態(tài)。例如:當你新創(chuàng)建一個MediaPlayer,它處在空閑狀態(tài)(Idle state),這時應該調(diào)用setDataSource()方法初始化它,狀態(tài)改為初始化狀態(tài)。之后應該通過prepare()或prepareAsync()方法準備。MediaPlayer準備完畢后,進入已準備狀態(tài)(Prepared state),這時就可以調(diào)用start()方法播放了。這時,可以通過調(diào)用start()、pause()和seekTo()方法將狀態(tài)在已開始(Started)、暫停(Paused)和播放完成(PlaybackCompleted)之間轉(zhuǎn)換了。調(diào)用stop()后,只有重新準備才能再次start。
操作MediaPlayer的實例時應時刻謹記狀態(tài)表,因為經(jīng)常會在錯誤的狀態(tài)調(diào)用方法造成bug。
釋放MediaPlayer
MediaPlayer會消耗寶貴的系統(tǒng)資源,因此,你應該采取額外措施防止在不必要時擁有MediaPlayer實例。用完之后需要調(diào)用release()方法來確保系統(tǒng)合適回收分配給它的資源。例如,在Activity中使用MediaPlayer 時,在onStop()中必須釋放MediaPlayer,因為當Activity失去焦點時會保持MediaPlayer的實例(除非你想在后臺播放,但是系統(tǒng)不推薦這樣)。當Activity被喚醒(Resumed)或重啟(Restarted)時,你需要新創(chuàng)建一個MediaPlayer實例并在播放前準備。
mediaPlayer.release();
mediaPlayer = null;
舉個例子,假設你在Activity 開始的時候新創(chuàng)建了一個MediaPlayer實例,但是Activity 停止時忘記釋放MediaPlayer。當橫豎屏來回切換時,默認系統(tǒng)會重新啟動Activity,因為每次啟動都會新創(chuàng)建一個MediaPlayer實例,這將很快消耗完系統(tǒng)內(nèi)存。
如果想在用戶離開當前頁面后仍然像音樂播放器那樣播放音視頻,應該在Service中控制MediaPlayer。
Service中使用MediaPlayer
如果想在應用進入后臺時仍然播放,就必須要啟動一個Service來控制一個MediaPlayer 的實例了。這時應該小心,因為用戶和系統(tǒng)期望在應用后臺運行的同時可以與其它應用互相影響,如果沒有考慮這些,就會降低用戶體驗,這塊主要描述你應該知道并提供建議達到它。
異步運行
首先,跟Activity 一樣,Service 中所有的工作都在一個線程中完成,實際上,如果在同一個應用中啟動一個Activity 和一個Service ,他們運行在同一個線程,也就是主線程。因此,Service 需要快速響應傳遞進來的Intent ,響應時不進行耗時操作。如果任何繁重的工作或阻塞調(diào)用,你必須異步完成這些任務:無論是從另一個線程、自己實現(xiàn)或使用該框架的許多異步處理設施。
例如,在主線程使用MediaPlayer 時,應該調(diào)用prepareAsync()而不是prepare(),并且實現(xiàn)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();
}
}
/** MediaPlayer prepare完成后回調(diào) */
public void onPrepared(MediaPlayer player) {
player.start();
}
}
處理異步error
異步操作時,error通常會通過異?;蝈e誤碼的形式通知,但是,不管什么時候使用異步資源,應該確定應用在適當?shù)臅r候提示錯誤。針對MediaPlayer,你可以通過給MediaPlayer實例設置MediaPlayer.OnErroristener接口并實現(xiàn)接口來完成。
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!
}
}
error產(chǎn)生時,MediaPlayer會變成錯誤狀態(tài)(Error state),必須重置之后才能再次使用。
使用喚醒鎖(wake locks)
設計后臺播放應用時,設備可能在service 運行的時候睡眠,Android 系統(tǒng)會盡量保持電量,所以就會停止一些不必要的手機功能,如果這時你的service 在播放音頻或網(wǎng)絡音頻,你應該防止系統(tǒng)干撓播放。
為了保證在這些條件下service 仍然運行,需要使用“wake locks”。wake locks會通知系統(tǒng)你的應用需要保持可用以執(zhí)行某些功能,即使手機是空閑的。
必要時使用喚醒鎖,因為它會降低電池使用壽命。
調(diào)用setWakeMode()方法初始化MediaPlayer 可以確保播放過程中CPU 持續(xù)運行,這個MediaPlayer 在播放時會擁有特定的鎖,當Activity 暫停時會釋放鎖。
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
喚醒鎖只會保證CPU 喚醒,但是使用Wi-Fi 播放網(wǎng)絡音頻時,同樣需要手動獲取釋放Wi-Fi 鎖。
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "myLock");
wifiLock.acquire();
暫停、停止播放或者不需要網(wǎng)絡時應該釋放
wifiLock.release();
處理音頻焦點
雖然在任何給定的時間只有一個活動可以運行,但是Android是一個多任務環(huán)境。這帶來了一個特定的挑戰(zhàn),使用音頻的應用程序,因為只有一個音頻輸出,但有可能是幾個競爭使用媒體服務。在安卓2.2之前,沒有內(nèi)置的機制來解決這個問題,這可能在某些情況下導致一個壞的用戶體驗。例如,當一個用戶正在聽音樂而另一個應用程序需要通知用戶的一些非常重要的東西時,用戶可能由于大聲的音樂聽不到通知的聲音。從安卓2.2開始,提供了一種方法來協(xié)商使用該設備的音頻輸出的方法。這種機制被稱為音頻焦點。
當應用需要輸出像音樂或者通知這樣的音頻時,應實時請求焦點。獲取焦點后,你可以自由輸出音頻,但是需要監(jiān)聽焦點變化。被通知失去焦點時,應該立即關閉音頻或者調(diào)低音量,再次獲取焦點時喚醒大聲播放。
可以通過AudioManager的requestAudioFocus()方法請求焦點。
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// 沒有獲取焦點
}
第一個參數(shù)是AudioManager.OnAudioFocusChangeListener ,音頻焦點變化后會回調(diào)它的onAudioFocusChange()方法,所以你需要在Activity 或Service 實現(xiàn)這個接口并重寫onAudioFocusChange()方法。
class MyService extends Service
implements AudioManager.OnAudioFocusChangeListener {
// ....
public void onAudioFocusChange(int focusChange) {
// Do something based on focus change...
}
}
方法參數(shù)中的focusChange就是音頻焦點值,是下列值之一
- AUDIOFOCUS_GAIN:獲取焦點。
- AUDIOFOCUS_LOSS:失去焦點,應該做好長時間失去焦點的準備,這是盡可能釋放資源的好地方,比如,你可以釋放MediaPlayer。
- AUDIOFOCUS_LOSS_TRANSIENT:瞬時失去焦點,只是瞬時失去焦點,不久就可以再次獲取焦點,可以保留資源。
- AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:瞬時失去焦點,但是允許低音量播放。
示例:
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
// resume playback
if (mMediaPlayer == null) initMediaPlayer();
else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start();
mMediaPlayer.setVolume(1.0f, 1.0f);
break;
case AudioManager.AUDIOFOCUS_LOSS:
// Lost focus for an unbounded amount of time: stop playback and release media player
if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// Lost focus for a short time, but we have to stop
// playback. We don't release the media player because playback
// is likely to resume
if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// Lost focus for a short time, but it's ok to keep playing
// at an attenuated level
if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f);
break;
}
}
注意音頻焦點只在Android 2.2以及以上的系統(tǒng)可用。
清理
如前所述,MediaPlayer實例會消耗很多系統(tǒng)資源,所以應該只是在需要時擁有實例,并在完成時調(diào)用release()方法釋放。調(diào)用release()方法而不是依靠系統(tǒng)的垃圾收集是很重要的,因為它是敏感的內(nèi)存需求,而不是其他媒體相關資源短缺,垃圾回收器自動回收MediaPlayer會有一段時間。因此當你使用Service時,你應該重寫ondestroy()方法確保你釋放MediaPlayer:
public class MyService extends Service {
MediaPlayer mMediaPlayer;
// ...
@Override
public void onDestroy() {
if (mMediaPlayer != null) mMediaPlayer.release();
}
}
處理AUDIO_BECOMING_NOISY Intent
編碼良好的應用在音頻變成噪音時會自動停止播放,可以通過處理AUDIO_BECOMING_NOISY Intent確保應用在這種情況下可以停止播放。
<receiver android:name=".MusicIntentReceiver">
<intent-filter>
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver>
public class MusicIntentReceiver extends android.content.BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent intent) {
if (intent.getAction().equals(
android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
// signal your service to stop playback
// (via an Intent, for instance)
}
}
}
從Content Resolver獲取Media
示例:
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());
}
結(jié)合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...