管理音频焦点

两个或两个以上的 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 之前的 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。此监听器应在媒体会话所在的 activity 或服务中创建。它会实现 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_DUCKAUDIOFOCUS_LOSS_TRANSIENT),您的应用应该降低音量(如果您不依赖于自动降低音量)或暂停播放,否则保持相同的状态。

在暂时性失去音频焦点时,您应该继续监控音频焦点的变化,并准备好在重新获得焦点后恢复正常播放。当抢占焦点的应用放弃焦点时,您会收到一个回调 (AUDIOFOCUS_GAIN)。此时,您可以将音量恢复到正常水平或重新开始播放。

永久性失去焦点
如果是永久性失去音频焦点 (AUDIOFOCUS_LOSS),则其他应用会播放音频。您的应用应立即暂停播放,因为它不会收到 AUDIOFOCUS_GAIN 回调。如需重新开始播放,用户必须执行明确的操作,例如在通知或应用界面中按播放传输控件。

以下代码段演示了如何实现 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() 回调中调用此方法。