管理音訊焦點

兩個以上的 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 之前的 durationHint 呼叫中使用的 requestAudioFocus() 相同: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)。
  • 使用 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。如果您要求暫時性焦點,這會通知已暫停或降低音量的應用程式,可以繼續播放或還原音量。

回應音訊焦點變更

應用程式取得音訊焦點後,必須能在其他應用程式要求音訊焦點時釋出。發生這種情況時,應用程式會收到 AudioFocusChangeListener 中 onAudioFocusChange() 方法的呼叫,而您在應用程式呼叫 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)。舉例來說,請在 Callback 的 onPlay()、onSkipToNext() 等中呼叫 removeCallbacks()。此外,在清除服務使用的資源時,您也應在服務的 onDestroy() 回呼中呼叫這個方法。