天天看點

MediaPlay Api的使用

MediaPlay 用來控制音頻、視訊檔案和流的回放。

  • 狀态圖
    MediaPlay Api的使用
  • 回放控制是通過一個狀态機來管理的。

    橢圓圖代表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);