android音視訊指南-管理音訊焦點

DamonRen發表於2018-11-01

翻譯自Managing audio focus

兩個或更多的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()回撥中呼叫此方法。

相關文章