Manage Audio

Media Playback

Android多媒體框架包涵了對播放多種通用媒體的類型的支持,所以你可以很容易的集成音頻,視頻和圖像到你的應用中。你可以使用MediaPlayer APIs播放保存在應用raw資源中單獨的媒體文件,或者通過網絡連接下載的數據流

The Basics

以下的類在Android框架中被用于播放聲音和視頻

MediaPlayer 這個類主要用于播放聲音和視頻
AudioManager 這個類管理設備上的音頻源和音頻輸出
Manifest Declarations

在使用MediaPlayer開發你的應用前,確定你的應用聲明了允許使用相關的特性

Internet Permission

如果你使用MediaPlayer去播放網絡的內容,你的應用必須請求網絡權限

<uses-permission android:name="android.permission.INTERNET" />
Wake Lock Permission

如果你的播放器應用程序需要保持屏幕變暗或處理器睡眠,要么使用MediaPlayer.setScreenOnWhilePlaying()方法,要么使用MediaPlayer.setWakeMode()方法,但是你必須請求這個權限

<uses-permission android:name="android.permission.WAKE_LOCK" />

Using MediaPlayer

media框架最重要的類之一就是MediaPlayer類。這個類的對象可以使用最少步驟獲取,解析,播放音頻和視頻。它支持幾個不同的媒體源。
比如:
本地資源
網絡URIs,比如你從Content Resolver獲取到的
外部URLs(流)
具體android支持的媒體格式,請查看文檔
以下例子演示了怎么播放一個本地raw資源中可用的音頻

MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // no need to call prepare(); create() does that for you

以下是從本地播放可用URI(從Content Resolver獲取到的)

Uri myUri = ....; // initialize Uri here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();

以下是通過HTTP遠程播放遠程的流

String url = "http://........";
 // your URL hereMediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // might take long! (for buffering, etc)
mediaPlayer.start();

注意:當你使用setDataSource時,你必須處理 IllegalArgumentException或IOException,因為你索引的文件可能不存在

Asynchronous Preparation

MediaPlayer原則上是可以直接使用的,但是有一些要點是需要記住的,比如,prepare()的調用可能會執行很長時間,因為它涉及到提取,解析媒體數據,所以,像其他需要長時間運行的方法,你應該避免在UI線程中調用。因為這樣做會使UI線程掛起直到方法返回,這是非常不好的用戶體驗并且可能導致ANR錯誤。即使你預計你的資源會加載的很快,超多1/10秒才回應就會導致卡頓,這會給用戶留下你的app很慢的映像
為了避免UI線程掛起,創建一個新的線程去prepare MediaPlayer并在完成時通知主線程,框架提供了一個便捷的方法prepareAsync()去完成這個任務,這個方法在后臺prepare音頻并在完成時立馬返回,當音頻完成preparing時,將調用通過setOnPreparedListener()配置的MediaPlayer.OnPreparedListener的onPrepared()方法。

Managing State

從MediaPlayer的另一個方面來說它是基于狀態的。也就是說,MediaPlayer有一個內部狀態,你在編寫代碼時必須始終注意,因為某些操作只有在播放器處于特定狀態時才有效。如果在錯誤狀態下執行操作,系統可能會拋出異常或導致其他不需要的行為。

MediaPlayer類中的文檔顯示了一個完整的狀態圖,說明了哪些方法將MediaPlayer從一個狀態移動到另一個狀態。例如,當您創建一個新的MediaPlayer時,它處于空閑狀態。此時,您應該通過調用setDataSource()初始化它,使其處于Initialized狀態。之后,您必須使用prepare()或prepareAsync()方法來準備它。當MediaPlayer完成準備時,它將進入準備狀態,這意味著您可以調用start()使其播放媒體。此時,如圖所示,您可以通過調用start(),pause()和seekTo()等方法在Started,Paused和PlaybackCompleted狀態之間切換。但是,當調用stop()時,請注意,在再次準備MediaPlayer之前,不能再次調用start()(從狀態圖中看出需要調用prepareAsync)。

當編寫與MediaPlayer對象交互的代碼時,始終記得狀態圖,因為從錯誤狀態調用其方法是錯誤的常見原因。

Releasing the MediaPlayer

MediaPlayer會消耗有限的系統資源,所以,你應該預防在不需要的時候還持有Mediaplayer的實例,當你完成你的任務時,你應該調用release()去釋放任何分配給你的資源,例如,如果你正在使用MediaPlayer但是你的Activity的onStop方法被回調了,那么你應該立即釋放MediaPlayer,因為當你的活動沒有與用戶交互時(除非你在后臺播放媒體,這將在下一節討論),保持它是沒有意義的。當你的Activity恢復或者重新啟動了,你需要創建一個新的MediaPlayer并重新prapare

mediaPlayer.release();
mediaPlayer = null;

例如,如果你在活動停止時忘記釋放MediaPlayer,但在活動再次啟動時創建一個新的MediaPlayer,可能會發生的問題。我們都知道,當用戶更改屏幕方向(或以其他方式更改設備配置)時,系統會通過重新啟動活動(默認情況下)來處理,因此當設備在縱向和橫向之間來回切換時會快速消耗所有系統資源,因為在每個方向更改時,你將創建一個新的MediaPlayer,但你從不釋放。 (有關運行時重新啟動的詳細信息,請參閱處理運行時更改。)

你可能想知道,如果你想繼續播放“后臺媒體”,即使用戶離開你的活動,很大程度上與內置音樂應用程序的行為相同的方式。 在這種情況下,你需要的是由服務控制的MediaPlayer,如在使用MediaPlayer中的服務中所述。

Using a Service with MediaPlayer

如果你希望你的媒體在后臺播放,即使你的應用程序沒有運行在當前屏幕上,也就是說,你希望你的媒體在用戶與其他應用程序交互時繼續播放 - 那么你必須啟動一個服務并在服務中控制MediaPlayer實例。 你應該小心這個設置,因為用戶和系統都期望運行后臺服務的應用程序如何與系統的其余部分交互。 如果你的應用程序不滿足這些期望,用戶可能會有不好的體驗。 本節介紹了你應該注意的主要問題,并提供有關如何處理這些問題的建議。

Running asynchronously

首先,像Activity一樣,默認情況下服務中的所有工作都是在單個線程中完成的 - 事實上,如果你在同一個應用程序運行中一個活動和一個服務,默認他們在同一個線程(“主線程 “)中。 因此,服務需要快速處理傳入意圖,并且在響應它們時不執行冗長的計算。 如果有任何繁重的工作或阻塞調用,你必須異步地執行這些任務:從你自己實現的另一個線程,或使用框架的許多設施進行異步處理。
例如,當你在主線程中使用MediaPlayer,你應該調用prepareAsync()方法而不是prepare()方法,實現MediaPlayer.OnPreparedListener 是為了在preparation完成時可以被通知并且你可以開始播放。例如:

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 = ... // 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();
    }
}
Handling asynchronous errors

在同步操作中,通常會使用異?;蝈e誤代碼來報告錯誤,但是當使用異步資源時,你應該確保你的應用正確地收到錯誤通知。 在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!
    }
}

重要的是記住當一個錯誤發生時,MediaPlayer會切換到錯誤狀態(完整狀態圖的MediaPlayer類的文檔),你必須重置它,然后才能再次使用它。

Using wake locks

當設計在后臺播放媒體的應用程序時,設備可能會在服務運行時進入睡眠狀態。 由于Android系統嘗試在設備休眠時節省電池,因此系統會嘗試關閉手機的任何不需要的功能,包括CPU和WiFi硬件。 但是,如果你的服務正在播放或流式傳輸音樂,則希望系統不要干擾你的播放。

為了確保你的服務在這些條件下繼續運行,你必須使用“喚醒鎖”。 喚醒鎖是通知系統你的應用程序正在使用一些功能,系統應該保持可用,即使設備處于空閑狀態。

注意:你應始終謹慎使用喚醒鎖,并且只在真正必要的時間內保持它們,因為它們會顯著縮短設備的電池壽命。

要確保CPU在播放MediaPlayer時繼續運行,請在初始化MediaPlayer時調用setWakeMode()方法。 一旦完成,MediaPlayer在播放時保持指定的鎖定,并在暫停或停止時釋放鎖定:

mMediaPlayer = new MediaPlayer();
// ... other initialization here ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

但是,在此示例中獲取的喚醒鎖僅保證CPU保持喚醒。 如果你通過網絡流傳輸媒體,并且使用的是Wi-Fi,則你可能還需要持有一個WifiLock,你必須手動獲取和釋放。 因此,當你開始使用遠程URL準備MediaPlayer時,應創建并獲取Wi-Fi鎖。 例如:

WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))    .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");
wifiLock.acquire();

當你暫停或停止媒體,或不再需要網絡時,應釋放鎖定:

wifiLock.release();
Running as a foreground service

服務通常用于執行后臺任務,諸如獲取電子郵件,同步數據,下載內容以及其它可能性。在這些情況下,用戶沒有主動地了解服務的執行,并且可能不會注意到這些服務是否被中斷并且稍后重新啟動。

考慮正在播放音樂的服務。這種服務用戶可以清晰的意識到它的活動,任何中斷的都會嚴重影響體驗。此外,在服務執行期間用戶可能希望與之交互。在這種情況下,服務應作為“前臺服務”運行。前臺服務在系統中重要性等級更高 - 系統幾乎不會殺死服務,因為它對用戶是直接重要的。當在前臺運行時,服務還必須提供狀態欄通知,以確保用戶知道正在運行的服務并允許他們打開可與服務交互的Activity。

為了將你的服務切換到前臺,你必須為狀態欄創建通知,并且為服務調用startForeground。示例:

String songName;
// assign the song name to 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 |= Notification.FLAG_ONGOING_EVENT;
notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample","Playing: " + songName, pi);
startForeground(NOTIFICATION_ID, notification);

當你的服務在前臺運行時,你配置的通知在設備的通知區域中可見。 如果用戶選擇通知,系統會調用你提供的PendingIntent。 在上面的示例中,它打開一個Activity(MainActivity)。

你應該只保持“前臺服務”狀態,而你的服務實際執行用戶主動感知的東西。 一旦這不再是true,你應該通過調用stopForeground()釋放它:

stopForeground(true);
Handling audio focus

Android是一個多任務環境,但在同一時間中只有一個活動可以在當前屏幕運行。這對使用音頻的應用提出了特別的挑戰,因為只有一個音頻輸出,并且可能存在若干媒體服務競爭其使用。在Android 2.2之前,沒有內置的機制來解決這個問題,這在某些情況下可能會導致糟糕的用戶體驗。例如,當用戶正在聽音樂而此時另一應用需要向用戶通知非常重要的事情時,由于大聲的音樂,用戶可能聽不到通知鈴聲。從Android 2.2開始,平臺為應用程序提供了一種方式,協商其使用設備的音頻輸出。這種機制稱為音頻焦點。

當你的應用程序需要輸出音頻(如音樂或通知)時,應始終請求音頻焦點。一旦它有焦點,它可以自由地使用聲音輸出,但它應該總是監聽焦點的更改。如果它被通知已經失去了音頻焦點,它應該立即殺死音頻或將音量降低到安靜的級別(稱為“ducking” - 有一個標志,指示哪一個是適當的),并且只有再次接收焦點才能恢復高聲播放。

音頻焦點應該是合作的。也就是說,應用程序需要(并高度鼓勵)遵守音頻焦點指南,但規則不是由系統強制執行。如果應用程序想要播放大聲的音樂,即使失去音頻焦點,系統中的任何東西都不會阻止。然而,用戶更可能具有不良體驗,并且將更可能卸載行為不當的應用程序

要請求音頻焦點,您必須從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.
}

requestAudioFocus()的第一個參數是AudioManager.OnAudioFocusChangeListener,只要音頻焦點發生變化,就會調用onAudioFocusChange()方法。 因此,你還應該為你的服務和Activity實現此接口。 例如:

class MyService extends Service implements AudioManager.OnAudioFocusChangeListener {
    // ....
    public void onAudioFocusChange(int focusChange) {
        // Do something based on focus change...
    }
}

focusChange參數告訴你音頻焦點是如何改變的,并且可以是以下值之一(它們都是在AudioManager中定義的常量):

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;
    }
}

請注意,音頻焦點API僅適用于API級別8(Android 2.2)及更高版本,因此如果你要支持以前的Android版本,則應采用向后兼容性策略,以便可以使用此功能(如果有)

你可以通過反射調用音頻焦點方法或通過在單獨的類(例如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級別為8或更高時,才能創建AudioFocusHelper類的實例

if (android.os.Build.VERSION.SDK_INT >= 8) {
    mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
} else {
    mAudioFocusHelper = null;
}
Performing cleanup

如前所述,MediaPlayer對象會消耗大量的系統資源,因此你應該在你需要時保留它,并在完成后調用release()。 顯示的調用這個清除方法,而不是依賴系統垃圾收集器時很重要的,因為在垃圾收集器回收MediaPlayer前它可能需要花一些時間,因為它只對內存需求敏感,而不是缺乏其他媒體相關資源。 因此,在使用服務的情況下,您應該總是覆蓋onDestroy()方法,以確保你釋放MediaPlayer:

public class MyService extends Service {
   MediaPlayer mMediaPlayer;
   // ...
   @Override
   public void onDestroy() {
       if (mMediaPlayer != null) mMediaPlayer.release();
   }
}

除了在關閉時釋放MediaPlayer以外,你也應該總是尋找其他機會釋放你的MediaPlayer。 例如,如果你預計不能長時間播放媒體(例如,失去音頻焦點后),你應該確保會釋放您現有的MediaPlayer并稍后重新創建。 另一方面,如果你只希望短時間停止播放,那么你應該保存MediaPlayer,以避免創建和準備再次的開銷。

Handling the AUDIO_BECOMING_NOISY Intent

許多精心編寫的音頻播放程序會在發生導致音頻外放(通過外部揚聲器輸出)的事件時自動停止播放。 例如,可能發生當用戶通過耳機收聽音樂并意外地將耳機從設備拔掉這種情況。 但是,這種行為不會自動發生。 如果你不實現此功能,音頻會從設備的外部揚聲器播放,這可能不是用戶想要的。

您可以通過處理ACTION_AUDIO_BECOMING_NOISY意圖,確保你的應用在這些情況下停止播放音樂,您可以通過向清單添加以下內容來注冊接收者:

<receiver android:name=".MusicIntentReceiver">
   <intent-filter>
      <action android:name="android.media.AUDIO_BECOMING_NOISY" />
   </intent-filter>
</receiver>

為ACTION_AUDIO_BECOMING_NOISY的intent注冊MusicIntentReceiver類為廣播接收器

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)
      }
   }
}
Retrieving Media from a Content Resolver

對一個音頻播放器app有用的另一個特性是可以在用戶的設備上通過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) {
    // 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());
}
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...

Manage Audio Playback

1.Controlling Your App’s Volume and Playback

良好的用戶體驗是可以預測的。如果你的app可以播放media,那么你的用戶可以使用他們設備,藍牙耳機,頭戴耳機的硬件或者軟件去控制你的app的音量是很重要的。
同樣,通過你的app他們可以適當使用play,stop,pause,skip,和previous媒體播放鍵在音頻流上做出各自的行為

Identify Which Audio Stream to Use

創建可預測音頻體驗的第一步是知道你的app將要使用的音頻流,Android支持單獨播放音樂,鬧鐘,通知,來電鈴聲,系統聲音,呼叫中的聲音,DTMF音。這主要是為了允許用戶獨立地控制每個流的音量。大部分音頻流僅限于系統事件,所以除非你app替代鬧鐘,不然,你基本上都是使用STREAM_MUSIC播放你的音頻

默認情況下,使用物理音量鍵區控制你app的音量,按下音量控制鍵調整正在播放的音頻流的音量。如果你的app當前沒有播放任何音頻流,那么按下音量鍵調整的將是鈴聲的音量

如果你開發的是游戲或者音樂app,那么即使他們處于在歌曲切換時或游戲的當前時間點沒有音樂,當用戶按下音量鍵也表示他們想要控制游戲或者音樂的音量。

當你的音頻流調整聲音的時候你可能想要監聽音量鍵的按下事件。Android提供了便利的方法setVolumeControlStream(int)去直接監聽你指定的音頻流的音量鍵的按下事件

確定你的應用將使用音頻流,你因該設置方法到目標音頻流上。該方法你應該在應用生命周期的早期調用,因為在Activity的生命周期你只需要調用一次,典型的,你應該在控制你音頻流的Activity或者Fragment的onCreate方法中調用,這確保了一旦你的app可見,音量控制功能也如用戶期望的可用

setVolumeControlStream(AudioManager.STREAM_MUSIC);
從這時起,當目標Activity或者Fragment可見的時候,在設備上按下音量鍵會影響你指定的音頻流。使用物理播放鍵去控制你的App音頻的播放

Use Hardware Playback Control Keys to Control Your App’s Audio Playback

當用戶按下這些硬件按鈕(比如:耳機,許多已連接或者無線連接的頭戴耳機的播放,暫停,停止,下一首,上一首),系統會廣播一個帶ACTION_MEDIA_BUTTON行為的Intent,為了回應音頻設備按鈕的點擊,你需要在你的manifest中注冊一個BroadcastReceiver去監聽這個行為的廣播,如以下示例代碼

<receiver android:name=".RemoteControlReceiver">
   <intent-filter>
       <action android:name="android.intent.action.MEDIA_BUTTON" />
   </intent-filter>
</receiver>

接收器實現本身需要提取哪個鍵被按下來引起的廣播,Intent中的EXTRA_KEY_EVENT鍵下包括此鍵,而KeyEvent類中包括一系列表示媒體按鈕的靜態常量KEYCODE_MEDIA_ *列表,例如KEYCODE_MEDIA_PLAY_PAUSE和KEYCODE_MEDIA_NEXT。以下片段顯示如何提取按下的媒體按鈕,并相應地影響媒體播放。

public class RemoteControlReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
            KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
            if (KeyEvent.KEYCODE_MEDIA_PLAY == event.getKeyCode()) {
                // Handle key press.
            }
        }
    }
}

因為可能多個應用想要監聽media按鈕的按下,所以你還必須通過程序控制應用程序何時應該接收媒體按鈕按下事件。
以下代碼可在你的應用程序中使用AudioManager注冊和注銷媒體按鈕事件接收器。 注冊后,你的廣播接收器是所有media按鈕廣播的專屬接收器。

AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
...// Start listening for button
 pressesam.registerMediaButtonEventReceiver(RemoteControlReceiver);
...// Stop listening for button
 pressesam.unregisterMediaButtonEventReceiver(RemoteControlReceiver);

通常,apps應當在它變得非活動或者被可見的時候注銷它們的接收器(比如onStop回調中),然而,對于媒體播放應用程序來說并不簡單,事實上,當應用程序不可見且無法通過屏幕上的UI控制時,響應媒體播放按鈕是最重要的。更好的方法是在應用程序獲取音頻焦點時注冊media按鈕事件接收器,丟失音頻焦點時注銷media按鈕事件接收器。

2.Managing Audio Focus

有多個應用程序可能播放音頻,重要的是要考慮他們應該如何交互。 為了避免每個音樂應用程序同時播放,Android使用音頻焦點來控制音頻播放 - 只有擁有音頻焦點的應用程序才能播放音頻。

在您的應用程序開始播放音頻之前,應該請求并接收音頻焦點。 同樣,它應該知道如何監聽音頻焦點的丟失,并在發生這種情況時適當地做出反應。

Request the Audio Focus

在你的app開始播放任何音頻前,app應該為將要播放的流獲取到音頻焦點。這是通過調用requestAudioFocus()完成的,如果您的請求成功,它返回AUDIOFOCUS_REQUEST_GRANTED。

你必須指定你要使用的流,以及是需要暫時還是永久的音頻焦點。 當你希望只在短時間內播放音頻時則請求暫時聚焦(例如在播放導航指示時)。 當您計劃在可預見的未來播放音頻時則請求永久音頻聚焦(例如,在播放音樂時)。

以下片段會為音樂音頻流請求的永久音頻焦點。 你應該在開始播放之前立即請求音頻焦點,例如當用戶按下播放或下一個游戲關卡的背景音樂開始前。

AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
...// Request audio focus for playback
int result = am.requestAudioFocus(afChangeListener,
                                 // Use the music stream.
                                 AudioManager.STREAM_MUSIC,
                                 // Request permanent focus.
                                 AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    am.registerMediaButtonEventReceiver(RemoteControlReceiver);
    // Start playback.
}

一旦你完成播放,請確定調用了abandonAudioFocus()方法,這將通知系統你不在需要焦點并注銷相關聯的AudioManager.OnAudioFocusChangeListener。在放棄暫時焦點的情況下,這允許任何被中斷的app繼續播放

// Abandon audio focus when playback
 completeam.abandonAudioFocus(afChangeListener);

當請求暫時音頻焦點時,您有一個額外選項:是否要啟用“低音”(“Ducking”)。 通常,當優秀的音頻應用程序失去音頻焦點時,它會立即停止它的播放。 通過請求一個暫時音頻焦點,你告訴其他音頻應用程序你可以接受他們繼續播放,只要他們降低音量,直到焦點回到他們。

// Request audio focus for playback
int result = am.requestAudioFocus(afChangeListener,
                             // Use the music stream.
                             AudioManager.STREAM_MUSIC,
                             // Request permanent focus.
                             AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback.
}

低音尤其適用于間歇性使用音頻流的應用程序,例如用于可聽見的駕駛方向。

每當另一個應用程序像之前描述的那樣請求音頻焦點,你在請求焦點時注冊的偵聽器就會接收另一個應用選擇的永久或暫時的音頻焦點。

Handle the Loss of Audio Focus

如果你的應用程序可以請求音頻焦點,那么當其他應用程序請求焦點時,它會轉而失去焦點。 你的應用程序如何響應音頻焦點的丟失取決于丟失的方式。

音頻焦點改變監聽器的回調方法onAudioFocusChange()有一個參數它描述了焦點改變事件。具體來說,可能的焦點丟失事件來自前焦點請求類型一部分 - 永久丟失,暫時丟失和暫時被允許的ducking。

如果音頻焦點永久的丟失,假如另一個應用正在使用音頻,那么你的app應該快速的結束音頻的使用,實際上,這意味著停止播放,移除音頻按鈕監聽器-允許新的音頻播放器去單獨處理這些事件-并丟棄你的音頻焦點。這時,你將期待一個用戶行為(在你的app中按下播放)去請求音頻焦點在你恢復音頻播放前

在以下的代碼片段中,我們暫停播放或者我們的音頻播放器暫時丟失了焦點,那么當我們獲取焦點時恢復它。如果是永久丟失,那么注銷我們的音頻按鈕事件接收器并停止監聽音頻焦點改變

AudioManager.OnAudioFocusChangeListener afChangeListener =    new AudioManager.OnAudioFocusChangeListener() {
        public void onAudioFocusChange(int focusChange) {
            if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT) {
                // Pause playback
            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                // Resume playback
            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                am.unregisterMediaButtonEventReceiver(RemoteControlReceiver);
                am.abandonAudioFocus(afChangeListener);
                // Stop playback
            }
        }
    };

在ducking被允許的情況下暫時丟失音頻焦點,相對于暫停播放,你可以使用duck代替

Duck!

低音是降低音頻流輸出音量的過程,使來自另一個應用程序暫時的音頻更容易聽到,而不會完全中斷你自己的應用程序的音頻。

在以下代碼段中,當我們暫時失去音頻焦點時,降低媒體播放器對象上的音量,然后在我們重新獲得焦點時將其返回到原來的音量。

OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
    public void onAudioFocusChange(int focusChange) {
        if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
            // Lower the volume
        } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
            // Raise it back to normal
        }
    }
};

3.Dealing With Audio Output Hardware

當用戶從他們的Android設備享受音頻時,用戶有很多選擇。 大多數設備都有內置揚聲器,有線耳機的耳機插孔,許多設備還具有藍牙連接和支持A2DP音頻。

Check What Hardware is Being Used

你的應用程序的行為可能會受到路由到的硬件輸出的影響。

您可以查詢AudioManager以確定音頻當前是否路由到設備揚聲器,有線耳機或連接的藍牙設備,如以下代碼段所示:

if (isBluetoothA2dpOn()) {
    // Adjust output for Bluetooth.
} else if (isSpeakerphoneOn()) {
    // Adjust output for Speakerphone.
} else if (isWiredHeadsetOn()) {
    // Adjust output for headsets
} else {
    // If audio plays and noone can hear it, is it still playing?
}
Handle Changes in the Audio Output Hardware

當耳機拔掉或藍牙設備斷開連接時,音頻流自動重新路由到內置揚聲器。 如果你使用最大音量聽你的音樂,這可能是一個噪音。

幸運的是,當發生這種情況時,系統廣播ACTION_AUDIO_BECOMING_NOISY的Intent。 當你在播放音頻時注冊一個BroadcastReceiver監聽這個意圖是一個好習慣。 在音樂播放器的情況下,用戶通常期望暫停播放,而對于游戲,可以選擇顯著降低音量。

private class NoisyAudioStreamReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
            // Pause the playback
        }
    }
}
private IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);

private void startPlayback() {
    registerReceiver(myNoisyAudioStreamReceiver(), intentFilter);
}

private void stopPlayback() {
    unregisterReceiver(myNoisyAudioStreamReceiver);
}

小實現

public class UseAudioActivity extends AppCompatActivity implements AudioManager.OnAudioFocusChangeListener {
    private Button playAudio,pauseAudio,stopAudio;
    private MediaPlayer mp;
    private AudioManager audioManager;
    private NoisyBroadCastReceiver receiver;
    private boolean isPrepared;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_use_audio);
        playAudio = (Button) findViewById(R.id.play_audio);
        pauseAudio = (Button) findViewById(R.id.pause_audio);
        stopAudio = (Button) findViewById(R.id.stop_audio);
        playAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mp != null && isPrepared){
                    mp.start();
                }else{
                    requestAudioFocus();
                }
            }
        });
        pauseAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mp != null){
                    mp.pause();
                }
            }
        });
        stopAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mp != null){
                    mp.stop();
                    //if you do this,please prepare again
                }
            }
        });
        requestAudioFocus();
        IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
        receiver = new NoisyBroadCastReceiver();
        this.registerReceiver(receiver,filter);
    }
    public void requestAudioFocus(){
        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.
            Toast.makeText(this,"can't play audio now,try later",Toast.LENGTH_SHORT).show();
        }else {
            initMediaPlayer();
        }
    }
    @Override
    public void onAudioFocusChange(int focusChange) {
        Log.e("Audio","is change focus " + focusChange);
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                // resume playback
                if (mp == null){
                    initMediaPlayer();
                } else if (!mp.isPlaying()) {
                    mp.start();
                    mp.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 (mp != null) {
                    if (mp.isPlaying()) mp.stop();
                    mp.release();
                    mp = 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 (mp != null) {
                    if (mp.isPlaying()) mp.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 (mp != null) {
                    if (mp.isPlaying()) mp.setVolume(0.1f, 0.1f);
                }
                break;
        }
    }
    public void initMediaPlayer(){
        try {
            mp = new MediaPlayer();
            mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
            AssetFileDescriptor afd = this.getResources().openRawResourceFd(R.raw.neighborhood);
            if (afd == null){
                throw new Exception("res is not found");
            }
            mp.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
            afd.close();
            setPrepareListener();
            mp.prepareAsync();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void setPrepareListener(){
        if (mp != null){
            mp.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    isPrepared = true;
                }
            });
        }
    }
    public class NoisyBroadCastReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)){
                Log.e("Audio","ouch,the headset is out,did you do it?");
                if (mp != null && mp.isPlaying()){
                    mp.pause();
                }
            }
        }
    }
    @Override
    protected void onStop() {
        super.onStop();
        if (mp != null){
            if(mp.isPlaying()) mp.stop();
            mp.release();
            mp = null;
        }
        if(audioManager != null){
            audioManager.abandonAudioFocus(this);
        }
        if(receiver != null) {
            this.unregisterReceiver(receiver);
        }
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容