天天看點

多媒體播放Media Playback基礎Manifest使用MediaPlayer異步準備管理狀态釋放 MediaPlayer使用Service播放多媒體使用wake locks作為前台Service運作操作音頻焦點(Handling audio focus)清理處理AUDIO_BECOMING_NOISY Intent通過Content Resolver擷取多媒體資源

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 類的文檔展示了一個完整的狀态圖,如下:

多媒體播放Media Playback基礎Manifest使用MediaPlayer異步準備管理狀态釋放 MediaPlayer使用Service播放多媒體使用wake locks作為前台Service運作操作音頻焦點(Handling audio focus)清理處理AUDIO_BECOMING_NOISY Intent通過Content Resolver擷取多媒體資源

當你建立一個 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)。

下圖展示了通知是如何顯示的:

多媒體播放Media Playback基礎Manifest使用MediaPlayer異步準備管理狀态釋放 MediaPlayer使用Service播放多媒體使用wake locks作為前台Service運作操作音頻焦點(Handling audio focus)清理處理AUDIO_BECOMING_NOISY Intent通過Content Resolver擷取多媒體資源

隻有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...