MediaPlay 用來控制音頻、視訊檔案和流的回放。
- 狀态圖
-
回放控制是通過一個狀态機來管理的。
橢圓圖代表MediaPlay執行個體可能存在的一個狀态;
弧線代表使狀态之間轉換的操作;
單箭頭代表同步的方法調用;
雙箭頭代表異步的方法調用。
一個MediaPlay執行個體有以下狀态:
- 當new或者reset被調用,來建立MediaPlay執行個體後,它是Idle狀态。
- 但release被調用後,它是end狀态,在這兩者之間是它的生命周期。
- 通過new來構造執行個體和通過reset來産生執行個體,是有差別的,通過new實際就是通過create為給定的uri來建立mediaplayer,如果建立成功,其prepare方法會被自動調用,也自動調用了setDataSource,即完成了初始化。Reset方法會把執行個體轉到未初始化狀态,在調用reset之後,需要再次通過setDataSource,prepare方法來初始化它。
- 在建立好MediaPlayer執行個體後,可以通過OnErrorListener.onError()指定錯誤的回調函數。當在不正确的狀态下,調用某些方法時出錯,會調用onError,同時把狀态轉到Error狀态。
- Mediaplayer執行個體不在使用,要立即調用release釋放相關的資源。Release之後回到end狀态,它将不可再使用,也沒有任何方法可以讓它回到其他狀态。
- 當有Error發生,就會轉到Error狀态,從Error狀态恢複、在Error狀态下再次使用,就要用reset來把它恢複到Idle狀态。
- 一個很好的程式設計實踐是要注冊OnErrorListener,來提防來自内部播放引擎的錯誤提示。
- setDataSource會把狀态轉移到初始化狀态。
- Mediaplayer在回放started之前,必須先進入Prepared狀态。
- 有兩種方式可以進入到Prepared狀态,一種是同步的方式prepare(),一種是異步的方式prepareAsync()。Prepare()是同步的,對于檔案來說,這個調用時Ok的,它會blocks直到mediaplayer準備好了回放;prepareAsync()是一步的,對于流類型的資料源,應該調用這個,它會立即傳回,而不是blocking直到緩沖了足夠的資料,當調用傳回時它會先進入到Preparing狀态,然後内部的播放引擎會繼續完成剩下的準備工作,直到準備工作完成。當準備工作完成或者prepare()傳回,如果注冊了OnPreparedListener.OnPreapred()接口會被調用。在Prepared狀态,一些屬性才可以通過相關的方法調用。
- 調用start()開始回放,start()成功傳回,就會進入到stared狀态,isPlaying()可以被調用來檢查目前是否在started狀态。
- 在開始狀态,播放引擎會調用使用者提供的OnBufferingUpdateListener.onBufferingUpate()回調接口,當然前提是這個listener事先被注冊了。當操作音視訊流時,這個回調讓應用程式可以記錄跟蹤緩沖區的狀态。
- 通過pause(),讓回放進入到暫停狀态。
- 通過start(),可以恢複暫停狀态的回放,然後狀态回到Started狀态。
- Stop(),讓回放從started、Paused、Prepared、PlaybackCompleted狀态到Stopped狀态。一旦進入到Stopped狀态,隻有再次通過Prepare,進入到Prepared狀态才能繼續。
- 通過seekTo(long,int)調整播放位置。即使是異步調用,這個方法也是立即傳回的,尤其是在流類型的情況下,實際的seek操作可能需要花些時間完成。當實際的seek操作完成,播放引擎調用注冊的OnSeekComplete.onSeekComplete()回調。seekTo可以在prepared、Paused、playbackCompleted狀态調用,當在這些狀态調用時,如果這個流有視訊幀,并且請求的位置有效,相應的那幀視訊幀會被顯示。
- 可以通過getCurrentPosition來檢索目前的播放位置,這可以用于跟蹤播放進度。
- 當回放到達流末尾,回放結束。如果通過setlooping設定了循環,mediaplayer應該停留在Started狀态。如果回放是false,注冊的OnCompletion.onCompletion()的回調被調用,這個調用表示現在是播放完成狀态。
二,下面是怎樣寫一個媒體播放的應用。
MediaPlayer 是播放音頻和視訊的主要api。
AudioManager 管理裝置上的音頻源和音頻輸出通道。
Internet Permission,如果需要使用網絡上的流,請求通路網絡,需要添權重限
<uses-permission android:name=”android.permission.INTERNET”>
Wake Lock Permission,如果需要防止螢幕變暗,或者防止處理器休眠,或者使用MediaPlayer.setScreenOnWhilePlaying(),MediaPlayer.setWakeMode(),需要申請權限
<uses-permission android:name=”android.permission.WAKE_LOCK”>
MediaPlayer.java 可以擷取,解碼,播放音視訊,支援的資料源:本地資料,來自資料庫的URI,來自網絡的URL。
播放一個本地中繼資料(儲存在res/raw/目錄下的資源):
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file);
mediaPlayer.start();
這裡的raw資料源,是一個檔案,系統不需要對它做任何解析,但是檔案内容應該是有合适的編碼,并且是被支援的格式格式化過的媒體資料。
播放一個URL資源,來自于ContentResolver:
Uri myUri = ;
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();
播放來自網絡的資源:
String url = “http://”;
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare();//這個調用可能耗時,因為要緩沖…
mediaPlayer.start();
異步的Preparation。
Prepare()方法,可能執行較長時間,因為這可能涉及到擷取、解碼媒體資料,是以這個方法不應該在UI線程調用。相對的prepareAsync()可以在UI線程被調用,它在背景開始準備資料之後,立即傳回了。
看下這兩個方法的差別,java層沒有太多注意的地方,直接看c++層代碼:
mediaplayer.cpp
status_t MediaPlayer::prepareAsync()
{
ALOGV("prepareAsync");
Mutex::Autolock _l(mLock);
return prepareAsync_l();
}
status_t MediaPlayer::prepare()
{
ALOGV("prepare");
Mutex::Autolock _l(mLock);
mLockThreadId = getThreadId();
if (mPrepareSync) {
mLockThreadId = 0;
return -EALREADY;
}
mPrepareSync = true;
status_t ret = prepareAsync_l();
if (ret != NO_ERROR) {
mLockThreadId = 0;
return ret;
}
if (mPrepareSync) {
mSignal.wait(mLock); // wait for prepare done
mPrepareSync = false;
}
ALOGV("prepare complete - status=%d", mPrepareStatus);
mLockThreadId = 0;
return mPrepareStatus;
}
從以上源碼可以看出,這兩個方法都會調用prepareAsync_l,再往下調用播放引擎開始準備工作,差別是preapre()方法在調用prepareAsync_l之後,通過mSignal.wait(mLock)進入等待,喚醒的條件是prepare done,也即是收到MEDIA_PREPARED消息。
使用Service控制MediaPlayer。
如果希望app不在前台時,media可以在背景播放,需要啟動一個service來控制mediaplayer執行個體。
異步的運作:
預設情況,所有service中的工作都在一個線程,這點跟activity類似。在同一個應用裡面運作一個activity和一個service,他們預設是在同一個主線程中。是以,這種情況下,service需要快速的處理輸入事件,在傳回結果前,不應該做太多計算。如果有重量級的工作或者阻塞的調用,應該異步的來做:使用另外的線程,或者使用framework中的異步處理元件。
下面的例子,在主線程使用Mediaplayer,是以用prepareAsync,而不是prepare,同時應該實作MediaPlayer.onPreparedListener接口,以便在準備工作完成時被通知到。
Public class MyService extends Service implements MediaPlayer.OnPreparedListener {
Private static final String ACTION_PLAY= “com.example.action.PLAY”;
MediaPlay mMediaPlayer = null;
Public int onStartCommond(Intent intent, int flags, int startId){
If (intent.getAction().equals(ACTION_PLAY)){
mMediaPlayer = …//initialize it
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.prepareAsync();//這裡不會block。
}
}
//準備完成,會被調用
Public void onPrepared(MediaPlayer player){
Player.start();
}
}
處理異步操作的錯誤:
在同步調用時,錯誤會被正常的通知,同時帶有exception資訊,錯誤碼。但是,在使用異步資源時,要確定錯誤可以适當的通知到應用,可以通過注冊MediaPlayer.OnErrorListener來實作:
Public class MyService extends Service implements MediaPlayer.OnErrorListener{
MediaPlayer mMediaPlayer;
Public void initMediaPlayer(){
//initialize the mediaplayer
mMediaPlayer.setOnErrorListener(this);
}
@Override
Public Boolean onError(MediaPlayer mp, int what, in extra){
//mediaplayer moved to Error state.再次使用,必須reset。
}
}
使用喚醒鎖。
當回放音視訊時,不想讓系統休眠,可以使用wake locks,它可以給系統發個信号,表示應用正在使用系統功能,即使目前手機是idle狀态,也要保持可用。
在初始化MediaPlayer之後,可以通過setWakeMode來讓cpu保持運作,在playing期間,mediaplayer會持有一個特定鎖,在paused、stopped之後釋放這個鎖。
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManger.PARTIAL_WAKE_LOCK);
上面的方式,隻是讓cpu保持喚醒,如果需要使用網絡資源,就要使用wifi-lock來保持wifi可用。這時要手動申請、釋放。
在使用remote URL開始preparing時,建立、申請wifi-lock:
WifiLock wifiLock = ((WifiManager)getSystemService(Context. WIFI_SERVICE)).createWifiLock(WifiManager.WIFI_MODE_FULL, “myLock”);
WifiLock.acquire();
在pause、stop時,或不在需要網絡時,釋放:
wifiLock.release();
作為一個前台Servie來運作。
通常service是放到背景去執行,這種情況service被中斷然後又重新執行,使用者通常是意識不到的。但是,如果用service來播放音樂,任何中斷,都會被明顯的感受到。另外,使用者可能需要在service執行的過程中與其互動。這些情況下,service應該以foreground service來運作,前台service有更高的優先級,系統盡量不去kill它。當運作與前台時,service必須提供一個狀态欄提示,以確定使用者可以意識到這個運作的service,允許使用者跟這個service互動。
在建立一個Notification後,調用startForeground使service運作與前台:
String songName;
PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,
New Intent(getApplicationContext(),MainActivity.class),
PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification();
Notification.tickerText = text;
Notification.icon = R.drawable.play0;
Notification.flags |= Notificaiton.FLAG.ONGOING_EVENT;
Notification.setLatestEventInfo(getApplicationContext(), “MusicPlayerSample”,
“Playing:”+songName, pi);
StartForeground(NOTIFICATION_ID, notificaiton);
當點選狀态欄的這個notification時,系統會調用PendingIntent,來啟動指定的Activity。
不在需要這個service時,記得調用:
stopForeground(true);
處理音頻焦點。
Android是一個多任務的環境,這對于使用audio的app是一個挑戰,因為隻有一個音頻輸出通道,會有多個媒體服務競争它的使用。從android2.2開始,平台提供了一種方式來交涉裝置的音頻輸出通道的使用,這個機制就是Audio Focus。
當app需要輸出music或者notification這樣的音頻時,需要去請求audio focus,一旦擷取到focus,就可以自由的使用音頻的輸出通道,但是要保持對focus changes的監聽,一旦被通知失去了焦點,應該立即kill這個audio或者把它轉為安靜的級别(也叫ducking),再次得到焦點時,恢複正常的回放。
音頻焦點本質上是協作的方式,也就是說,建議app去遵從音頻焦點的準則,但是這個規則不會被系統強制執行。如果app在失去焦點後,仍然要大聲的播放music,系統是不會做什麼來阻止這個行為。但是,使用者的體驗會較差。
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.
}
requestFocus()的第一個參數是AudioManager.OnAudioFocusChangeListener,在audio focus有變化時,onAudioFocusChange()會被調用,在service和activity中最好實作這個接口。
Class MyService extends Servie implements AudioManager.OnAudioFocusChangeListener {
Public void onAudioFocusChange(int focusChange){
//
}
}
其中的參數focusChange,值是AudioManager中定義的一些常量:
AUDIOFOCUS_GAIN:已經擷取了audio focus。
AUDIOFOCUS_LOSS:你會失去audio focus一段時間,必須停止所有的音頻回放,因為你可能一段時間不會重新擷取到焦點,盡可能在這個時候去清理資源,比如釋放掉MediaPlayer執行個體。
AUDIOFOCUS_LOSS_TRANSIENT:短暫的失去焦點,很快會再次擷取到,也應該停止掉所有的音頻回放,但是可以保留所有的資源,因為可能很快再次擷取焦點。
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:臨時地失去焦點,但是允許繼續播放音頻(以較低的聲音播放),而不是完全停止掉audio。
Public void onAudioFocusChange(int focusChange){
Switch (focusChange) {
Case AudioManager.AUDIOFOCUS_GAIN:
//resume playback
If (mMediaPlayer == null) initMediaPlayer();
Else if (!mMediaPlayer != null) mMediaPlayer.start();
mMediaPlayer.setVolume(1.0f, 1.0f);
break;
case AduioManager.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(0.1f, 0.1f);
break;
}
}
AudioFocus這個api要在Android2.2之後使用,如果要向前相容,可以通過反射機制調用AudioFocus的方法,或者通過一個單獨的類實作所有audio focus 的特性,如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
}
}
如果系統運作在api8之前,可以建立AudioFocusHelper執行個體。
If (android.os.Build.VERSION.SDK_INT >= 8)
MAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
Else
mAudioFocusHelper = null;
執行清理。
MediaPlayer執行個體,會消耗一些重要的系統資源,是以應該隻在需要的時間内保留,在完成後調用release();明确的調用清除方法,比依賴系統的垃圾回收更重要,因為垃圾回收機制在回收mediaplayer之前會花些時間,因為他隻對記憶體需求敏感,而對media相關的資源缺乏不敏感。是以,重寫onDestory方法時有必要的。
Public class MyService extends Servie {
@Override
Public void onDestory(){
If (mMediaPlayer 1= null) mMediaPlayer.release();
}
}
當然,除了在關閉時釋放,也應該尋找其他的機會釋放mediaplayer。
處理AUDIO_BECOMING_NOISY intent。
優秀的app,當會讓聲音變成噪音的事件發生時(比如通過外放輸出),應該要自動停止回放。比如當用耳機聽音樂時,耳機突然跟裝置斷開了,不管怎樣,這個行為不是自動發生的,如果沒有實作這個特性,那麼聲音會通過外放輸出,這個情況可能不是使用者想要的。
通過在manifest注冊這個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 onReceiver(Context ctx, Intent intent){
If (intent.gatAction().equals(
Android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
//single to stop playback
}
}
}
從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.
} else if (!cursor.moveFirst()) {
//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);
}while (cursor.moveToNext())
}
Long id = //retrieve it from somewhere;
Uri contentUri = ContentUris.withAppendedId(
Android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
mMediaPlayer.setDataSource(getApplicationContext(), contentUri);