一文讲清:Android音频焦点

简介

1、概念

两个或两个以上的 Android 应用可同时向同一输出流播放音频,系统会将所有音频流混合在一起,为了避免所有音乐应用同时播放,Android 引入了"音频焦点"的概念。 一次只能有一个应用获得音频焦点。

2、使用

当你的应用需要输出音频时,需要请求获得音频焦点,获得焦点后,就可以播放声音了。当其他应用请求焦点时,会抢占你持有的音频焦点,此时,你的应用应暂停播放或降低音量,以便于用户听到新的音频源。

注:音频焦点策略是Android的规范化要求,但是并不是Audio出声的必要条件

使用方法

1、使用规则

  • 在即将开始播放之前调用 requestAudioFocus(),并验证调用是否返回 AUDIOFOCUS_REQUEST_GRANTED。即将开始播放,即在你代码逻辑中明确意图需要开始播放媒体时,需要调用。
  • 设置AudioFocusListener监听焦点状态,在其他应用获得音频焦点时,停止或暂停播放,或降低音量。
  • 播放停止且界面不在前台显示时,可抛弃音频焦点。(抛焦点策略需要慎重,需要结合系统整体音频策略)

2、关键代码

Android O 之前的焦点申请
java 复制代码
AudioManager mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
int focusRequest = mAudioManager.requestAudioFocus(
    audioFocusListener, // 音频焦点监听器
    AudioManager.STREAM_MUSIC,
    AudioManager.AUDIOFOCUS_GAIN);
switch (focusRequest) {
    case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
        // 请求焦点失败 -> 不允许播放
        break;
    case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
        // 请求焦点成功 -> 开始播放
        break;
}
Android O 以及之后的版本上使用
scss 复制代码
val playbackAttributes = AudioAttributes.Builder()
    .setUsage(AudioAttributes.USAGE_MEDIA)
    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
    .build()
mFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
    .setAudioAttributes(playbackAttributes)
    .setAcceptsDelayedFocusGain(true) // 是否支持延时获取焦点
    .setOnAudioFocusChangeListener(audioFocusListener, Handler(Looper.getMainLooper()))
    .build()
// request接口需要用到mFocusRequest作为参数,mFocusRequest可一开始初始化一次
val focusRequest = mAudioManager.requestAudioFocus(mFocusRequest)
if (focusRequest == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
    // 申请delay了,需要处理焦点随后分发来的播放
} else if (focusRequest == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // 申请成功,可以播放
} else if(focusRequest == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
    // 申请失败,不能播放
}

音频焦点的意图类型:

  • AUDIOFOCUS_GAIN 的使用场景:应用需要聚焦音频的时长会根据用户的使用时长改变,属于不确定期限。例如:多媒体播放或者播客等应用。
  • AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 的使用场景:应用只需短暂的音频聚焦,来播放一些提示类语音消息,或录制一段语音。例如:闹铃,导航等应用。
  • AUDIOFOCUS_GAIN_TRANSIENT 的使用场景:应用只需短暂的音频聚焦,但包含了不同响应情况,例如:电话、QQ、微信等通话应用。
  • AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 的使用场景:同样您的应用只是需要短暂的音频聚焦。未知时长,但不允许被其它应用截取音频焦点。例如:录音软件。

requestResult --- AudioManager.AUDIOFOCUS_REQUEST_DELAYED 说明:

假如当用户在通话中打开游戏,他们想玩游戏,但是当前还在打电话所以不想听到游戏声音。但是当他们通话结束的时候他们想听到游戏声音。如果这个应用支持延迟音频聚焦,会发生如下情况:

  • 当应用申请音频焦点的时候,会被拒绝并锁住,通话应用继续持有音频焦点,此时应用不能播放音频,因为应用申请焦点失败,但是申请的应用是游戏,可以正常继续操作,只是没有声音。
  • 当通话结束,之前申请的应用会被延迟分发音频焦点,这个授权是来自刚才申请音频聚焦被拒绝后锁住的那个请求,它只是被延迟一段时间后再授权,此时便可以开始恢复播放。

目前低于 Android O 的版本是不支持延迟音频焦点这个功能的。

音频焦点监听

一旦音频聚焦改变,应用要马上做出响应,它的状态可能在任何时间发生改变(丢失或重新获取),我们可以实现 OnAudioFocusChangeListener 的来响应状态改变:

kotlin 复制代码
val audioFocusListener = object : AudioManager.OnAudioFocusChangeListener {
    override fun onAudioFocusChange(focusChange: Int) {
        Timber.i("$TAG, onAudioFocusChange, state: $focusChange")
        when (focusChange) {
            AudioManager.AUDIOFOCUS_GAIN -> { handleAudioGain() }
            AudioManager.AUDIOFOCUS_LOSS -> { handleAudioLoss() }
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { handleAudioLoss(true) }
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {} // ignore
            else -> {}
        }
    }
}

状态说明:

  • AUDIOFOCUS_GAIN:重新获取音频焦点。
  • AUDIOFOCUS_LOSS:永久性失去音频焦点,则其他应用会播放音频。您的应用应立即暂停播放,清理资源;因为它不会收到 AUDIOFOCUS_GAIN 回调。
  • AUDIOFOCUS_ LOSS_TRANSIENT:暂时失去音频焦点,但是很快就会重新获得,在此状态应该暂停所有音频播放,但是不能清除资源,
  • AUDIOFOCUS_ LOSS_TRANSIENT _CAN_DUCK:暂时失去音频焦点,应用应该降低音量(一般系统底层实现),允许持续播放音频(以很小的声音),不需要完全停止播放。
焦点抛弃
kotlin 复制代码
val result = mAudioManager.abandonAudioFocusRequest(mFocusRequest) // 0: 失败, 1: 成功
Timber.i("$TAG, abandonAudioFocus end, result: $result")

3、正常接入Android焦点策略后的播放场景:

4、常见日志分析

关键字:QQAudioFocus(自己应用的音频的TAG)、MediaFocusControl、CarAudioFocus(车载)

By the way :车载媒体中心"焦点"

结论: 车载媒体中心的音源抢占和通知只是决定当前系统关联模块的媒体信息展示,并不能作为各个媒体源的播放暂停控制依据。

graph TD A(用户播放意图) --> B{尝试申请\nAndroid焦点} B --> |成功| C[requestAudioFocus] C --> D[获取AudioFocus] D --> |其他| I B --> |失败| I(结束) D --> E[Psd] D --> F[Csd] D --> G[歌词服务] D --> H[...] H --> I E --> I F --> I G --> I

代码示例

AudioFocusManager代码示例

可以通过以下代码来对AudioFocus进行管理:

Kotlin 复制代码
      
package com.max.audiofocus

import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.os.Handler
import android.os.Looper
import timber.log.Timber

/**
 * @author MaChao
 * @Description 音频焦点管理单例类
 */
object AudioFocusManager {

    private const val TAG = "AudioFocusManager"

    private val mAudioManager: AudioManager =
        (MusicApplication.mApp.getSystemService(Context.AUDIO_SERVICE)) as AudioManager
    private val mFocusRequest: AudioFocusRequest
    private var isHasFocus = false
    private var isPauseByLoss = false

    init {
        val audioFocusListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
            Timber.i("$TAG, onAudioFocusChange, state: $focusChange")
            when (focusChange) {
                AudioManager.AUDIOFOCUS_GAIN -> {
                    handleAudioGain()
                }
                AudioManager.AUDIOFOCUS_LOSS -> {
                    handleAudioLoss()
                }
                AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
                    handleAudioLoss(true)
                }
                AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {} // ignore
                else -> {}
            }
        }
        val playbackAttributes = AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
            .build()
        mFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
            .setAudioAttributes(playbackAttributes)
            .setAcceptsDelayedFocusGain(true) // 是否支持延时获取焦点
            .setOnAudioFocusChangeListener(audioFocusListener, Handler(Looper.getMainLooper()))
            .build()
    }

    /**
     * 申请音频焦点
     * @param forceRequest Boolean 是否强制调用AudioService申请(系统音频经常挂,导致异常焦点Loss了不会分配过来)
     * @return Boolean
     */
    fun requestAudioFocus(forceRequest: Boolean = true): Boolean {
        if (!forceRequest && isHasFocus) {
            Timber.i("$TAG, requestAudioFocus, qqmusic already has Focus")
            isPauseByLoss = false
            return true
        }
        val result = mAudioManager.requestAudioFocus(mFocusRequest) // 0: 失败, 1: 成功, 2: delay
        Timber.i("$TAG, requestAudioFocus end, result: $result")
        if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) { // 申请delay了一会会通过audioListener分过来
            isPauseByLoss = true
        } else if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            isPauseByLoss = false
        }
        isHasFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
        return isHasFocus
    }

    /**
     * 当前是否已经有焦点
     * @return Boolean
     */
    fun isCurrHasFocus(): Boolean {
        return isHasFocus
    }

    /**
     * 抛弃音频焦点
     * @return Boolean 是否抛成功
     */
    fun abandonAudioFocus(): Boolean {
        val result = mAudioManager.abandonAudioFocusRequest(mFocusRequest) // 0: 失败, 1: 成功
        Timber.i("$TAG, abandonAudioFocus end, result: $result")
        isHasFocus = false
        isPauseByLoss = false
        return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
    }

    /**
     * 处理焦点获取
     */
    private fun handleAudioGain() {
        isHasFocus = true
        if (isPauseByLoss) {
            PlayManager.resume()
        }
        isPauseByLoss = false
    }

    /**
     * 处理焦点丢失
     * @param isShortLoss Boolean 是否是短暂丢失
     */
    private fun handleAudioLoss(isShortLoss: Boolean = false) {
        isHasFocus = false
        if (PlayManager.isPlaying()) {
            PlayManager.pause()
            isPauseByLoss = true
        }
        if (!isShortLoss) { // 如果是长丢失,抛掉当前焦点
            abandonAudioFocus()
        }
    }

}

    
相关推荐
java_heartLake2 分钟前
设计模式之建造者模式
java·设计模式·建造者模式
G皮T2 分钟前
【设计模式】创建型模式(四):建造者模式
java·设计模式·编程·建造者模式·builder·建造者
niceffking6 分钟前
JVM HotSpot 虚拟机: 对象的创建, 内存布局和访问定位
java·jvm
菜鸟求带飞_9 分钟前
算法打卡:第十一章 图论part01
java·数据结构·算法
骆晨学长25 分钟前
基于springboot的智慧社区微信小程序
java·数据库·spring boot·后端·微信小程序·小程序
AskHarries30 分钟前
利用反射实现动态代理
java·后端·reflect
@月落31 分钟前
alibaba获得店铺的所有商品 API接口
java·大数据·数据库·人工智能·学习
liuyang-neu37 分钟前
力扣 42.接雨水
java·算法·leetcode
z千鑫40 分钟前
【人工智能】如何利用AI轻松将java,c++等代码转换为Python语言?程序员必读
java·c++·人工智能·gpt·agent·ai编程·ai工具
Flying_Fish_roe1 小时前
Spring Boot-Session管理问题
java·spring boot·后端