React Native中Android实现蓝牙SCO录音播放+有线耳机录音播放
当前RN实现播放录音方式
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); //turn speaker on @ReactMethod public void setSpeakerphoneOn(final Double key, final Boolean speaker) { MediaPlayer player = this.playerPool.get(key); if (player != null) { AudioManager audioManager = (AudioManager)this.context.getSystemService(this.context.AUDIO_SERVICE); if(speaker){ audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); }else{ audioManager.setMode(AudioManager.MODE_NORMAL); } audioManager.setSpeakerphoneOn(speaker); } }
通过sound模块封装顺序播放控制PlayRecord.ts进行播放
import Sound from 'react-native-sound'; type ISetCurrentFilepath = {(filepath: string): void}; Sound.setCategory('Voice') export class PlayRecord { BaseUrl: string; sound = {} as Sound; playList = [] as string[]; isPlay = false as boolean; setCurrentFilepath: ISetCurrentFilepath; constructor(setCurrentFilepath: ISetCurrentFilepath, baseUrl: string) { this.setCurrentFilepath = setCurrentFilepath; this.BaseUrl = baseUrl; } push(filepath: string) { console.log('this.isPlay', this.isPlay); if (this.isPlay) { this.playList.push(filepath); } else { this.play(filepath); } } closePlay() { console.log('closePlay', this.isPlay); if (this.isPlay) { this.isPlay = false; this.setCurrentFilepath(''); this.sound.release(); } } clickPlay(filepath: string) { console.log('clickPlay,this.isPlay:', this.isPlay); if (this.isPlay) { this.sound.release(); this.isPlay = false; this.setCurrentFilepath(''); this.playList = []; } this.play(filepath); } play(filepath: string,sourceType: 'url'|'local' = 'url') { let url = this.BaseUrl + filepath; if(sourceType === 'local'){ url = filepath } this.sound = new Sound(url, Sound.CACHES, async e => { console.log('callback duration :>> ', this.sound.getDuration()); if (e) { console.log(`${url} play fail, error:${e}`); } // await this.sound.setSpeakerphoneOn(true); 通过播放采用切换到通话模式(默认听筒),并打开扬声器 this.isPlay = true; this.setCurrentFilepath(filepath); this.sound.play(() => { console.log(`${url}play success,speed:${this.sound.getSpeed()}`); this.setCurrentFilepath(''); this.isPlay = false; this.sound.release(); if (this.playList.length) { return new Promise((resolve, reject) => { this.play(this.playList.shift() as string); }); } }); }); } }
RN录制:react-native-audio-record —— 基于AudioRecord实现,通过audioSource控制录音类型,当前使用的VOICE_COMMUNICATION
new AudioRecord( audioSource, sampleRateInHz, channelConfig, audioFormat, recordingBufferSize );
使用方式:
const options = { sampleRate: 16000, // default 44100 channels: 1, // 1 or 2, default 1->AudioFormat.CHANNEL_IN_MONO 2->AudioFormat.CHANNEL_IN_STEREO bitsPerSample: 16, // 8 or 16, default 16 //android only (see below) https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION //7 -> AudioSource#VOICE_COMMUNICATION //1 -> MIC audioSource: 7, wavFile: 'test.wav', // default 'audio.wav' }; const ar = useRef(AudioRecord); ar.current.init(options); ar.current.start()
当前实现方式主要通过以下2点实现:
1播放是AudioManager类采用VOICE_COMMUNICATION模式,从听筒进行播放,再打开扬声器进行播放,实现回声消除。
2.录音采用VOICE_COMMUNICATION模式
当前正常进行录音播放没有问题,但是如果遇到耳机、蓝牙等收听播放播放时,播放录音都有问题。蓝牙(无法播放录音)、有线耳机(无法播放录音)
Android实现蓝牙播放录音方式
原生打开蓝牙Sco方式:
private AudioManager mAudioManager = null; mAudioManager = (AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE); mAudioManager.setBluetoothScoOn(true); # https://developer.android.com/reference/android/media/AudioManager#startBluetoothSco() mAudioManager.startBluetoothSco();
原生实现蓝牙播放录音
至此我们知道通过SCO模式,可以使蓝牙进入录音状态,至于无法播放的原因主要有2个
1.因为react-native-sound模块播放的时候调用了打开扬声器,导致音频从扬声器上进行播放。
2.录音时Audiorecord采用VOICE_COMMUNICATION,蓝牙耳麦离嘴巴远录音效果不好,在使用蓝牙时采用MIC(1)或者Default模式
原生实现主要控制2点,1.判断当前是否有蓝牙连接 2.蓝牙插入与断开检测
1:通过BluetoothAdapter类实现蓝牙状态信息获取
/** * 通过这个来获取是否有耳机连接 * @return * -1 无蓝牙耳机连接 * -2 蓝牙没开 * >0 已连接 */ public int getHeadSetStatus() { BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); // 蓝牙耳机 if (bluetoothAdapter == null) { // 若蓝牙耳机无连接 return -1; } else if (bluetoothAdapter.isEnabled()) { int a2dp = bluetoothAdapter.getProfileConnectionState(BluetoothProfile.A2DP); // 可操控蓝牙设备,如带播放暂停功能的蓝牙耳机 int headset = bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET); // 蓝牙头戴式耳机,支持语音输入输出 int health = bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEALTH); // 蓝牙穿戴式设备 // 查看是否蓝牙是否连接到三种设备的一种,以此来判断是否处于连接状态还是打开并没有连接的状态 int flag = -1; if (a2dp == BluetoothProfile.STATE_CONNECTED) { flag = a2dp; } else if (headset == BluetoothProfile.STATE_CONNECTED) { flag = headset; } else if (health == BluetoothProfile.STATE_CONNECTED) { flag = health; } // 说明连接上了三种设备的一种 if (flag != -1) { return flag; } } return -2; }
2:判断耳机插入需要通过原生注册广播,进行获取插入拔出状态
// 注册广播 private void registerBluetoothDeviceReceiver() { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); intentFilter.setPriority(Integer.MAX_VALUE);//设置优先级 blueToothtReceiver = new BlueToothtReceiver();//注册 mReactContext.registerReceiver(blueToothtReceiver, intentFilter); } public class BlueToothtReceiver extends BroadcastReceiver{ @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction();if(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)){//蓝牙连接成功 int currentState= intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1);//当前状态 if(currentState == BluetoothA2dp.STATE_CONNECTED) {//连接成功 // connectPromise.resolve("连接成功"); sendEvent("connectSucceeded", ""); } if(currentState == BluetoothA2dp.STATE_DISCONNECTED){//断开连接 sendEvent("connectDisconnect", ""); // connectPromise.reject("连接失败"); } } } }
原生实现耳机播放录音
这一个可以参考RN Incall的原生代码实现,与蓝牙类似、获取是否存在耳机、已经监听耳机插入拔出,进行对应的切换控制
这里我尝试后,发现ACTION_HEADSET_PLUG事件一直注册不上,注册了一获取action,app打开后就会闪退,希望有Android大咖能指出错误,谢谢
public class HeadsetModule extends ReactContextBaseJavaModule { private BroadcastReceiver wiredHeadsetReceiver; private boolean hasWiredHeadset = false; private static final String ACTION_HEADSET_PLUG = (android.os.Build.VERSION.SDK_INT >= 21) ? AudioManager.ACTION_HEADSET_PLUG : Intent.ACTION_HEADSET_PLUG; public static ReactApplicationContext reactContext; public HeadsetModule(ReactApplicationContext context) { super(context); reactContext = context; // registerBluetoothDeviceReceiver(); } @Override public String getName() { return "Headset"; } // 注册广播 private void registerBluetoothDeviceReceiver() { IntentFilter filter = new IntentFilter(ACTION_HEADSET_PLUG); wiredHeadsetReceiver = new WiredHeadsetReceiver();// 注册 reactContext.registerReceiver(wiredHeadsetReceiver, filter); } public class WiredHeadsetReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (ACTION_HEADSET_PLUG.equals(intent.getAction())) { hasWiredHeadset = intent.getIntExtra("state", 0) == 1; String deviceName = intent.getStringExtra("name"); if (deviceName == null) { deviceName = ""; } WritableMap data = Arguments.createMap(); data.putBoolean("isPlugged", (intent.getIntExtra("state", 0) == 1) ? true : false); data.putBoolean("hasMic", (intent.getIntExtra("microphone", 0) == 1) ? true : false); data.putString("deviceName", deviceName); sendEvent("WiredHeadset", data); } else { hasWiredHeadset = false; } } } public static void sendEvent(String eventName, String params) { reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params); } }
RN通过原生代码实现对应设备播放录音
通过原生实现了基础能力,接下来只要在RN中调用进行切换状态即可,下面是RN切换要点(暂不考虑耳机、蓝牙同时使用情况)
|
蓝牙断开连接触发
|
蓝牙连接成功
|
录音
|
ar,AudioResource值使用
VOICE_COMMUNICATION 模式录音,Mic模式录音范围太广,杂音多 |
MIC 方式录音 |
播放
|
使用
VOICE_COMMUNICATION 同时开启speakPhoneOn使用听筒播放 |
Voice 方式播放,使用VOICE_COMMUNICATION 同时关闭speakPhoneOn使用听筒播放 |
useEffect(() => { // 初始播放录音设备 // 初始化蓝牙监听器、自动切换播放录音模式 (async () => { let bluetoothStatus = await A2dp.getBluetoothStatus(); if(bluetoothStatus > 0){ dispatch(playRecordDeviceChange('bluetooth')) }else{ dispatch(playRecordDeviceChange('local_machine')) } A2dp.on('connectSucceeded', async () => { console.log('蓝牙连接成功'); dispatch(playRecordDeviceChange('bluetooth')) }); A2dp.on('connectDisconnect', async () => { console.log('蓝牙断开连接'); dispatch(playRecordDeviceChange('local_machine')) }); })(); return () => { if(playRecordDevice == 'bluetooth'){ A2dp.stopBluetoothSco(); console.log("关闭蓝牙SCO(app关闭)"); } }; }, []) useEffect(() => { // 初始化权限 // 初始化录音模块 AudioRecord const options = { sampleRate: 16000, // default 44100 channels: 1, // 1 or 2, default 1 bitsPerSample: 16, // 8 or 16, default 16 //7 -> AudioSource#VOICE_COMMUNICATION //1 -> MIC audioSource: 7, // android only (see below) https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION wavFile: 'test.wav', // default 'audio.wav' }; (async () => { if(!hasPermission.current) { console.log("检查权限",hasPermission.current) hasPermission.current = await checkPermission(); } if(hasPermission.current){ console.log("权限获取成功",hasPermission.current) if(playRecordDevice){ if(playRecordDevice == 'local_machine'){ await A2dp.stopBluetoothSco(); console.log("关闭蓝牙SCO(playRecordDevice变化)"); }else if(playRecordDevice == 'bluetooth'){ options.audioSource = 1 await A2dp.startBluetoothSco(); console.log("打开蓝牙SCO(playRecordDevice变化)"); } dispatch(initAudioRecord(options)); console.log("重新初始化录音模块 AudioRecord",options); let mode = await A2dp.getAudioManagerMode() console.log('AudioManager mode :>> ', mode); }else{ dispatch(initAudioRecord(options)); } }else{ console.log("权限获取失败"); Alert.alert("权限错误","软件无法使用,获取权限失败") } })(); return () => {} }, [playRecordDevice])
参考资料/代码:
https://dandelioncloud.cn/article/details/1518425101983899650
microCloudCode/react-native-a2dp: reactNative连接a2dp和开启sco (github.com)