[譯]Android 多媒體播放

Android 多媒體框架包含了支持播放的一系列常見多媒體類型配并,以此可以很容易地整合諸如音頻、視頻高镐、圖片到你的應用程序溉旋。資源文件、本地和網絡中的視頻嫉髓、音頻都可以通過Media Player播放观腊。

本文展示如何寫一個高性能并且體驗良好的多媒體播放應用。

基礎知識

下邊是兩個Android中用來播放聲音岩喷、視頻的類

  • Media Player
    提供播放聲音恕沫、視頻的API监憎。
  • AudioManager
    管理設備上的音頻源和音頻輸出

Manifest 聲明

使用Media Player 開發(fā)之前纱意,確定已經在清單文件Manifest 的正確位置聲明了所需要的權限。

  1. 如果需要播放網絡數(shù)據(jù)鲸阔,需要聲明網絡權限
    <uses-permission android:name="android.permission.INTERNET" />
  2. 如果需要屏幕常亮偷霉,需要聲明<uses-permission android:name="android.permission.WAKE_LOCK" />

使用Media Player

Media Player 是多媒體框架中的一個重要組件。這個類的實例可以通過最少的設置獲取褐筛、解碼以及播發(fā)音視頻类少,支持下面集中不同的播放源:

  1. 本地資源
  2. 內部URI
  3. 外部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...
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市篷帅,隨后出現(xiàn)的幾起案子史侣,更是在濱河造成了極大的恐慌,老刑警劉巖魏身,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惊橱,死亡現(xiàn)場離奇詭異,居然都是意外死亡箭昵,警方通過查閱死者的電腦和手機税朴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來家制,“玉大人正林,你說我怎么就攤上這事〔梗” “怎么了觅廓?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長诅病。 經常有香客問我哪亿,道長,這世上最難降的妖魔是什么贤笆? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任蝇棉,我火速辦了婚禮,結果婚禮上芥永,老公的妹妹穿的比我還像新娘篡殷。我一直安慰自己,他們只是感情好埋涧,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布板辽。 她就那樣靜靜地躺著奇瘦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪劲弦。 梳的紋絲不亂的頭發(fā)上耳标,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機與錄音邑跪,去河邊找鬼次坡。 笑死,一個胖子當著我的面吹牛画畅,可吹牛的內容都是我干的砸琅。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼轴踱,長吁一口氣:“原來是場噩夢啊……” “哼症脂!你這毒婦竟也來了?” 一聲冷哼從身側響起淫僻,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤诱篷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后嘁傀,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兴蒸,經...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年细办,在試婚紗的時候發(fā)現(xiàn)自己被綠了橙凳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡笑撞,死狀恐怖岛啸,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情茴肥,我是刑警寧澤坚踩,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站瓤狐,受9級特大地震影響瞬铸,放射性物質發(fā)生泄漏。R本人自食惡果不足惜础锐,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一嗓节、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸淆党。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碳抄。三九已至草添,卻和暖如春酪劫,著一層夾襖步出監(jiān)牢的瞬間豆瘫,已是汗流浹背珊蟀。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留靡羡,地道東北人系洛。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓俊性,卻偏偏與公主長得像略步,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子定页,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,747評論 25 707
  • Media Playback Android多媒體框架包涵了對播放多種通用媒體的類型的支持趟薄,所以你可以很容易的集成...
    VegetableAD閱讀 872評論 0 0
  • ¥開啟¥ 【iAPP實現(xiàn)進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,365評論 0 17
  • 你浪了很多年典徊, 談了很多場戀愛 他們會說喜歡你杭煎, 甚至愛你, 卻沒有人真正理解你懂你卒落, 所以你學會了可有可無的陪伴...
    南方白燭閱讀 329評論 0 2
  • 現(xiàn)在的姑娘們都不怎么看一個男人是否真的愛她儡毕,只想著她愛上了個男人就毫不猶豫的撲上了也切。 如果一個男人做到了一下幾點說...
    故留情閱讀 299評論 0 0