Android 音频焦点处理

刚开始的时候,认为在智能机中,每个 APP 都是各自管各自的,媒体播放也是这样子的;然而对比同类产品,发现同类产品可以放到播放自如,体验很好,通过对比研究,根源就在于音频焦点处理上。



一、引言

在功能机时代,系统是不允许有两个或多个音源播出,因为有优先级管理,但在智能机系统中,多个音源同时播放是被允许的,两个或更多的 Android 应用程序可以同时播放音频到相同的输出流。系统把所有东西混合在一起。虽然这在技术上是令人印象深刻的,但对用户来说却是非常令人恼火的,体验感极差。为了避免所有音乐应用同时播放,Android 引入了音频聚焦的概念。只有一个应用程序可以一次聚焦音频。

当应用程序需要输出音频时,它应该请求音频焦点。当它有焦点时,它可以播放声音。然而,在获得音频焦点后,可能无法持有它直到你播放完。另一个应用程序可以请求焦点,它会抢占你的音频焦点。如果出现这种情况,你的应用程序应该暂停播放或降低音量,让用户更容易听到新的音频源。

音频聚焦是合作的。鼓励应用程序遵守音频聚焦指南,但该系统不执行这些规则。如果一个应用程序想在失去声音焦点后继续大声播放,没有什么能阻止它。这是一种糟糕的体验,用户很有可能会卸载出现这种糟糕体验的应用程序。

二、音频焦点申请与释放

一个表现良好的音频应用程序应该管理音频焦点根据这些一般准则:

  • 在开始播放之前立即调用 requestAudioFocus(),并验证调用是否返回 AUDIOFOCUS_REQUEST_GRANTED。如果按照参考设计中描述的那样设计应用程序,那么应该在媒体会话的 onPlay() 回调中调用 requestAudioFocus()。
  • 当另一个应用程序获得音频焦点时,停止或暂停播放,或降低音量。
  • 当播放停止,放弃音频焦点。

音频焦点的处理方式因 Android 版本不同而异:

  • 从 Android 2.2 (API level 8) 开始,应用程序通过调用 requestAudioFocus() 和 abandonAudioFocus() 来管理音频焦点。应用程序还必须注册一个 AudioManager.OnAudioFocusChangeListener,以接收回调和管理自己的音频级别。
  • 对于 Android 5.0 (API level 21) 或更高版本的应用程序,音频应用程序应该使用 AudioAttributes 来描述你的应用程序正在播放的音频类型。例如,播放语音的应用程序应该指定CONTENT_TYPE_SPEECH。
  • 运行 Android 8.0 (API级别26) 或更高版本的应用程序应该使用 requestAudioFocus() 方法,它接受 AudioFocusRequest 参数。AudioFocusRequest 包含关于音频上下文和应用程序功能的信息。系统使用这些信息自动管理音频焦点的获取和丢失。

三、音频聚焦

当你调用 requestAudioFocus() 时,你必须指定一个 duration hint,这个 duration hint 可能会被另一个当前保持焦点并播放的应用程序使用:

  • 当你计划在可预见的将来播放音频时(例如,播放音乐时)请求永久的音频焦点(AUDIOFOCUS_GAIN),并且您希望先前的音频焦点持有者停止播放。

  • 请求瞬态焦点(AUDIOFOCUS_GAIN_TRANSIENT),当您希望只在短时间内播放音频,而您希望之前的持有者暂停播放。

  • 请求 ducking 瞬态焦点 (AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK),以指示您希望只在短时间内播放音频,并且如果先前的焦点所有者 “duck”(降低) 其音频输出,仍可以继续播放着。两个音频输出都混合到音频流中。Ducking 特别适用于间歇性使用音频流的应用程序,比如声音驱动方向。

requestAudioFocus() 方法也需要一个 AudioManager.OnAudioFocusChangeListener。这个侦听器应该创建在你的媒体会话所处的相同活动或服务中。它实现了回调 onAudioFocusChange(),当其他应用程序获得或放弃音频焦点时,您的应用程序会接收到这个回调。

以下代码片段请求流 STREAM_MUSIC 上的永久音频焦点,并注册一个 OnAudioFocusChangeListener 来处理音频焦点:

   public static AudioFocusUtils getInstance() {
        if (mIntance == null) {
            mIntance = new AudioFocusUtils();
            mAudioManager = (AudioManager) CGenieApplication.getInstance().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
        }
        return mIntance;
    }

    private AudioManager.OnAudioFocusChangeListener mOnAudioFocusChangeListener = new
            AudioManager.OnAudioFocusChangeListener() {

                @Override
                public void onAudioFocusChange(int focusChange) {
                    adjustAudioFocus(focusChange);
                }
            };

    public void adjustAudioManagerListener(boolean isRequestFocus) {
        if (mAudioManager == null) {
            return;
        }
        if (isRequestFocus) {
            mAudioManager.requestAudioFocus(mOnAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
        } else {
            mAudioManager.abandonAudioFocus(mOnAudioFocusChangeListener);
        }
    }

    private void adjustAudioFocus(int focusChange) {
        logShow("focusChange: " + focusChange);
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_LOSS:
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                mAudioFocusLossState = true;
                MediaManager.getInstance().suspendAudioPlaying();   //失去焦点,暂停播放
                break;
            case AudioManager.AUDIOFOCUS_GAIN:
                if (mAudioFocusLossState) {
                    mAudioFocusLossState = false;
                    MediaManager.getInstance().resumeAudioPlaying();   //获得焦点,恢复播放
                }
                break;
            default:
                break;
        }
    }

当前,这种处理方式,在 Android5.0 - Android 9.0 上验证,都是有效的。

四、对音频焦点变化的响应

当一个应用程序获得音频焦点时,它必须能够在另一个应用程序请求自己的音频焦点时释放它。当发生这种情况时,应用程序在 AudioFocusChangeListener 中接收到对onAudioFocusChange() 方法的调用,该调用是在调用 requestAudioFocus() 时指定的。

传递给 onAudioFocusChange() 的 focusChange 参数指示正在发生的变化。它对应于应用程序获取焦点所使用的持续时间提示。你的应用程序应该做出适当的响应。

瞬态失焦

如果焦点的变化是瞬态的(AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK或AUDIOFOCUS_LOSS_TRANSIENT),应用程序应该 ducking 降低音量(如果你不依赖自动 ducking)或暂停播放,但以其他方式保持相同的状态。
在音频焦点暂时消失期间,您应该继续监视音频焦点的变化,并准备在恢复焦点后恢复正常回放。当阻塞应用程序放弃焦点时,您将收到一个回调(AUDIOFOCUS_GAIN)。此时,您可以将音量恢复到正常水平或重新开始播放。

永久失去焦点

如果音频焦点丢失是永久性的(AUDIOFOCUS_LOSS),另一个应用程序正在播放音频。你的应用应该立即暂停播放,因为它永远不会收到 AUDIOFOCUS_GAIN 回调。要重新启动回放,用户必须采取显式操作,比如在通知或应用程序UI中按下 play 传输控件。


补充:

音频 APP 可以设置两种播放模式一种可以被抢占式(音乐 APP 主流模式),一种不被抢占式(但是会和其他混合播放)
先打开第三方 APP 并播放音乐,再打开我们 APP 及操作音频播放,第三方APP设置的是同时播放,两边都会播放;第三方设置的是可以被抢占式的,那我们 APP 能播放,第三方 APP 就播放不了(会暂停)。
同理,先打开我们 APP 及操作音频播放,再打开第三方 APP 并播放音乐,我们的 APP 一般都设置为可以被抢占式的,就会暂停 APP 音频播放。


参考