兩個或更多的Android應用程式可以同時播放音訊到相同的輸出流。系統把所有東西混合在一起。雖然這在技術上是令人印象深刻的,但對使用者來說卻是非常令人惱火的。為了避免所有音樂應用同時播放,Android引入了音訊聚焦的概念。只有一個應用程式可以一次聚焦音訊。
當您的應用程式需要輸出音訊時,它應該請求音訊焦點。當它有焦點時,它可以播放聲音。然而,在你獲得音訊焦點後,你可能無法持有它直到你播放完。另一個應用程式可以請求焦點,它會搶佔你的音訊焦點。如果出現這種情況,你的應用程式應該暫停播放或降低音量,讓使用者更容易聽到新的音訊源。
音訊聚焦是合作的。鼓勵應用程式遵守音訊聚焦指南,但該系統不執行這些規則。如果一個應用程式想在失去聲音焦點後繼續大聲播放,沒有什麼能阻止它。這是一種糟糕的體驗,使用者很有可能會解除安裝出現這種糟糕體驗的應用程式。
一個表現良好的音訊應用程式應該管理音訊焦點根據這些一般準則:
- 在開始播放之前立即呼叫
requestAudioFocus()
,並驗證呼叫是否返回AUDIOFOCUS_REQUEST_GRANTED。如果您按照我們在本指南中描述的那樣設計應用程式,那麼應該在媒體會話的onPlay()
回撥中呼叫requestAudioFocus()
。 - 當另一個應用程式獲得音訊焦點時,停止或暫停播放,或降低音量。
- 當播放停止,放棄音訊焦點。
音訊焦點的處理方式因Android版本不同而異:
- 從Android 2.2 (API level 8)開始,應用程式通過呼叫requestAudioFocus()和abandonAudioFocus()來管理音訊焦點。應用程式還必須註冊一個 AudioManager.OnAudioFocusChangeListener,以接收回撥和管理自己的音訊級別。
- 對於Android 5.0 (API level 21)或更高版本的應用程式,音訊應用程式應該使用AudioAttributes來描述你的應用程式正在播放的音訊型別。例如,播放語音的應用程式應該指定CONTENT_TYPE_SPEECH。
- 執行Android 8.0 (API級別26)或更高版本的應用程式應該使用
requestAudioFocus()
方法,它接受AudioFocusRequest引數。AudioFocusRequest包含關於音訊上下文和應用程式功能的資訊。系統使用這些資訊自動管理音訊焦點的獲取和丟失。
在Android 8.0和更高版本上的音訊聚焦
從Android 8.0 (API級別26)開始,當您呼叫requestAudioFocus()時,必須提供AudioFocusRequest引數。若要釋放音訊焦點,請呼叫abandonAudioFocusRequest()方法,該方法還將AudioFocusRequest作為其引數。當請求和放棄焦點時,應該使用相同的AudioFocusRequest例項。
要建立AudioFocusRequest,請使用AudioFocusRequest. builder。由於焦點請求必須始終指定請求的型別,因此該型別包含在構造器的建構函式中。使用構建器的方法設定請求的其他欄位。
需要FocusGain
欄位;所有其他欄位都是可選的。
解釋一下主要方法:
每個請求都需要這個欄位。它的值與android8.0之前對requestaudiofococus()的呼叫中使用的durationHint相同:
AUDIOFOCUS_GAIN
,AUDIOFOCUS_GAIN_TRANSIENT
,AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
,或AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
。
audioattributes描述了系統應用程式的用例。當一個應用程式獲得和失去了音訊的焦點時系統會檢視他們。屬性取代了流型別的概念。在Android 8.0 (API級別26)及更高版本中,除了音量控制之外的任何操作都不支援流型別。在focus請求中使用與音訊播放器中相同的屬性(如下表所示)。
使用一個AudioAttributes.Builder首先指定屬性,然後使用此方法將屬性分配給請求。
如果沒有指定,AudioAttributes預設為
AudioAttributes.USAGE_MEDIA
。
當另一個應用程式請求使用
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
進行焦點處理時,具有焦點的應用程式通常不會收到onAudioFocusChange()回撥,因為系統可以自己執行這個操作。當你需要暫停播放而不是降低音量時,呼叫setWillPauseWhenDucked(true),建立並設定一個OnAudioFocusChangeListener,如下面的自動ducking所述。
當焦點被另一個應用鎖定時,對音訊焦點的請求可能會失敗。這種方法允許延遲焦點增益:當焦點可用時,非同步獲取焦點的能力。
注意,只有當你指定了一個AudioManager.OnAudioFocusChangeListener時,延遲的焦點增益(focus gain)才有效。因為您的應用程式需要接收回撥以便知道焦點已被允許獲取。
只有在請求中指定willPauseWhenDucked(true)或setAcceptsDelayedFocusGain(true)時,才需要OnAudioFocusChangeListener。
設定偵聽器有兩種方法:一種有處理程式引數,另一種沒有處理程式引數。處理程式是偵聽器執行的執行緒。如果不指定處理程式,則使用與主Looper關聯的處理程式(handler)。
下面的例子展示瞭如何使用AudioFocusRequest.Bulder構建一個AudioFocusRequest還有請求和放棄音訊焦點:
mAudioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE);
mPlaybackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
mFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(mPlaybackAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(afChangeListener, mHandler)
.build();
mMediaPlayer = new MediaPlayer();
final Object mFocusLock = new Object();
boolean mPlaybackDelayed = false;
boolean mPlaybackNowAuthorized = false;
// ...
int res = mAudioManager.requestAudioFocus(mFocusRequest);
synchronized(mFocusLock) {
if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
mPlaybackNowAuthorized = false;
} else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mPlaybackNowAuthorized = true;
playbackNow();
} else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
mPlaybackDelayed = true;
mPlaybackNowAuthorized = false;
}
}
// ...
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
if (mPlaybackDelayed || mResumeOnFocusGain) {
synchronized(mFocusLock) {
mPlaybackDelayed = false;
mResumeOnFocusGain = false;
}
playbackNow();
}
break;
case AudioManager.AUDIOFOCUS_LOSS:
synchronized(mFocusLock) {
mResumeOnFocusGain = false;
mPlaybackDelayed = false;
}
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
synchronized(mFocusLock) {
mResumeOnFocusGain = true;
mPlaybackDelayed = false;
}
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// ... pausing or ducking depends on your app
break;
}
}
}
複製程式碼
自動ducking
在Android 8.0 (API級別26)中,當另一個應用程式使用AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
請求焦點時,系統可以降低音量並恢復音訊播放,而無需呼叫應用程式的onAudioFocusChange()
回撥。
在音樂和視訊播放應用程式中,自動降低音量是可以接受的行為,但在播放語音內容(比如在有聲書應用程式中)時,它就沒用了。在這種情況下,應用程式應該暫停。
如果你想讓你的應用程式在被要求ducking時暫停而不是減少它的音量,建立一個OnAudioFocusChangeListener
,使用一個onAudioFocusChange()
回撥方法來實現所需的暫停/恢復行為。呼叫setOnAudioFocusChangeListener()來註冊偵聽器,並呼叫setWillPauseWhenDucked(true)來告訴系統使用您的回撥,而不是執行自動ducking操作。
延遲獲得焦點
有時系統不能批准音訊焦點請求,因為焦點被另一個應用程式“鎖定”,比如在打電話時。在本例中,requestAudioFocus()
返回AUDIOFOCUS_REQUEST_FAILED
。當這種情況發生時,您的應用程式不應該繼續進行音訊播放,因為它沒有獲得焦點。
該方法名為setAcceptsDelayedFocusGain(true),它允許應用程式非同步處理焦點請求。設定此標誌後,鎖定焦點時發出的請求將返回AUDIOFOCUS_REQUEST_DELAYED
。當鎖定音訊焦點的條件不再存在時,例如當電話結束時,系統會授予掛起的焦點請求,並呼叫onAudioFocusChange()來通知應用程式。
為了處理延遲的焦點增益,您必須建立一個OnAudioFocusChangeListener,它使用一個onAudioFocusChange()回撥方法來實現所需的行為,並通過呼叫setOnAudioFocusChangeListener()來註冊偵聽器。
android 8.0之前版本 音訊聚焦
當你呼叫requestAudioFocus()時,你必須指定一個duration hint,這個duration hint可能會被另一個當前保持焦點並播放的應用程式使用:
-
當你計劃在可預見的將來播放音訊時(例如,播放音樂時)請求永久的音訊焦點(AUDIOFOCUS_GAIN),並且您希望先前的音訊焦點持有者停止播放。
-
請求瞬態焦點(AUDIOFOCUS_GAIN_TRANSIENT),當您希望只在短時間內播放音訊,而您希望之前的持有者暫停播放。
-
請求ducking瞬態焦點 (AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK),以指示您希望只在短時間內播放音訊,並且如果先前的焦點所有者“duck”(降低)其音訊輸出,仍可以繼續播放著。兩個音訊輸出都混合到音訊流中。Ducking特別適用於間歇性使用音訊流的應用程式,比如聲音驅動方向。
requestAudioFocus()方法也需要一個AudioManager.OnAudioFocusChangeListener。這個偵聽器應該建立在你的媒體會話所處的相同活動或服務中。它實現了回撥onAudioFocusChange(),當其他應用程式獲得或放棄音訊焦點時,您的應用程式會接收到這個回撥。
以下程式碼片段請求流STREAM_MUSIC上的永久音訊焦點,並註冊一個OnAudioFocusChangeListener來處理音訊焦點中的後續更改。(在下面的對音訊焦點變化的響應部分討論了更改偵聽器。)
AudioManager mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
AudioManager.OnAudioFocusChangeListener afChangeListener;
...
// 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) {
// Start playback
}
複製程式碼
當你完成播放時,呼叫abandonAudioFocus().
// Abandon audio focus when playback complete
mAudioManager.abandonAudioFocus(afChangeListener);
複製程式碼
這將通知系統您不再需要焦點,並登出相關的OnAudioFocusChangeListener。如果您請求瞬態焦點,這將通知暫停或duck的應用程式可能繼續播放或恢復音量。
對音訊焦點變化的響應
當一個應用程式獲得音訊焦點時,它必須能夠在另一個應用程式請求自己的音訊焦點時釋放它。當發生這種情況時,您的應用程式在AudioFocusChangeListener中接收到對onAudioFocusChange()方法的呼叫,該呼叫是在呼叫requestAudioFocus()時指定的。
傳遞給onAudioFocusChange()的focusChange引數指示正在發生的變化。它對應於應用程式獲取焦點所使用的持續時間提示。你的應用程式應該做出適當的響應。
瞬態失焦
如果焦點的變化是瞬態的(AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK或AUDIOFOCUS_LOSS_TRANSIENT),應用程式應該ducking降低音量(如果你不依賴自動ducking)或暫停播放,但以其他方式保持相同的狀態。
在音訊焦點暫時消失期間,您應該繼續監視音訊焦點的變化,並準備在恢復焦點後恢復正常回放。當阻塞應用程式放棄焦點時,您將收到一個回撥(AUDIOFOCUS_GAIN
)。此時,您可以將音量恢復到正常水平或重新開始播放。
永久失去焦點
如果音訊焦點丟失是永久性的(AUDIOFOCUS_LOSS
),另一個應用程式正在播放音訊。你的應用應該立即暫停播放,因為它永遠不會收到AUDIOFOCUS_GAIN
回撥。要重新啟動回放,使用者必須採取顯式操作,比如在通知或應用程式UI中按下play傳輸控制元件。
下面的程式碼片段演示瞭如何實現OnAudioFocusChangeListener及其onAudioFocusChange()回撥。請注意,使用一個Handler在永久失去音訊焦點時以延遲執行停止操作的回撥。
private Handler mHandler = new Handler();
AudioManager.OnAudioFocusChangeListener afChangeListener =
new AudioManager.OnAudioFocusChangeListener() {
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
// Permanent loss of audio focus
// Pause playback immediately
mediaController.getTransportControls().pause();
// Wait 30 seconds before stopping playback
mHandler.postDelayed(mDelayedStopRunnable,
TimeUnit.SECONDS.toMillis(30));
}
else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
// Pause playback
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// Lower the volume, keep playing
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// Your app has been granted audio focus again
// Raise volume to normal, restart playback if necessary
}
}
};
複製程式碼
handler使用一個如下所示的Runnable
private Runnable mDelayedStopRunnable = new Runnable() {
@Override
public void run() {
getMediaController().getTransportControls().stop();
}
};
複製程式碼
如果使用者重新啟動播放,為了確保延遲停止不會起作用,可以呼叫mHandler.removeCallbacks(mDelayedStopRunnable)
來響應任何狀態更改。例如,在回撥的onPlay()、onSkipToNext()中呼叫removeCallbacks()。在清理服務使用的資源時,還應該在服務的onDestroy()回撥中呼叫此方法。