手机音频的输出有外放(Speaker)、听筒(Telephone Receiver)、有线耳机(WiredHeadset)、蓝牙音箱(Bluetooth A2DP)等输出设备。在平时,电话免提、插拔耳机、连接断开蓝牙设备等操作系统都会自动切换Audio音频到相应的输出设备上。比如电话免提就是从听筒切换到外放扬声器,插入耳机就是从外放切换到耳机。

场景需求

Android系统自动切换的这些策略,并不能全部满足我们的产品需求,比如音乐App需要对听歌时拔出耳机的操作进行阻止(暂停播放),防止突然切换到外放导致尴尬。

最近项目需求希望即使在连接蓝牙音箱的情况下,仍旧使用手机外放播放音频。这就需要强制切换Audio输出通道,打破系统原有的策略。

查阅资料,看到了Android中可以通过AudioManager查询、切换当前Audio输出通道,并且在Audio输出发生变化时,捕获并处理这种变化。

首先提醒下大家,使用下面的方法时,需要添加权限:

<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

Audio输出状态查询

AudioManager 提供的下列方法可以用来查询当前Audio输出的状态:

  • isBluetoothA2dpOn():检查A2DPAudio音频输出是否通过蓝牙耳机;

  • isSpeakerphoneOn():检查扬声器是否打开;

  • isWiredHeadsetOn():检查线控耳机是否连着;注意这个方法只是用来判断耳机是否是插入状态,并不能用它的结果来判定当前的Audio是通过耳机输出的,这还依赖于其他条件。

  • setSpeakerphoneOn(boolean on):直接选择外放扬声器发声;

  • setBluetoothScoOn(boolean on):要求使用蓝牙SCO耳机进行通讯;

此处根据这篇文章简单地介绍一下蓝牙耳机的两种链路,A2DP及SCO。android的api表明:

  • A2DP:是一种单向的高品质音频数据传输链路,通常用于播放立体声音乐;
  • SCO: 则是一种双向的音频数据的传输链路,该链路只支持8K及16K单声道的音频数据,只能用于普通语音的传输,若用于播放音乐那就只能呵呵了。

两者的主要区别是:A2DP只能播放,默认是打开的,而SCO既能录音也能播放,默认是关闭的。 如果要录音肯定要打开sco啦,因此调用上面的 setBluetoothScoOn(boolean on) 就可以通过蓝牙耳机录音、播放音频了,录完、播放完记得要关闭。

另外,在Android系统中通过AudioManager.setMode()方法来管理播放模式。在setMode()方法中有以下几种对应不同的播放模式:

  • MODE_NORMAL : 普通模式,既不是铃声模式也不是通话模式
  • MODE_RINGTONE : 铃声模式
  • MODE_IN_CALL : 通话模式
  • MODE_IN_COMMUNICATION : 通信模式,包括音/视频,VoIP通话.(3.0加入的,与通话模式类似)

在设置播放模式的时候,需要考虑流类型,我在这里使用的流类型是 STREAM_MUSIC ,所以切换播放设备的时候就需要设置为MODE_IN_COMMUNICATION 模式而不是 MODE_NORMAL 模式。可以参考这个问题

解决问题

使用以下方法切换音频Audio输出,参考Android : Switching audio between Bluetooth and Phone Speaker is inconsistent

AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
/**
* 切换到外放
*/
public void changeToSpeaker(){
  //注意此处,蓝牙未断开时使用MODE_IN_COMMUNICATION而不是MODE_NORMAL
  mAudioManager.setMode(bluetoothIsConnected ? AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_NORMAL);
  mAudioManager.stopBluetoothSco();
  mAudioManager.setBluetoothScoOn(false);
  mAudioManager.setSpeakerphoneOn(true);
}
/**
* 切换到蓝牙音箱
*/
public void changeToHeadset(){
  mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
  mAudioManager.startBluetoothSco();
  mAudioManager.setBluetoothScoOn(true);
  mAudioManager.setSpeakerphoneOn(false);
}
/************************************************************/
//注意:以下两个方法还未验证
/************************************************************/
/**
* 切换到耳机模式
*/
public void changeToHeadset(){
  mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
  mAudioManager.stopBluetoothSco();
  mAudioManager.setBluetoothScoOn(false);
  mAudioManager.setSpeakerphoneOn(false);
}
/**
* 切换到听筒
*/
public void changeToReceiver(){
  audioManager.setSpeakerphoneOn(false);
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
    audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
  } else {
    audioManager.setMode(AudioManager.MODE_IN_CALL);
  }
}

直接切换输出通道的方法我们已经知道了。剩下需要解决的问题是,当蓝牙设备断开、连接的时候,我们希望可以自动切换到用户原本设置的输出通道上,比如在蓝牙未连接时,用户设置的是希望通过蓝牙播报,所以应该在蓝牙一旦连接以后,就把音频切换到蓝牙设备上。

下面我们就看看如何监听蓝牙设备的连接状态。

首先注意使用前需要以下权限:

<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH" />

根据这篇文章,我们发现可以使用 AudioManager.ACTION_AUDIO_BECOMING_NOISY 这个Intent Action来监听蓝牙断开、耳机插拔的广播,但是测试发现,它也只能收到蓝牙断开的广播,无法接收到蓝牙连接的广播,所以不是我们想要的。

进一步找到这篇文章:关于蓝牙开发,必须注意的广播,总结了以下蓝牙广播。

/**
* 有注释的广播,蓝牙连接时都会用到
*/
intentFilter.addAction(BluetoothDevice.ACTION_FOUND); //搜索蓝压设备,每搜到一个设备发送一条广播
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); //配对开始时,配对成功时
intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); //配对时,发起连接
intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED);
intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); //配对结束时,断开连接
intentFilter.addAction(PAIRING_REQUEST); //配对请求(Android.bluetooth.device.action.PAIRING_REQUEST)
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); //开始搜索
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); //搜索结束。重新搜索时,会先终止搜索
intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); //本机开启、关闭蓝牙开关
intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); //蓝牙设备连接或断开
intentFilter.addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); //更改蓝牙名称,打开蓝牙时,可能会调用多次
intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_ENABLE);
intentFilter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); //搜索模式改变

我们发现了BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED 和 BluetoothAdapter.ACTION_STATE_CHANGED 这两个Intent广播。

那么这两个广播Intent的区别是什么呢?只用其中一个可以吗?查看Google文档发现

  • BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED :指的是本地蓝牙适配器的连接状态的发生改变(比如没有关闭本机蓝牙开关时,另外一个配对设备自己把连接断开)

  • BluetoothAdapter.ACTION_STATE_CHANGED :指的是本地蓝牙适配器的状态已更改。 例如,蓝牙开关打开或关闭。

换句话说,一个是用于连接状态的变化,另一个用于蓝牙适配器本身的状态变化。经过测试发现,如果只使用BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED 监听广播,则会接收不到“主动关闭本机蓝牙开关”的广播事件。但只是用BluetoothAdapter.ACTION_STATE_CHANGED 的话,很明显这时候蓝牙设备并未真正配对。

动态注册蓝牙连接、断开广播的方式如下:

  • 动态注册广播
public class BluetoothConnectionReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent){
    if (BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { //蓝牙连接状态
      int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1);
      if (state == BluetoothAdapter.STATE_CONNECTED || state == BluetoothAdapter.STATE_DISCONNECTED) {
        //连接或失联,切换音频输出(到蓝牙、或者强制仍然扬声器外放)
      }
    } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())){ //本地蓝牙打开或关闭
      int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
      if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF) {
      //断开,切换音频输出
      }
    }
  }
}
BluetoothConnectionReceiver audioNoisyReceiver = new BluetoothConnectionReceiver();
//蓝牙状态广播监听
IntentFilter audioFilter = new IntentFilter();
audioFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
audioFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
mContext.registerReceiver(audioNoisyReceiver, audioFilter);

之后,我们就可以根据上面切换音频输出通道的代码来实现蓝牙设备连接、断开以后强制打破操作系统原有的输出通道切换策略,来实现我们自己想要的切换功能了。

本文转自:Audio音频输出通道切换 - 蓝牙、外放

AudioManager: android插上耳机仍然使用扬声器播放音频的更多相关文章

  1. kali 插上耳机没声音

    这几天装kali装的真是心力憔悴,好不容易勉强可以使用了,插上耳机想要放松下,结果没有声音? excusu me? 在经历各种方法之后终于找到了解决方法,亲测可行. 1.先打开终端,输入systemc ...

  2. 笔记本电脑插上耳机声音依然外放解决方法 为什么插入HDMI线,电脑的音响就没有声音了

    笔记本电脑插上耳机声音依然外放解决方法: 下载一个驱动大师,安装声卡驱动.(驱动人生安装的驱动有可能不能用) 为什么插入HDMI线,电脑的音响就没有声音了 参考:https://zhidao.baid ...

  3. Android中通过耳机按键控制音乐播放的实现

    今天在研究Android中实现Android 4.2.2源码中的Music应用的源码,关于通过耳机按键控制音乐播放的实现,有点好奇,就仔细分析了一下源码, 主要由 MediaButtonIntentR ...

  4. 腾讯优测优分享 | 你是否体验过Android手机插入耳机后仍外放的尴尬?

    腾讯优测是专业的移动自动化测试平台,提供多维度的自动化测试服务,让测试更简单! 近期有报道称,澳大利亚悉尼市新某大学的一名男生在课堂上看电影,不料耳机没有插好,变成了现场直播... 如果你认为耳机没插 ...

  5. 强烈推荐:Android史上最强大的自定义任务软件Tasker

    强烈推荐:Android史上最强大的自定义任务软件Taskerhttp://bbs.mumayi.com/thread-28387-1-1.html(出处: 木蚂蚁手机乐园) Android上的Tas ...

  6. Android智能手机上的音频浅析

    手机可以说是现在人日常生活中最离不开的电子设备了.它自诞生以来,从模拟的发展到数字的,从1G发展到目前的4G以及不久将来的5G,从最初的只有唯一的功能(打电话)发展到目前的全功能,从功能机(featu ...

  7. 如何通过Chrome远程调试android设备上的Web网站

    网上的帖子很多,但很多都是老版本的,试过了,根本不管用,花了一天时间,终于在本机试验通过了,特记录下来,以备用.有需要的朋友也可以参考.先上一张图,看看PC端chrome上调试的效果: 左边是手机的模 ...

  8. 如何在Android平台上使用USB Audio设备

    http://blog.csdn.net/kevinx_xu/article/details/12951131 需求:USB Headset插上去后,声音要从本地CODEC切换到USB Headset ...

  9. Android智能手机上的音频浅析【转】

    本文转载自:https://blog.csdn.net/david_tym/article/details/80903385 手机可以说是现在人日常生活中最离不开的电子设备了.它自诞生以来,从模拟的发 ...

随机推荐

  1. springboot2.1.3+jacoco检测代码覆盖率

    关于 jacoco的介绍,不在本文中详细描述,简单点说,只是个代码覆盖率工具,想要了解具体的可以参考如下地址: https://www.jianshu.com/p/639e51c76544 好了,闲话 ...

  2. Termux和Ubuntu建立ssh连接

    1 本机环境 Android:Termux v0.77 作为客户端 Linux:Ubuntu 19.10 作为服务器 两者处于同一局域网下 2 ssh安装 2.1 Termux pkg install ...

  3. 解决pycharm导入pygame包问题

    在正确用pip命令安装好pygame后, 运行pycharm仍然会报错找不到pygame包, 这时候我们可以用pycharm自带的工具安装好pygame包,操作步骤如下: 点击设置,(或者ctrl+a ...

  4. 万众期待的kintone开发账号免费开放申请啦!

    亲爱的小伙伴们,等了很久很久的kintone开发账号终于可以免费申请使用了! 有人想问了,什么是kintone? kintone是指无需开发知识,即可根据公司业务轻松创建系统的Cybozu的云服务. ...

  5. python爬虫中的ip代理设置

    设置ip代理是爬虫必不可少的技巧: 查看本机ip地址:打开百度,输入“ip地址”,可以看到本机的IP地址: 本文使用的是goubanjia.com里面的免费ip: 使用时注意要注意传输协议是http还 ...

  6. Codeforces Round #560 (Div. 3) Microtransactions

    Codeforces Round #560 (Div. 3) F2. Microtransactions (hard version) 题意: 现在有一个人他每天早上获得1块钱,现在有\(n\)种商品 ...

  7. python使用print写文件

    刚刚看sumo的官方教程,有一段代码是生成配置文件,发现python中的print函数也可以写文件. with open("data/cross.rou.xml", "w ...

  8. 项目Beta冲刺 - 凡事预则立

    课程: 软件工程1916|W(福州大学) 作业要求: 项目Beta冲刺 团队名称: 火鸡堂 作业目标: 尽力交付 火鸡堂 队员学号 队员姓名 博客地址 备注 221600111 彼术向 http:// ...

  9. danci1

    oddball 英 ['ɒdbɔːl] 美 adj. 古怪的:奇怪的 n. 古怪:古怪的人 rather than 英 美 而不是:宁可…也不愿 grasp 英 [grɑːsp] 美 [ɡræsp] ...

  10. c++中关联容器set的使用

    c++中set的用法 #include<iostream> #include<vector> #include<algorithm> #include<set ...