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: AudioManagerprivate var mFocusRequest: AudioFocusRequest? = nullprivate var mAudioAttributes: AudioAttributes? = nullprivate var mAudioFocusChangeListener: OnAudioFocusChangeListener? = nullinit {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_LOSSval REQUEST_MUSIC = RequestParams(AudioAttributes.USAGE_MEDIA,AudioAttributes.CONTENT_TYPE_MUSIC,AudioManager.AUDIOFOCUS_GAIN)//语音识别场景 : 其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENTval REQUEST_SPEAK = RequestParams(AudioAttributes.USAGE_VOICE_COMMUNICATION,AudioAttributes.CONTENT_TYPE_SPEECH,AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)//导航场景 : 其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCKval 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? = nullfun play(context: Context, loop: Boolean, musicResId: Int) {if (mediaPlayer == null) {mediaPlayer = MediaPlayer.create(context, musicResId)mediaPlayer?.isLooping = loopmediaPlayer?.start()} else {mediaPlayer?.reset()mediaPlayer?.isLooping = loopval 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) { //1Log.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音频焦点处理

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【百度智能体】零代码创建你的 AI 宠物助手

前言 今天给大家介绍一下百度的 AI 产品 – 百度智能体,在文心智能体平台你可以0代码就可以创建出属于自己的 AI 机器人,几乎可以选择任何你想要的领域或者行业机器人,进行无代码打造自己的对话助手,本文将介绍文心智能体&#x…

java —— MySQL 操作

一、常用 MySQL 语句 (一)创建数据库 create database 数据库名; 例如:创建一个名为 hello 的数据库 create database hello; 注:如果起初没有任何数据库,那么在建立连接的时候,地址 String …

oracle的cbo怎么使用cbo和rule的区别

Oracle CBO的使用与RBO的区别 一、Oracle CBO的使用 Oracle的基于成本的优化器(CBO)在Oracle 7中引入,但在Oracle 8i中才变得成熟。CBO会根据SQL语句的预计成本(这里的成本主要指CPU和内存的使用)来选择最优的执行计…

【OpenCV 基础知识 18】对两图像按位与操作

cvAnd是opencv的一个库函数,功能是计算两个数组的按位与的结果。 计算两个数组的每个元素的按位与 void cvAnd( const CvArr* src1, const CvArr* src2, CvArr* dst, const CvArr* maskNULL ); src1 第一个原数组 src2 第二个原数组. dst 输出数组 mask 操作覆盖面&…

Kafka消息丢失处理方式,消息丢失与消费失败区别和分别的处理

Kafka 消息丢失的处理方式可以从生产者、Broker 和消费者三个角度来考虑,以确保消息的可靠传递。以下是一些关键的处理措施: Kafka消息丢失处理方式 1. 生产者端的处理方式: 使用生产者确认(acks配置):通…

jmeter之MD5加密接口请求教程

前言: 有时候在项目中,需要使用MD5加密的方法才可以登录,或者在某一个接口中遇到 登录获取token后才可以进行关联,下面介绍下遇到的常见使用 一、第一种方法:使用jmeter自带的函数助手digest 选择工具,选择…

高效的大型语言模型适应方法:提升基础性的解决方案

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…

jenkins升级,涉及ssh remote执行出现Algorithm negotiation fail

问题:jenkins升级,发现自由风格job和流水线在用到ssh remote命令连接远程服务器的时候出现Algorithm negotiation fail错误。如下错误:1、algorithmName"server_host_key",2、algorithmName"kex" com.jcraft…

表查询基础【mysql】【表内容 增,删,改,查询】

博客主页:花果山~程序猿-CSDN博客 文章分栏:Linux_花果山~程序猿的博客-CSDN博客MySQL之旅_花果山~程序猿的博客-CSDN博客Linux_花果山~程序猿的博客-CSDN博客 关注我一起学习,一起进步,一起探索编程的无限可能吧!让我…

【EXCEL_VBA_常用过程 函数】

结合日常工作需要,收录或改写相关过程及函数,现共享给大家,希望能对大家有所帮助! 目录 1. 过程 1.1 批量处理框架Application.GetOpenFilename 2. 功能函数 2.1 字符串数组比对 / 两组数据比对是否一致(不要求元素…

服务器硬件基础知识以及如何部署相关应用

服务器硬件是构成服务器物理基础的组件集合,它们共同工作以提供计算服务、数据存储、网络通信等功能。以下是一些关键的服务器硬件基础知识: 1. 处理器(CPU) 作用:服务器的大脑,负责执行程序指令和处理数…

Redis(十三) 事务

文章目录 前言事务的特性Redis事务的执行原理Redis中使用事务WATCH UNWATCH实现乐观锁 前言 前面我们学习 MySQL 的时候,肯定也学习了事务。事务是什么?给大家举个例子:假如我给朋友微信转账,我给他转了 100 块钱,当我…

【14】bat脚本备份mysql数据

说明 1、请注意windows的bat脚本语法 2、请注意linux和windows的格式 3、请注意时间是个位数的问题 一、参数说明 1、参数说明: setlocal ENABLEDELAYEDEXPANSION 是延迟变量 setlocal和endlocal 是必须要一起用 @echo on 是开启显示,方便调试 :: 注释 cd /d D:\dbbackup …

Golang | Leetcode Golang题解之第114题二叉树展开为链表

题目: 题解: func flatten(root *TreeNode) {curr : rootfor curr ! nil {if curr.Left ! nil {next : curr.Leftpredecessor : nextfor predecessor.Right ! nil {predecessor predecessor.Right}predecessor.Right curr.Rightcurr.Left, curr.Righ…

格式转化——Labelme标注好的json文件批量转为png(标签)文件(物体为红色,背景为黑色)和jpg原图

作用如题目,批量将标注好的json文件转成png标签,jpg原图,其中标签时红黑图。 代码如下: import argparse import base64 import json import os import os.path as osp import imgviz import PIL.Image import yaml from labelm…

Vue3实战笔记(46)—Vue 3高效开发定制化Dashboard的权威手册

文章目录 前言Dashboard开发总结 前言 后台管理系统中的Dashboard是一种图形化的信息显示工具,通常用于提供一个特定领域或系统的概况。它可以帮助用户监控和分析数据,快速获取重要信息。可以帮助用户监控业务状况、分析数据、获取关键信息和管理资源。…

list的模拟实现(一)

嗨喽大家好,时隔许久阿鑫又给大家带来了新的博客,list的模拟实现(一),下面让我们开始今天的学习吧! list的模拟实现(一) 1.list splice接口的使用 2.list尾插的实现 3.list的迭代…

Python编程的黑暗魔法:模块与包的神秘力量!

哈喽,我是阿佑,今天给大家讲讲模块与包~ 文章目录 1. 引言1.1 模块化编程的意义1.2 Python中模块与包的概念概述 2. 背景介绍2.1 Python模块系统模块的定义与作用Python标准库简介 2.2 包的结构与目的包的定义与目录结构包在项目组织中的重要性 3. 创建与…

Linux CentOs如何升级openssl

1.获取 OpenSSL 1.1.n 源码 wget https://www.openssl.org/source/openssl-1.1.1n.tar.gz tar -xzf openssl-1.1.1n.tar.gz cd openssl-1.1.1n 2.编译和安装 OpenSSL 1.1.n ./config --prefix/usr/local/openssl-1.1.1n --openssldir/usr/local/openssl-1.1.1n shared zlib m…

【C语言】strstr函数的使用和模拟

前言 今天给大家带来一个字符串函数,strstr()的使用介绍和模拟实现。 模拟实现这个函数,可以帮助我们更深刻地理解这个函数的功能和提高解决字符串相关问题的能力,有兴趣的话就请往下看吧。 strstr函数介绍 函数功能: strstr函…