管理音訊焦點

兩個以上的 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_MEDIAAudioAttributes.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 之前的 durationHint 呼叫中使用的 requestAudioFocus() 相同:AUDIOFOCUS_GAINAUDIOFOCUS_GAIN_TRANSIENTAUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCKAUDIOFOCUS_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)。
  • 使用 ducking (AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) 要求暫時性焦點,表示您預期只會短暫播放音訊,且先前的焦點擁有者可以繼續播放音訊 (如果「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。如果您要求暫時性焦點,這會通知已暫停或降低音量的應用程式,可以繼續播放或還原音量。

回應音訊焦點變更

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

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

暫時失去焦點
如果焦點變更是暫時性的 (AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCKAUDIOFOCUS_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)。舉例來說,請在 Callback 的 onPlay()onSkipToNext() 等中呼叫 removeCallbacks()。您也應該在服務的 onDestroy() 回呼中呼叫這個方法,以便清除服務使用的資源。