Android 处理音频焦点,解决音乐播放冲突的问题

1. 音频焦点是什么

在Android中,两个或多个 Android 应用可以同时将音频播放到同一输出流,系统会将所有音频混合在一起。
但是多数情况下,这对于用户来说是感到困惑的。为了避免多个应用的多个音频一起播放,Android 引入了“音频焦点”的概念,这样能够实现一次只有一个应用获得音频焦点。

在启动逻辑声音流之前,应用应使用将用于其逻辑声音流的同一个音频属性来请求获得音频焦点。虽然我们建议发送此类焦点请求,但系统不会强制要求发送。有些应用可能会明确跳过发送请求的步骤,以实现特定行为(例如,在通话期间故意播放声音)。为此,应将焦点视为间接控制播放和消除播放冲突的一种方式。

2. 音频焦点在不同Android版本的表现

2.1 在 Android 12 之前

Android 12API 31)之前,音频焦点不由系统管理。因此,虽然我们鼓励应用开发者遵守音频焦点准则,但如果应用在搭载 Android 11API 30)或更低版本的设备上丢失音频焦点后仍继续大声播放,系统将无法阻止此类播放。但是,此应用行为会导致糟糕的用户体验,并且常常会导致用户卸载出现异常的应用。

2.2 在 Android 12 及之后

音频焦点最终由系统管理和控制,而不是由应用开发者直接控制。
当其他应用请求音频焦点时,系统会强制使某个应用的音频播放淡出。收到来电时,系统会将音频播放静音。

2.3 交互矩阵

下表罗列了传入焦点请求的 CarAudioContext(列)与现有焦点持有者的上下文(行)之间的焦点交互。每个单元格表示两种上下文的预期交互类型,其中:

  • R 代表拒绝交互
  • E 代表独占交互
  • C 代表并发交互
    在这里插入图片描述

这部分具体详见 音频焦点 | Android Open Source Project (google.cn)

3. 如何实现音频焦点

Android8.0之前可以通过requestAudioFocus()来请求音频焦点。
Android8.0之后引入了新的音频焦点的API,目前Android8.0以前的机型已经很少了,可以不考虑老API
而是直接使用新的音频焦点的API,所以我们这里直接来介绍新的音频焦点API

3.1 请求音频焦点

官方推荐我们先请求音频焦点后,再进行播放音频等操作。

mAudioAttributes = AudioAttributes.Builder()
    .setUsage(AudioAttributes.USAGE_MEDIA)
    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
    .build()
mFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
    .setAudioAttributes(mAudioAttributes!!)
    .setWillPauseWhenDucked(false)
    .setOnAudioFocusChangeListener(this)
    .build()
val result : Int = mAudioManager.requestAudioFocus(mFocusRequest!!)
if(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED){
	//执行播放音频等自己的操作
}

这里有一个关注点

  • AudioAttributes : 描述的是应用的用例
    • setUsage : 设置描述音频信号的预期用途的属性。
    • setContentType:设置描述音频信号的内容类型的属性。
  • AudioFocusRequest :
    • 构造方法传参 : 它的值和 Android 8.0 之前的 requestAudioFocus() 中使用的 durationHint值相同:AUDIOFOCUS_GAIN、AUDIOFOCUS_GAIN_TRANSIENT、AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 或 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
    • setWillPauseWhenDucked : 当其他应用使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 请求获得焦点时,获得焦点的应用通常不会收到 onAudioFocusChange() 回调,因为系统可以自行降低音量。如果需要暂停播放而不是降低音量,需要调用 setWillPauseWhenDucked(true) 并创建并设置 OnAudioFocusChangeListener,如自动降低音量中所述。
    • setOnAudioFocusChangeListener : 音频焦点回调监听

3.2 释放音频焦点

释放音频焦点后,就不会回调OnAudioFocusChangeListener了。
一般可以在完成音频播放、销毁当前页面的时候,来释放音频焦点。

mAudioManager.abandonAudioFocusRequest(mFocusRequest)   

4. 封装代码

我们可以对上述代码进行封装

4.1 音频焦点封装

class AudioFocusManager(context: Context) : OnAudioFocusChangeListener {
    private val mAudioManager: AudioManager
    private var mFocusRequest: AudioFocusRequest? = null
    private var mAudioAttributes: AudioAttributes? = null
    private var mAudioFocusChangeListener: OnAudioFocusChangeListener? = null

    init {
        mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    }

    data class RequestParams(
        var usage: Int,
        var contentType: Int,
        var focusGain: Int,
        /*
         *  默认为false,如果设为false,onAudioFocusChange应该返回-3时,不会回调onAudioFocusChange,
         *  而是系统自己已经处理了,系统会自动将音量调低,不用再由我们代码中额外处理
         *  如果设为true,则需要自己在onAudioFocusChange里单独处理-3的逻辑
         */
        var willPauseWhenDucked: Boolean = false
    ) {
        companion object {
            /*
             * AudioManager.AUDIOFOCUS_LOSS : 永久失去焦点,应该停止音乐的播放
             * AudioManager.AUDIOFOCUS_LOSS_TRANSIENT : 短暂失去焦点,应该暂停音乐的播放
             * AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK : 短暂失去焦点,应该降低音量继续播放
             */

            //播放音乐场景 : 其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS
            val REQUEST_MUSIC = RequestParams(
                AudioAttributes.USAGE_MEDIA,
                AudioAttributes.CONTENT_TYPE_MUSIC,
                AudioManager.AUDIOFOCUS_GAIN
            )

            //语音识别场景 : 其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
            val REQUEST_SPEAK = RequestParams(
                AudioAttributes.USAGE_VOICE_COMMUNICATION,
                AudioAttributes.CONTENT_TYPE_SPEECH,
                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
            )

            //导航场景 : 其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
            val REQUEST_NAVIGATION = RequestParams(
                AudioAttributes.USAGE_NOTIFICATION,
                AudioAttributes.CONTENT_TYPE_SONIFICATION,
                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
            )
        }
    }

    /**
     * Request audio focus.
     */
    fun requestFocus(requestParams: RequestParams): Int {
        /*if (mAudioAttributes == null) {}
        if (mFocusRequest == null) {}*/

        mAudioAttributes = AudioAttributes.Builder()
            .setUsage(requestParams.usage)
            .setContentType(requestParams.contentType)
            .build()

        mFocusRequest = AudioFocusRequest.Builder(requestParams.focusGain)
            .setAudioAttributes(mAudioAttributes!!)
            // 默认为false,如果设为false,onAudioFocusChange应该返回-3时,不会回调onAudioFocusChange,
            // 而是系统自己已经处理了,系统会自动将音量调低,不用再由我们代码中额外处理
            // 如果设为true,则需要自己在onAudioFocusChange里单独处理-3的逻辑
            .setWillPauseWhenDucked(requestParams.willPauseWhenDucked)
            .setOnAudioFocusChangeListener(this)
            .build()

        return mAudioManager.requestAudioFocus(mFocusRequest!!)
    }

    override fun onAudioFocusChange(focusChange: Int) {
        if (mAudioFocusChangeListener != null) {
            mAudioFocusChangeListener!!.onAudioFocusChange(focusChange)
        }
    }

    /**
     * Release audio focus.
     */
    fun releaseAudioFocus() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (mFocusRequest != null) {
                mAudioManager.abandonAudioFocusRequest(mFocusRequest!!)
                mFocusRequest = null
            }
        } else {
            mAudioManager.abandonAudioFocus(this)
        }
    }

    /**
     * Same as AudioManager.OnAudioFocusChangeListener.
     */
    interface OnAudioFocusChangeListener {
        fun onAudioFocusChange(focusChange: Int)
    }

    fun setOnAudioFocusChangeListener(listener: OnAudioFocusChangeListener?) {
        mAudioFocusChangeListener = listener
    }
}

4.2 音乐播放封装

这里附带上音乐播放的封装

class MusicManger {
    private var mediaPlayer: MediaPlayer? = null

    fun play(context: Context, loop: Boolean, musicResId: Int) {
        if (mediaPlayer == null) {
            mediaPlayer = MediaPlayer.create(context, musicResId)
            mediaPlayer?.isLooping = loop
            mediaPlayer?.start()
        } else {
            mediaPlayer?.reset()
            mediaPlayer?.isLooping = loop
            val afd = context.resources.openRawResourceFd(musicResId)
            mediaPlayer?.setDataSource(afd)
            mediaPlayer?.prepare()
            mediaPlayer?.start()
        }
    }

    fun play() {
        if (mediaPlayer?.isPlaying == false){
            mediaPlayer?.start()
        }
    }

    fun pause() {
        if (mediaPlayer?.isPlaying == true) {
            mediaPlayer?.pause()
        }
    }

    fun stop() {
        mediaPlayer?.stop()
    }
}

4.3 进行调用

当创建的时候

val audioFocusFlowManager = AudioFocusManager()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    
    audioFocusManager = AudioFocusManager(this)
    audioFocusManager.setOnAudioFocusChangeListener(object :
        AudioFocusManager.OnAudioFocusChangeListener {
        override fun onAudioFocusChange(focusChange: Int) {
            Log.i(TAG, "onAudioFocusChange:$focusChange")

            if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { //-2
                //短暂失去焦点,应该暂停音乐的播放
                Log.i(TAG, "onAudioFocusChange:短暂失去焦点,应该暂停音乐的播放")
                musicManger.pause()
            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { //-3
                //短暂失去焦点,应该降低音量继续播放
                Log.i(TAG, "onAudioFocusChange:短暂失去焦点,应该降低音量继续播放,或者根据自己的业务逻辑,可以停止播放")
                //musicManger.pause()
                //由于设置了.setWillPauseWhenDucked(false),系统自己处理了,这个分支不会被回调
                //如果想要被回调,那么需设置.setWillPauseWhenDucked(true)
            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { //-1
                //永久失去焦点,应该停止音乐的播放
                musicManger.stop()
                Log.i(TAG, "onAudioFocusChange:永久失去焦点,应该停止音乐的播放")
            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { //1
                Log.i(TAG, "onAudioFocusChange:重新获得焦点")
                musicManger.play()
            }
        }
    })
}

请求焦点,这里我已经封装了常用的三个场景

  • REQUEST_MUSIC : 播放音乐场景 : 请求焦点后,其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS
  • REQUEST_SPEAK : 语音识别场景 : 请求焦点后,其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
  • REQUEST_NAVIGATION : 导航场景 : 请求焦点后,其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
//请求焦点
val requestParams = AudioFocusManager.RequestParams.REQUEST_MUSIC
/*默认为false,如果设为false,onAudioFocusChange应该返回-3时,不会回调onAudioFocusChange,
而是系统自己已经处理了,系统会自动将音量调低,不用再由我们代码中额外处理
如果设为true,则需要自己在onAudioFocusChange里单独处理-3的逻辑*/
requestParams.willPauseWhenDucked = false
val result = audioFocusManager.requestFocus(requestParams)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    Toast.makeText(this, "获取了焦点", Toast.LENGTH_SHORT).show()
    musicManger.play(this@MainActivity, true, R.raw.music1)
}

当销毁的时候

override fun onDestroy() {
    super.onDestroy()

    //暂停播放音乐
    musicManger.stop()
    //释放音频焦点
    audioFocusManager.releaseAudioFocus()
}

5. 其他

参考文章

音频焦点
管理音频焦点
Android音频焦点处理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/647790.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HTML5 基本框架

HTML5基本的内容 文章目录 系列文章目录前言一、HTML5 基本框架二、具体框架结构三、知识补充总结 前言 HTML5的介绍: HTML5 是一种用于构建网页内容的标准化语言。它是 HTML(超文本标记语言)的第五个版本,引入了许多新的功能和特…

视频号小店去哪里找货源?最全货源渠道分享!

大家好,我是电商糖果 视频号小店因为是这两年电商行业新出来的黑马,吸引着不少商家入驻。 入驻了商家中很多都没有自己的货源渠道。 他们基本都是从无货源开始起步,后期通过积累资源,慢慢搭建属于自己的货源渠道。 可是渐渐的…

Oracle实践|内置函数之聚合函数

📫 作者简介:「六月暴雪飞梨花」,专注于研究Java,就职于科技型公司后端工程师 🏆 近期荣誉:华为云云享专家、阿里云专家博主、腾讯云优秀创作者、ACDU成员 🔥 三连支持:欢迎 ❤️关注…

SRE视角下的DevOps:构建稳定高效的软件交付流程

SRE 和 DevOps 有什么区别和联系?本文对此进行了解读,并着重从 SRE 实践出发阐述了 DevOps 的建设思路。 SRE 就是在用软件工程的思维和方法论完成以前由系统管理员团队手动完成的工作。SRE 的职责是运维一个服务,该服务由一些相关的系统组件…

解决vue3 vite打包报Root file specified for compilation问题

解决方法: 修改package.json打包命令 把 "build": "vue-tsc --noEmit && vite build" 修改为 "build": "vite build" 就可以了 另外关于allowJs这个问题,在tsconfig.json文件中配置"allowJs&qu…

基于深度学习和opencv的车牌识别系统

免费获取方式↓↓↓ 项目介绍028: 基于深度学习和opencv的车牌识别系统 同时利用对图片每一帧图像加入视频分析模块 图片分析模块可以依据界面按钮提示进行相应功能 视频分析模块可以根据按钮提示进行对视频的分析 (视频模块的视频追踪处理时间较长&…

JVM之【运行时数据区】

JVM简图 运行时数据区简图 一、程序计数器(Program Counter Register) 1.程序计数器是什么? 程序计数器是JVM内存模型中的一部分,它可以看作是一个指针,指向当前线程所执行的字节码指令的地址。每个线程在执行过程中…

AI预测体彩排3采取888=3策略+和值012路一缩定乾坤测试5月26日预测第2弹

今天继续基于8883的大底进行测试,昨天的预测已成功命中!今天继续测试,按照排三前面的规律,感觉要出对子了,所以本次预测不再杀对子,将采用杀一个和尾来代替。好了,直接上结果吧~ 首先&#xff0…

访问tomcat的webapps下war包,页面空白

SpringBootvue前后端分离项目,Vue打包到SpringBoot中 常见问题 错误一:war包访问页面空白 前提:项目在IDEA里配置tomcat可以启动访问项目 但是,打成war包拷贝到tomcat webapps下能启动却访问不了,页面显示空白 原…

YAML详情

一、kubernetes支持对象 Kubernetes支持YAML和JSON格式管理资源对象 JSON格式:主要用于api接口之间消息的传递YAML格式:用于配置和管理,YAML是一种简洁的非标记性语言,内容格式人性化,较易读 二、YAML语法格式注意点 …

LeetCode热题100—链表(一)

160.相交链表 题目 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。 图示两个链表在节点 c1 开始相交: 题目数据 保证 整个链式结构中不存在环。 注意&#x…

【css3】06-css3新特性之网页布局篇

目录 伸缩布局或者弹性布局【响应式布局】 1 设置父元素为伸缩盒子 2 设置伸缩盒子主轴方向 3 设置元素在主轴的对齐方式 4 设置元素在侧轴的对齐方式 5 设置元素是否换行显示 6 设置元素换行后的对齐方式 7 效果测试原码 伸缩布局或者弹性布局【响应式布局】 1 设置父元…

C#屏蔽基类成员

可以用与积累成员名称相同的成员来屏蔽 要让编译器知道你在故意屏蔽继承的成员,可以用new修饰符。否则程序可以成功编译,但是编译器会警告你隐藏了一个继承的成员 using System;class someClass {public string F1 "Someclass F1";public v…

ISIS协议

isis协议基础 isis概述 isis:中间系统到中间系统isis是公有协议,属于IGP协议,主要应用于一个AS(企业)自治系统内部isis是一种链路状态协议,使用SPF算法早期的isis是基于CLNP(无连接网络协议&a…

【知识蒸馏】feature-based 知识蒸馏 - - CWD(channel-wise knowledge dissillation)

一、CWD特征蒸馏介绍 大部分的KD方法都是通过algin学生网络和教师网络的归一化的feature map, 最小化feature map上的激活值的差异。 逐通道知识蒸馏(channel-wise knowledge dissillation, CWD)将每个通道的特征图归一化来得到软概率图。通过简单地最小…

一款颜值颇高的虚拟列表!差点就被埋没了,终于还是被我挖出来了

大家好,我是晓衡! 今天,推荐一款颇有颜值的虚拟列表组件,不然真的被埋没就可惜了! 我们先来看下效果: 感觉怎么样?还不错吧! 为什么说这个资源差点被埋没呢?因为个朋友找…

Java面向对象知识总结+思维导图

🔖面向对象 📖 Java作为面向对象的编程语言,我们首先必须要了解类和对象的概念,本章的所有内容和知识都是围绕类和对象展开的! ▐ 思维导图1 ▐ 类和对象的概念 • 简单来说,类就是对具有相同特征的一类事…

【全开源】多功能投票小程序(ThinkPHP+FastAdmin+Uniapp)

打造高效、便捷的投票体验 一、引言 在数字化快速发展的今天,投票作为一种常见的决策方式,其便捷性和效率性显得尤为重要。为了满足不同场景下的投票需求,我们推出了这款多功能投票小程序系统源码。该系统源码设计灵活、功能丰富&#xff0…

《AI学习笔记》大模型-微调/训练区别以及流程

阿丹: 之前一直对于大模型的微调和训练这两个名词不是很清晰,所有找了一个时间来弄明白到底有什么区别以及到底要怎么去使用去做。并且上手实践一下。 大模型业务全流程: 大模型为啥要微调?有哪些微调方式? 模型参数…

Jeecg | 如何解决 ERR Client sent AUTH, but no password is set 问题

最近在尝试Jeecg低代码开发,但是碰到了超级多的问题,不过总归是成功运行起来了。 下面说说碰到的最后一个配置问题:连接redis失败 Error starting ApplicationContext. To display the conditions report re-run your application with deb…