管理音訊焦點

兩個以上的 Android 應用程式可以同時將音訊播放到同一個輸出串流,系統會將所有音訊混合在一起。雖然這在技術上令人驚豔,但可能會讓使用者感到非常困擾。為避免所有音樂應用程式同時播放,Android 導入了「音訊焦點」的概念。一次只能有一個應用程式取得音訊焦點。

應用程式需要輸出音訊時,應要求音訊焦點。當它獲得焦點時,就可以播放音效。不過,取得音訊焦點後,您可能無法一直保留到播放完畢。其他應用程式可以要求焦點,搶先取得音訊焦點。發生這種情況時,應用程式應暫停播放或調低音量,讓使用者更容易聽到新的音訊來源。

在 Android 12 (API 級別 31) 之前,音訊焦點並非由系統管理。因此,雖然我們鼓勵應用程式開發人員遵守音訊焦點指南,但如果應用程式在搭載 Android 11 (API 級別 30) 以下版本的裝置上失去音訊焦點後,仍繼續大聲播放,系統也無法阻止。不過,這種應用程式行為會導致使用者體驗不佳,且通常會導致使用者解除安裝行為不當的應用程式。

設計良好的音訊應用程式應根據下列一般指南管理音訊焦點:

  • 在開始播放前立即呼叫 requestAudioFocus(),並確認呼叫傳回 AUDIOFOCUS_REQUEST_GRANTED。在媒體工作階段的 onPlay() 回呼中呼叫 requestAudioFocus()。

  • 當其他應用程式取得音訊焦點時,請停止或暫停播放,或降低音量。

  • 停止播放時 (例如應用程式沒有可播放的內容),請捨棄音訊焦點。如果使用者暫停播放,但稍後可能會繼續播放,應用程式就不必捨棄音訊焦點。

  • 使用 AudioAttributes 描述應用程式播放的音訊類型。舉例來說,如果應用程式會播放語音,請指定 CONTENT_TYPE_SPEECH。

音訊焦點的處理方式會因執行的 Android 版本而異:

Android 12 (API 級別 31) 以上版本
音訊焦點由系統管理。當另一個應用程式要求音訊焦點時,系統會強制淡出應用程式的音訊播放。此外,系統也會在收到來電時將音訊播放靜音。
Android 8.0 (API 級別 26) 至 Android 11 (API 級別 30)
音訊焦點並非由系統管理,但包含 Android 8.0 (API 級別 26) 以上版本推出的部分變更。
Android 7.1 (API 級別 25) 以下版本
音訊焦點並非由系統管理,應用程式會使用 requestAudioFocus() 和 abandonAudioFocus() 管理音訊焦點。

Android 12 以上版本的音訊焦點

使用音訊焦點的媒體或遊戲應用程式,不應在失去焦點後播放音訊。在 Android 12 (API 級別 31) 以上版本中,系統會強制執行這項行為。當某個應用程式要求音訊焦點,而另一個應用程式擁有焦點並正在播放時,系統會強制播放中的應用程式淡出。加入淡出效果後,從一個應用程式切換到另一個應用程式時,轉場效果會更流暢。

符合下列條件時,就會發生這種淡出行為:

  1. 第一個是目前播放的應用程式,符合下列所有條件:

  2. 第二個應用程式使用 AudioManager.AUDIOFOCUS_GAIN 要求音訊焦點。

符合這些條件時,音訊系統會淡出第一個應用程式。淡出結束後,系統會通知第一個應用程式焦點遺失。應用程式的播放器會保持靜音,直到應用程式再次要求音訊焦點。

現有的音訊焦點行為

您也應該留意其他涉及音訊焦點切換的情況。

自動降低音量

Android 8.0 (API 級別 26) 推出自動降低音量功能,可暫時降低某個應用程式的音量,讓使用者能清楚聽見另一個應用程式的音訊。

讓系統實作降低音量功能,您就不必在應用程式中實作這項功能。

當音訊通知從播放中的應用程式取得焦點時,也會自動降低音量。通知播放的開始時間會與降低音量斜坡的結束時間同步。

符合下列條件時,系統會自動降低音量:

  1. 第一個是目前播放內容的應用程式,須符合下列所有條件:

  2. 第二個應用程式使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 要求音訊焦點。

符合這些條件時,音訊系統會在第二個應用程式取得焦點時,降低第一個應用程式所有有效播放器的音量。當第二個應用程式放棄焦點時,系統會取消降低音量。第一個應用程式失去焦點時不會收到通知,因此不需要執行任何操作。

請注意,如果使用者正在聆聽語音內容,系統不會自動降低音量,因為使用者可能會錯過部分節目。例如,行車路線的語音導引不會降低音量。

有來電時,將目前播放的音訊設為靜音

部分應用程式無法正常運作,在通話期間仍會繼續播放音訊。 這種情況會迫使使用者尋找並將違規應用程式設為靜音或退出,才能聽到通話內容。為避免這種情況,系統可以在有來電時將其他應用程式的音訊設為靜音。當系統收到來電,且應用程式符合下列條件時,就會叫用這項功能:

  • 應用程式具有 AudioAttributes.USAGE_MEDIA 或 AudioAttributes.USAGE_GAME 用途屬性。
  • 應用程式成功要求音訊焦點 (任何焦點增益),並正在播放音訊。

如果應用程式在通話期間繼續播放,系統會將播放內容設為靜音,直到通話結束。不過,如果應用程式在通話期間開始播放,系統會假設使用者是刻意開始播放,因此不會將該播放器設為靜音。

Android 8.0 至 Android 11 的音訊焦點

自 Android 8.0 (API 級別 26) 起,呼叫 requestAudioFocus() 時,必須提供 AudioFocusRequest 參數。AudioFocusRequest 包含應用程式的音訊內容和功能相關資訊。系統會使用這項資訊,自動管理音訊焦點的取得和失去。如要釋出音訊焦點,請呼叫 abandonAudioFocusRequest() 方法,該方法也會接收 AudioFocusRequest 做為引數。在要求和放棄焦點時,請使用相同的 AudioFocusRequest 執行個體。

如要建立 AudioFocusRequest,請使用 AudioFocusRequest.Builder。由於焦點要求一律必須指定要求類型,因此建構工具的建構函式會包含該類型。使用建構工具的方法設定要求的其他欄位。

FocusGain 為必填欄位,其他欄位則為選填。

方法附註
setFocusGain() 每項要求都必須填寫這個欄位。其值與 Android 8.0 之前版本呼叫 requestAudioFocus() 時使用的 durationHint 相同:AUDIOFOCUS_GAIN、AUDIOFOCUS_GAIN_TRANSIENT、AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 或 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE。
setAudioAttributes() AudioAttributes 描述應用程式的用途。當應用程式取得和失去音訊焦點時,系統會查看這些屬性。屬性會取代串流類型的概念。在 Android 8.0 (API 級別 26) 以上版本中,除了音量控制以外,其他任何作業的串流類型都已淘汰。在焦點要求中使用與音訊播放器相同的屬性 (如本表下方的範例所示)。

請先使用 AudioAttributes.Builder 指定屬性,然後使用這個方法將屬性指派給要求。

如未指定,AudioAttributes 預設為 AudioAttributes.USAGE_MEDIA。

setWillPauseWhenDucked() 當其他應用程式使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 要求焦點時,具有焦點的應用程式通常不會收到 onAudioFocusChange() 回呼,因為系統可以自行執行閃避。如需暫停播放而非降低音量,請呼叫 setWillPauseWhenDucked(true),並建立及設定 OnAudioFocusChangeListener,如「自動降低音量」一節所述。
setAcceptsDelayedFocusGain() 如果焦點遭到其他應用程式鎖定,音訊焦點要求可能會失敗。這個方法可啟用延遲焦點取得:在焦點可用時,以非同步方式取得焦點。

請注意,只有在音訊要求中指定 AudioManager.OnAudioFocusChangeListener,延遲取得焦點才會生效,因為應用程式需要接收回呼,才能知道焦點已授予。

setOnAudioFocusChangeListener() 只有在要求中一併指定 willPauseWhenDucked(true) 或 setAcceptsDelayedFocusGain(true) 時,才需要 OnAudioFocusChangeListener。

設定事件監聽器的方法有兩種:一種有處理常式引數,另一種則沒有。處理常式是執行事件監聽器的執行緒。如未指定處理常式,系統會使用與主要 Looper 相關聯的處理常式。

以下範例說明如何使用 AudioFocusRequest.Builder 建構 AudioFocusRequest,以及要求和放棄音訊焦點:

Kotlin

// initializing variables for audio focus and playback management
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
    setAudioAttributes(AudioAttributes.Builder().run {
        setUsage(AudioAttributes.USAGE_GAME)
        setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        build()
    })
    setAcceptsDelayedFocusGain(true)
    setOnAudioFocusChangeListener(afChangeListener, handler)
    build()
}
val focusLock = Any()

var playbackDelayed = false
var playbackNowAuthorized = false

// requesting audio focus and processing the response
val res = audioManager.requestAudioFocus(focusRequest)
synchronized(focusLock) {
    playbackNowAuthorized = when (res) {
        AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false
        AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
            playbackNow()
            true
        }
        AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
            playbackDelayed = true
            false
        }
        else -> false
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
override fun onAudioFocusChange(focusChange: Int) {
    when (focusChange) {
        AudioManager.AUDIOFOCUS_GAIN ->
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false
                    resumeOnFocusGain = false
                }
                playbackNow()
            }
        AudioManager.AUDIOFOCUS_LOSS -> {
            synchronized(focusLock) {
                resumeOnFocusGain = false
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            synchronized(focusLock) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying()
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // ... pausing or ducking depends on your app
        }
    }
}

Java

// initializing variables for audio focus and playback management
audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE);
playbackAttributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_GAME)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();
focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(playbackAttributes)
        .setAcceptsDelayedFocusGain(true)
        .setOnAudioFocusChangeListener(afChangeListener, handler)
        .build();
final Object focusLock = new Object();

boolean playbackDelayed = false;
boolean playbackNowAuthorized = false;

// requesting audio focus and processing the response
int res = audioManager.requestAudioFocus(focusRequest);
synchronized(focusLock) {
    if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
        playbackNowAuthorized = false;
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
        playbackNowAuthorized = true;
        playbackNow();
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
        playbackDelayed = true;
        playbackNowAuthorized = false;
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
@Override
public void onAudioFocusChange(int focusChange) {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_GAIN:
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false;
                    resumeOnFocusGain = false;
                }
                playbackNow();
            }
            break;
        case AudioManager.AUDIOFOCUS_LOSS:
            synchronized(focusLock) {
                resumeOnFocusGain = false;
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            synchronized(focusLock) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying();
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // ... pausing or ducking depends on your app
            break;
        }
    }
}

自動降低音量

在 Android 8.0 (API 級別 26) 中,當其他應用程式使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 要求焦點時,系統可以降低及還原音量,而不必叫用應用程式的 onAudioFocusChange() 回呼。

雖然自動降低音量是音樂和影片播放應用程式可接受的行為,但在播放語音內容 (例如有聲書應用程式) 時,這種行為並不實用。在這種情況下,應用程式應改為暫停播放。

如果希望應用程式在收到降低音量要求時暫停播放,而不是降低音量,請建立 OnAudioFocusChangeListener,並使用 onAudioFocusChange() 回呼方法實作所需的暫停/繼續行為。呼叫 setOnAudioFocusChangeListener() 註冊監聽器,並呼叫 setWillPauseWhenDucked(true) 指示系統使用回呼,而非執行自動降低音量。

延遲取得焦點

有時系統無法授予音訊焦點要求,因為焦點已由其他應用程式「鎖定」,例如在通話期間。在這種情況下,requestAudioFocus() 會傳回 AUDIOFOCUS_REQUEST_FAILED。發生這種情況時,應用程式不應繼續播放音訊,因為未取得焦點。

setAcceptsDelayedFocusGain(true) 方法可讓應用程式以非同步方式處理焦點要求。設定這個標記後,在焦點鎖定時提出的要求會傳回 AUDIOFOCUS_REQUEST_DELAYED。當鎖定音訊焦點的條件不再存在時 (例如通話結束),系統會授予待處理的焦點要求,並呼叫 onAudioFocusChange() 通知應用程式。

如要處理延遲取得焦點的情況,您必須建立 OnAudioFocusChangeListener,並使用 onAudioFocusChange() 回呼方法實作所需行為,然後呼叫 setOnAudioFocusChangeListener() 註冊事件監聽器。

Android 7.1 以下版本的音訊焦點

呼叫 requestAudioFocus() 時,必須指定時間長度提示,目前持有焦點並播放內容的其他應用程式可能會遵循這項提示:

  • 如果您打算在可預見的未來播放音訊 (例如播放音樂),且預期先前的音訊焦點持有者會停止播放,請要求永久音訊焦點 (AUDIOFOCUS_GAIN)。
  • 如果您預期只會播放短時間的音訊,且希望先前的持有者暫停播放,請要求暫時焦點 (AUDIOFOCUS_GAIN_TRANSIENT)。
  • 使用降低音量 (AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) 要求暫時焦點,表示您預期只會播放音訊一小段時間,且如果先前的焦點擁有者「降低」其音訊輸出,則可以繼續播放。這兩個音訊輸出會混入音訊串流。降低音量特別適合間歇使用音訊串流的應用程式,例如語音行車路線。

requestAudioFocus() 方法也需要 AudioManager.OnAudioFocusChangeListener。這個事件監聽器應在擁有媒體工作階段的同一個活動或服務中建立。它會實作回呼 onAudioFocusChange(),當其他應用程式取得或放棄音訊焦點時,您的應用程式會收到該回呼。

下列程式碼片段會要求串流 STREAM_MUSIC 的永久音訊焦點,並註冊 OnAudioFocusChangeListener 來處理後續的音訊焦點變更。(變更事件監聽器會在「回應音訊焦點變更」中討論)。

Kotlin

audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
lateinit var afChangeListener AudioManager.OnAudioFocusChangeListener

...
// Request audio focus for playback
val result: Int = audioManager.requestAudioFocus(
        afChangeListener,
        // Use the music stream.
        AudioManager.STREAM_MUSIC,
        // Request permanent focus.
        AudioManager.AUDIOFOCUS_GAIN
)

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
}

Java

AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
AudioManager.OnAudioFocusChangeListener afChangeListener;

...
// Request audio focus for playback
int result = audioManager.requestAudioFocus(afChangeListener,
                             // Use the music stream.
                             AudioManager.STREAM_MUSIC,
                             // Request permanent focus.
                             AudioManager.AUDIOFOCUS_GAIN);

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
}

播放完畢後,請呼叫 abandonAudioFocus()。

Kotlin

audioManager.abandonAudioFocus(afChangeListener)

Java

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

這會通知系統您不再需要焦點,並取消註冊相關聯的 OnAudioFocusChangeListener。如果您要求暫時焦點,這會通知已暫停或降低音量的應用程式,可以繼續播放或還原音量。

回應音訊焦點變更

應用程式取得音訊焦點後,必須能在其他應用程式要求音訊焦點時釋出焦點。發生這種情況時,應用程式會收到呼叫,呼叫 AudioFocusChangeListener 中的 onAudioFocusChange() 方法,該 AudioFocusChangeListener 是應用程式呼叫 requestAudioFocus() 時指定的。

傳遞至 onAudioFocusChange() 的 focusChange 參數會指出發生的變更類型。這會對應到取得焦點的應用程式所使用的持續時間提示。應用程式應適當回應。

暫時失焦
如果焦點變更是暫時性的 (AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 或 AUDIOFOCUS_LOSS_TRANSIENT),應用程式應降低音量 (如果您不依賴自動降低音量) 或暫停播放,但仍維持相同狀態。

在暫時失去音訊焦點期間,您應繼續監控音訊焦點的變化,並準備在重新取得焦點時恢復正常播放。當封鎖應用程式放棄焦點時,您會收到回呼 (AUDIOFOCUS_GAIN)。此時,您可以將音量恢復到正常等級,或重新開始播放。

永久失去焦點
如果音訊焦點永久遺失 (AUDIOFOCUS_LOSS),表示另一個應用程式正在播放音訊。應用程式應立即暫停播放,因為不會收到 AUDIOFOCUS_GAIN 回呼。如要重新播放,使用者必須採取明確動作,例如按下通知或應用程式 UI 中的播放傳輸控制項。

下列程式碼片段示範如何實作 OnAudioFocusChangeListener 及其 onAudioFocusChange() 回呼。請注意,我們使用 Handler 延遲停止回呼,以永久失去音訊焦點。

Kotlin

private val handler = Handler()
private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
    when (focusChange) {
        AudioManager.AUDIOFOCUS_LOSS -> {
            // Permanent loss of audio focus
            // Pause playback immediately
            mediaController.transportControls.pause()
            // Wait 30 seconds before stopping playback
            handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30))
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            // Pause playback
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // Lower the volume, keep playing
        }
        AudioManager.AUDIOFOCUS_GAIN -> {
            // Your app has been granted audio focus again
            // Raise volume to normal, restart playback if necessary
        }
    }
}

Java

private Handler handler = 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
        handler.postDelayed(delayedStopRunnable,
          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
      }
    }
  };

處理常式會使用類似以下的 Runnable:

Kotlin

private var delayedStopRunnable = Runnable {
    mediaController.transportControls.stop()
}

Java

private Runnable delayedStopRunnable = new Runnable() {
    @Override
    public void run() {
        getMediaController().getTransportControls().stop();
    }
};

為確保使用者重新播放時不會延遲停止,請呼叫 mHandler.removeCallbacks(mDelayedStopRunnable) 以回應任何狀態變更。舉例來說,在回呼的 onPlay()、onSkipToNext() 等中呼叫 removeCallbacks()。在清除服務使用的資源時,您也應在服務的 onDestroy() 回呼中呼叫這個方法。