Android 多媒體框架包含了支持播放的一系列常見多媒體類型配并,以此可以很容易地整合諸如音頻、視頻高镐、圖片到你的應用程序溉旋。資源文件、本地和網絡中的視頻嫉髓、音頻都可以通過Media Player播放观腊。
本文展示如何寫一個高性能并且體驗良好的多媒體播放應用。
基礎知識
下邊是兩個Android中用來播放聲音岩喷、視頻的類
- Media Player
提供播放聲音恕沫、視頻的API监憎。 - AudioManager
管理設備上的音頻源和音頻輸出
Manifest 聲明
使用Media Player 開發(fā)之前纱意,確定已經在清單文件Manifest 的正確位置聲明了所需要的權限。
- 如果需要播放網絡數(shù)據(jù)鲸阔,需要聲明網絡權限
<uses-permission android:name="android.permission.INTERNET" />
- 如果需要屏幕常亮偷霉,需要聲明
<uses-permission android:name="android.permission.WAKE_LOCK" />
使用Media Player
Media Player 是多媒體框架中的一個重要組件。這個類的實例可以通過最少的設置獲取褐筛、解碼以及播發(fā)音視頻类少,支持下面集中不同的播放源:
- 本地資源
- 內部URI
- 外部URL(流)
- 播放本地資源文件(res/raw 目錄下):
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); //不需要調用prepare()方法,因為在create中已經執(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();
如果您正在通過一個網址來流一個在線媒體文件硫狞,該文件必須是能夠進行下載的。
因為文件可能不存在,所以在setDataSource()時需要處理IOException和IllegalArgumentException
異步準備操作
原則上使用Media Player是簡單直接的残吩,但是在將它整合進一個Android 應用的時需要謹記一些額外的東西财忽。例如,因為prepare()會獲取并解碼多媒體數(shù)據(jù)泣侮,是一個耗時操作即彪,所以不能在UI線程調用,否則會造成線程阻塞活尊,程序無響應隶校,降低用戶體驗。即使加載資源文件特別快蛹锰,UI響應耗時超過一秒就會有一個明顯的卡頓深胳,給用戶一種程序運行卡慢的感覺。
為了避免線程阻塞铜犬,需要另開線程準備MediaPlayer稠屠,并在準備完成后通知主線程。prepareAsync()方法在后臺準備media并且立即返回翎苫。media準備完會調用MediaPlayer.OnPrepareListener 的onPrepared()方法权埠,通過setOnPrepareListener()方法可以給MediaPlayer設置。
管理狀態(tài)
MediaPlayer另一個需要注意的方面是它是基于狀態(tài)的煎谍。這就是說攘蔽,寫代碼的時候需要知道MediaPlayer有自己的內部狀態(tài),因為指定的操作只有在player處于特定狀態(tài)的時候有效呐粘。如果在錯誤的狀態(tài)下執(zhí)行操作满俗,系統(tǒng)會拋異常或者導致其他不可預期的行為作岖。
MediaPlayer的API文檔展示了完整的狀態(tài)表唆垃。狀態(tài)表闡明了哪個方法把MediaPlayer從一個狀態(tài)改變成另一種狀態(tài)。例如:當你新創(chuàng)建一個MediaPlayer痘儡,它處在空閑狀態(tài)(Idle state)辕万,這時應該調用setDataSource()方法初始化它,狀態(tài)改為初始化狀態(tài)沉删。之后應該通過prepare()或prepareAsync()方法準備渐尿。MediaPlayer準備完畢后,進入已準備狀態(tài)(Prepared state)矾瑰,這時就可以調用start()方法播放了砖茸。這時,可以通過調用start()殴穴、pause()和seekTo()方法將狀態(tài)在已開始(Started)凉夯、暫停(Paused)和播放完成(PlaybackCompleted)之間轉換了货葬。調用stop()后,只有重新準備才能再次start劲够。
操作MediaPlayer的實例時應時刻謹記狀態(tài)表宝惰,因為經常會在錯誤的狀態(tài)調用方法造成bug。
釋放MediaPlayer
MediaPlayer會消耗寶貴的系統(tǒng)資源再沧,因此尼夺,你應該采取額外措施防止在不必要時擁有MediaPlayer實例。用完之后需要調用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)內存。
如果想在用戶離開當前頁面后仍然像音樂播放器那樣播放音視頻呵哨,應該在Service中控制MediaPlayer赁濒。
Service中使用MediaPlayer
如果想在應用進入后臺時仍然播放,就必須要啟動一個Service來控制一個MediaPlayer 的實例了孟害。這時應該小心拒炎,因為用戶和系統(tǒng)期望在應用后臺運行的同時可以與其它應用互相影響,如果沒有考慮這些挨务,就會降低用戶體驗击你,這塊主要描述你應該知道并提供建議達到它。
異步運行
首先耘子,跟Activity 一樣果漾,Service 中所有的工作都在一個線程中完成,實際上谷誓,如果在同一個應用中啟動一個Activity 和一個Service ,他們運行在同一個線程吨凑,也就是主線程捍歪。因此户辱,Service 需要快速響應傳遞進來的Intent ,響應時不進行耗時操作糙臼。如果任何繁重的工作或阻塞調用庐镐,你必須異步完成這些任務:無論是從另一個線程、自己實現(xiàn)或使用該框架的許多異步處理設施变逃。
例如必逆,在主線程使用MediaPlayer 時,應該調用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完成后回調 */
public void onPrepared(MediaPlayer player) {
player.start();
}
}
處理異步error
異步操作時,error通常會通過異郴嗣蓿或錯誤碼的形式通知损拢,但是,不管什么時候使用異步資源撒犀,應該確定應用在適當?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產生時荆姆,MediaPlayer會變成錯誤狀態(tài)(Error state),必須重置之后才能再次使用映凳。
使用喚醒鎖(wake locks)
設計后臺播放應用時胞枕,設備可能在service 運行的時候睡眠,Android 系統(tǒng)會盡量保持電量魏宽,所以就會停止一些不必要的手機功能腐泻,如果這時你的service 在播放音頻或網絡音頻,你應該防止系統(tǒng)干撓播放队询。
為了保證在這些條件下service 仍然運行派桩,需要使用“wake locks”。wake locks會通知系統(tǒng)你的應用需要保持可用以執(zhí)行某些功能蚌斩,即使手機是空閑的铆惑。
必要時使用喚醒鎖,因為它會降低電池使用壽命送膳。
調用setWakeMode()方法初始化MediaPlayer 可以確保播放過程中CPU 持續(xù)運行员魏,這個MediaPlayer 在播放時會擁有特定的鎖,當Activity 暫停時會釋放鎖叠聋。
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
喚醒鎖只會保證CPU 喚醒撕阎,但是使用Wi-Fi 播放網絡音頻時,同樣需要手動獲取釋放Wi-Fi 鎖碌补。
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "myLock");
wifiLock.acquire();
暫停虏束、停止播放或者不需要網絡時應該釋放
wifiLock.release();
處理音頻焦點
雖然在任何給定的時間只有一個活動可以運行棉饶,但是Android是一個多任務環(huán)境。這帶來了一個特定的挑戰(zhàn)镇匀,使用音頻的應用程序照藻,因為只有一個音頻輸出,但有可能是幾個競爭使用媒體服務汗侵。在安卓2.2之前幸缕,沒有內置的機制來解決這個問題,這可能在某些情況下導致一個壞的用戶體驗晰韵。例如发乔,當一個用戶正在聽音樂而另一個應用程序需要通知用戶的一些非常重要的東西時,用戶可能由于大聲的音樂聽不到通知的聲音宫屠。從安卓2.2開始列疗,提供了一種方法來協(xié)商使用該設備的音頻輸出的方法。這種機制被稱為音頻焦點浪蹂。
當應用需要輸出像音樂或者通知這樣的音頻時抵栈,應實時請求焦點。獲取焦點后坤次,你可以自由輸出音頻古劲,但是需要監(jiān)聽焦點變化。被通知失去焦點時缰猴,應該立即關閉音頻或者調低音量产艾,再次獲取焦點時喚醒大聲播放。
可以通過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 闷堡,音頻焦點變化后會回調它的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)資源糟描,所以應該只是在需要時擁有實例,并在完成時調用release()方法釋放书妻。調用release()方法而不是依靠系統(tǒng)的垃圾收集是很重要的船响,因為它是敏感的內存需求,而不是其他媒體相關資源短缺躲履,垃圾回收器自動回收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());
}
結合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...