Media and Camera
Media Playback
Android的多媒體架構支援各種格式的媒體類型,你可以很容易的內建音頻,視訊,圖像到你的應用中,通過 MediaPlayer的API,你可以從你的應用資源的媒體檔案,或者檔案系統的檔案,或者網絡連接配接的資料流。播放音頻或者視訊。
該文章主要展示如何寫一個媒體播放的應用,在使用者和系統之間互動,進而獲得一個更好的使用者體驗。
注意:你隻能使用标準輸出來播放音頻資料,也就是揚聲器或者藍牙耳機,你不能在通話過程中播放音頻檔案
基礎
下面兩個類在Android中用來播放音視訊
- MediaPlayer
播放音視訊的基本API
- AudioManager
用來管理音頻源和裝置的音頻輸出路徑
Manifest
在使用MediaPlayer之前,需要manifest 中聲明權限
- 網絡權限
如果MediaPlayer 播放網絡的資料流,需要申請網絡權限
-
Wake Lock權限(Wake Lock Permission )
如果應用需要保持螢幕常亮,或者阻止處理器睡眠(或者調用了MediaPlayer.setScreenOnWhilePlaying() , MediaPlayer.setWakeMode() 方法)。你需要聲明如下權限
使用MediaPlayer
MediaPlayer是Android多媒體架構中非常重要的一個元件,MediaPlayer可以使用最少的代碼—擷取,解碼,播放音頻和視訊。它支援多種不同的多媒體源:
- 本地資源
- 内部URI,例如通過Content Resolver 擷取資源
- 外部URL(例如網絡流)
支援的格式:
Android支援的媒體格式
下面通過示例來說明幾種不同源的多媒體播放.
本地Raw資源(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’資源是系統不用特别解析的檔案(比如layout目錄下的資源,是需要系統解析的)。不過,這裡音頻不是raw音頻,而一般是經過編碼壓縮的—注意差別這裡raw的意思。
本地URI
Uri myUri = ....; //在這裡初始化Uri
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();
外部URL
String url = "http://........"; // URL
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // 可能會很長時間
mediaPlayer.start();
(關鍵在setDataSource,設定什麼資料源)
注意:
使用setDataSource()時,你必須捕獲或者抛出 IllegalArgumentException 或者IOException。因為你引用的檔案可能不存在
異步準備
使用MediaPlayer 隻需要簡單的遵守其規則即可,但是有幾個很重要的點。例如:
調用prepare()方法需要占用一段時間,因為它需要擷取并解碼多媒體資料,是以不要在應用的UI線程中調用。以免引起ANR。
你需要啟動一個新的線程來處理準備工作,在完成後通知主線程,你可以自己實作自己的線程邏輯,但是更通用的做法是使用Android架構提供好的prepareAsync() 方法,該方法會在背景準備多媒體,然後馬上傳回,當準備結束後,通過setOnPreparedListener() 方法設定的MediaPlayer.OnPreparedListener中的onPrepared() 會被調用。然後就知道已經準備好了。
管理狀态
另一個需要注意的方面是MediaPlayer是基于狀态的,是以你在寫代碼的時候就要注意他的内部狀态,因為個别操作隻能在特定的狀态下操作。如果你在錯誤的狀态進行了操作,系統會抛出一個exception或者導緻預想不到的行為。
MediaPlayer 類的文檔展示了一個完整的狀态圖,如下:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiMwAzMzMDMzIDOwMDM1EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
當你建立一個 MediaPlayer ,就處于Idle狀态,這時,你應該調用 setDataSource()來初始化。進而進入Initized狀态。
然後通過調用 prepare() 或 prepareAsync() 來準備,準備結束後,會進入Prepared狀态。這時你可以調用start()方法來播放媒體。
這時,如圖表所示,你可以調用 start(), pause(), 或 seekTo()來轉移到 Started, Paused 或PlaybackCompleted 狀态。
需要注意的是,如果你調用了stop(),你不能在調用start()重新播放,而是必須再次prepare好了才可以播放。
你需要在寫代碼的時候,時刻注意這個狀态圖,如果在錯誤的狀态調用了不該調用的方法,是導緻bug的主要原因。
釋放 MediaPlayer
MediaPlayer需要消耗系統資源,是以,沒必要的話,就不要讓MediaPlayer執行個體再存在了,你可以調用release()方法來確定所有配置設定給MediaPlayer的系統資源全部釋放掉。例如,你在使用MediaPlayer,在你的activity的onStop()方法中,你可以釋放掉MediaPlayer。(除非是背景播放媒體,下一章節讨論。)。當你的activity恢複或者重新啟動時,你需要重新建立一個新的MediaPlayer執行個體,并在播放前再次調用prepare。
示例代碼:
mediaPlayer.release();
mediaPlayer = null;
如果你忘記在activity停止後釋放 MediaPlayer 。但是activity再次啟動的時候又重新建立了一個MediaPlayer.(在系統螢幕旋轉時,或者configuration改變的時候,系統會預設重新啟動activity)如果使用者來回的旋轉手機,很快系統資源就會想因為建立了太多MediaPlayer執行個體而消耗掉過多系統資源。
使用Service播放多媒體
如果想應用不在螢幕顯示的時候,能夠背景播放多媒體,你可以啟動一個Service,并在Service裡控制MediaPlayer執行個體。
使用者對應用在背景運作時,如何和系統的其他部分互動有自己的期待,你的應用如果不能夠滿足這些期待,使用者可能會覺得體驗不好,該章節讨論你應該注意的問題和如何解決它們的建議。
異步運作
首先,就像Activity,Service中的工作都是預設運作在單線程中的,實際上Activity和Service都是預設運作在同一個線程中的——————主線程。是以,Service必須快速的處理業務,并且不能進行耗時的計算。如果必須做耗時的計算,你需要異步的處理這些tasks——可以是你自己實作另一個線程,或者使用架構提供好的異步處理機制。
當在你的主線程中使用MediaPlayer時,你應該優先使用 prepareAsync() 而非prepare(),然互實作MediaPlayer.OnPreparedListener 接口來接收prepare準備好了的事件,然後開始播放。
示例代碼:
public class MyService extends Service implements MediaPlayer.OnPreparedListener {
private static final 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();
}
}
示例中的自定義Service實作了MediaPlayer.OnPreparedListener 接口,并在onPrepared方法中來調用MediaPlayer的start方法來播放。
(注意需要調用mMediaPlayer.setOnPreparedListener(this)來綁定監聽器)。
處理異步的錯誤
當進行同步操作時,一般錯誤會通過異常來通知,或者傳回錯誤碼。當使用異步資源是,你應該確定你的應用能夠妥善的處理錯誤。在 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!
}
}
上述代碼中自定義Service 實作MediaPlayer.OnErrorListener 。并通過mMediaPlayer.setOnErrorListener(this);
進行綁定。在接口的onError進行錯誤的處理。
注意:
一旦錯誤産生,MediaPlayer就會進入狀态圖中的**Error**狀态。你再次使用MediaPlayer前需要重建MediaPlayer
使用wake locks
當設計一個背景播放媒體的應用時,裝置可能會在你的Service運作時進入睡眠狀态,因為Android系統為在睡眠的時候節省電池。系統會嘗試任何非必須的特性。包括CPU和WiFi硬體。但是,如果你的service在播放或者讀取音樂,你還是希望能夠阻止系統妨礙你的播放。
為此,你應該使用wake locks。wake lock可以了解為休眠鎖,用來通知系統你的應用需要使用到一些系統特性,這樣當手機進入空閑狀态是依舊可用。
注意:
一定要保守的使用休眠鎖,當真的需要時再持有它們,否則會大量消耗電量
為了保證CPU在MediaPlayer 播放時繼續運作,調用在初始化MediaPlayer 是調用setWakeMode() 方法。 在MediaPlayer 播放的時候持有該鎖,在暫停或者停止時釋放該鎖。
示例代碼:
mMediaPlayer = new MediaPlayer();
// ... other initialization here ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
但是,上面的例子隻能保證CPU不睡眠,如果你的多媒體需要使用到網絡,并且使用的是WiFi.你還需要持有WifiLock—-也需要你手動的擷取和釋放。
示例代碼:
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");
wifiLock.acquire();
暫停或停止播放的時候,或者不再使用網絡時,記得釋放該鎖:
wifiLock.release();
作為前台Service運作
Service一般是用來運作背景任務,比如擷取郵件,異步資料,下載下傳内容等。這種情況,使用者不會意識到Service的運作,也可能即使Service被打斷并重新開機也不會知道。
但是對于播放音樂這種Service而言,使用者是很清楚的知道Service的運作的,是以播放期間是不能被打斷的。并且使用者希望能夠在Service運作期間能夠進行互動,這種情況下,Service應該作為“前台Service”來運作。前台Service在系統中具有較高的優先級—–系統幾乎不會kill掉這種Service。當在前台運作時,Service還需要提供一個狀态條來保證使用者能夠了解到運作中的Service的狀态,并允許使用者打開一個activity來和Service互動。
為了把你的Service變成前台Service。你需要建立一個通知( Notification)來展示狀态條,并在Service中調用 startForeground()來設為前台Service。
示例代碼:
String songName;
// 把歌曲名稱賦給songName字元串
PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), ,
new Intent(getApplicationContext(), MainActivity.class),
PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification();
notification.tickerText = text;
notification.icon = R.drawable.play0;
notification.flags |= Notification.FLAG_ONGOING_EVENT;
notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample",
"Playing: " + songName, pi);
startForeground(NOTIFICATION_ID, notification);
當你的Service在前台運作時,你設定的通知是在通知欄中可見的,如果使用者點選了這個通知,系統會調用你設定的PendingIntent (在上述代碼中,會啟動MainActivity)。
下圖展示了通知是如何顯示的:
隻有Service确實在運作時,才應該設定通知。如果Service不再跑了,需要通過調用stopForeground()來釋放:
stopForeground(true);
更多資訊參考Service和通知欄章節。
操作音頻焦點(Handling audio focus)
盡管同一時刻隻能有一個Activity運作,但Android是一個多任務系統,這給使用音頻的應用帶來了特殊的挑戰,因為隻有一個音頻輸出,但是可能會有多個媒體Service在競争使用。在Android2.2之前,并沒有内置的機制來處理這個問題,導緻一些情況體驗不太好。比如,在使用者聽音樂時,另一個應用需要通知重要事件,使用者可能因為大聲的音樂在播放而沒有聽到事件通知。
從Android2.2開始,系統提供了讓應用協商的機制來獲得裝置的音頻輸出。這種機制被稱作音頻焦點(Audio Focus)。
當你的應用需要輸出音頻時,你應該 請求音頻焦點。一旦擷取,應用可以自由的使用音頻輸出,但同時也要監聽焦點的變更。如果應用失去了音頻焦點,它必須立刻要麼關掉音頻或者把音量降低到靜音級别。隻有在再次獲得焦點後再恢複音量。
音頻焦點是合作機制的。應用應該遵守音頻焦點的規則,(不過這個規則不是強制的)。應用可以在失去焦點後依舊大聲的播放音樂,系統不會阻止,但是這回導緻不好的使用者體驗,沒準使用者會解除安裝你哦。
為了獲得音頻焦點,你應該使用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) {
// could not get audio focus.
}
首先獲得AudioManager服務,然後擷取音頻焦點,在确定擷取後,播放音頻。
requestAudioFocus()的第一個參數是一個AudioManager.OnAudioFocusChangeListener監聽器,監聽器的 onAudioFocusChange()方法會在音頻焦點變化時被調用。是以你可以在你的Service或者Activity中實作這個監聽器接口,并在焦點變化時做相應處理。
示例代碼:
class MyService extends Service
implements AudioManager.OnAudioFocusChangeListener {
// ....
public void onAudioFocusChange(int focusChange) {
// Do something based on focus change...
}
}
代碼中onAudioFocusChange(int focusChange)的focusChange 參數告訴你音頻焦點是如何變化的,其值可能是下面中的一種:
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:
// 恢複播放
if (mMediaPlayer == null) initMediaPlayer();
else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start();
mMediaPlayer.setVolume(f, f);
break;
case AudioManager.AUDIOFOCUS_LOSS:
//停止播放,釋放資源
if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// 停止播放
// 但是不釋放資源
// 因為可能很快再次得到焦點
if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 失去焦點,但是繼續播放
// 把聲音音量關小
if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(f, f);
break;
}
}
一定記住Android2.2(API leve8)之後才可以使用音頻焦點的API。如果你想支援較早版本的Android。你應該設定一個反相相容機制來在API不可用的時候正常運作。
為了實作反相相容,你可以通過反射來調用音頻焦點的方法,或者通過自定義一個單獨的類來實作音頻焦點的特性。
如下面實作的AudioFocusHelper類所示:
public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener {
AudioManager mAudioManager;
// other fields here, you'll probably hold a reference to an interface
// that you can use to communicate the focus changes to your Service
public AudioFocusHelper(Context ctx, /* other arguments here */) {
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
// ...
}
public boolean requestFocus() {
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
mAudioManager.requestAudioFocus(mContext, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
}
public boolean abandonFocus() {
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
mAudioManager.abandonAudioFocus(this);
}
@Override
public void onAudioFocusChange(int focusChange) {
// let your service know about the focus change
}
}
然後你可以隻在檢測到系統運作API level及以上的時候才在建立一個AudioFocusHelper 的執行個體。示例代碼:
if (android.os.Build.VERSION.SDK_INT >= ) {
mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
} else {
mAudioFocusHelper = null;
}
清理
如前面所提到的,MediaPlayer對象會消耗數量可觀的系統資源,是以你應該隻有在需要的時候保持它,不需要的時候即使調用release()方法來釋放。明确的調用這個清理的方法比依賴系統垃圾回收機制重要得多,因為系統垃圾回收肯呢過需要很長時間才能回收MediaPlayer。
是以在使用Service的時候,需要重寫onDestroy()方法來保證Service結束時順利釋放MediaPlayer。
示例代碼:
public class MyService extends Service {
MediaPlayer mMediaPlayer;
// ...
@Override
public void onDestroy() {
if (mMediaPlayer != null) mMediaPlayer.release();
}
}
除此之外,還應該在任何需要釋放MediaPlayer的地方都做到有效釋放。比如:
如果打算一段時間内不再播放音頻(或者失去了音頻焦點)。你definitely應該釋放存在的MediaPlayer對象,并在之後需要時重新建立。
當然,如果你隻是打算停止播放音頻一小會,那你可以不用釋放它,省得頻繁建立并準備。
處理AUDIO_BECOMING_NOISY Intent
許多非常好的應用在播放音頻時,會在發生意外導緻音頻變成噪聲時(揚聲器輸出),停止播放。
例如,當使用者用耳機聽音樂時,不小心耳機斷開了。
不過這種機制不會自動發生,需要程式猿去實作。如果你不去實作,音頻就可能通過揚聲器大聲的播放出來。
你可以通過處理 ACTION_AUDIO_BECOMING_NOISY intent來在這種情況下停止音樂。
首先,你需要在manifest檔案中注冊一個接受該intent的receiver。
<receiver android:name=".MusicIntentReceiver">
<intent-filter>
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver>
然後實作這個broadcast receiver :
public class MusicIntentReceiver implements android.content.BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent intent) {
if (intent.getAction().equals(
android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
// 停止音頻播放
// 比如,可以通過Intent來停止
}
}
}
通過Content Resolver擷取多媒體資源
一個媒體播放器應用的另一個很有用的特性是:能夠擷取使用者裝置上的音樂資源。你可以通過查詢external media的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) {
// 查詢失敗
} else if (!cursor.moveToFirst()) {
// 裝置上沒有media資源
} 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 = /* 例如上面代碼中擷取的某個ID */;
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...