管理音訊焦點

兩個以上的 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) 中推出,這項功能會暫時降低某個應用程式的音訊音量,讓使用者能夠清楚聽見其他應用程式的聲音。

透過系統實作「Duck」機制,您就不需要在應用程式中實作「Duck」。

音訊通知從播放中的應用程式取得焦點時,也會自動降低其他應用程式的耗電量。通知播放作業開始與降低音量的坡道結束即保持同步。

只有在符合下列條件時,系統就會自動將手指設為靜音:

  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 之前呼叫 requestAudioFocus() 時所用的 durationHint 相同: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() 回呼的情況下,對並還原磁碟區。

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

如果希望應用程式在系統要求「擲出作業」時暫停,而非降低音量,請使用 onAudioFocusChange() 回呼方法建立 OnAudioFocusChangeListener,藉此執行所需的暫停/繼續行為。呼叫 setOnAudioFocusChangeListener() 以註冊事件監聽器,並呼叫 setWillPauseWhenDucked(true) 來指示系統使用回呼,而不是執行自動取消作業。

延遲對焦增加

有時因為其他應用程式 (例如通話時) 將焦點「鎖定」,導致系統無法授予音訊焦點的要求。在這種情況下,requestAudioFocus() 會傳回 AUDIOFOCUS_REQUEST_FAILED。在這種情況下,應用程式不應對焦,因此不應繼續播放音訊。

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

為了處理延遲的聚焦取得,您必須使用 onAudioFocusChange() 回呼方法建立 OnAudioFocusChangeListener,藉此實作所需行為,並呼叫 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。如果您要求暫時聚焦,應用程式會通知已暫停或某個應用程式可以繼續播放或還原音量。

回應音訊焦點變更

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

傳遞到 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) 以回應任何狀態變更。例如,呼叫回呼的 onPlay()onSkipToNext() 等中的 removeCallbacks()。在清除服務使用的資源時,也應該在服務的 onDestroy() 回呼中呼叫此方法。