目录
前言
最近接到个需求,不使用第三方SDK的情况下实现IM通讯,文字聊天已经通过MQTT实现,而语音功能目前想到的较好解决方案就是进行录音文件的上传下载。可能还有更好解决方案,但我目前没想到,有建议的小伙伴劳烦指导下。
前提 :
1、权限申请: 清单文件中加上:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
对应读写文件和录音权限。
2、录音文件要写到相应文件夹中,此文件夹要先创建,丢到Application或Activity的onCreate()中都可以,但一定要先创建。
代码实现流程 :
1、创建MediaRecorder对象;
2、调用setAudioSource()方法设置声音的来源,一般传入MediaRecorder.MIC;
3、调用setOutputFormat()设置所录制的音频文件的格式;
4、调用setAudioRncoder()、setAudioEncodingBitRate(int bitRate)、setAudioSamlingRate(int SamplingRate)设置所录音的编码格式、编码位率、采样率等,当然不是每个都需要,根据具体业务调整(setAudioEncodingBitRate(96000),编码位率一般是96000);
5、调用setOutputFile(String path)方法设置录制的音频文件的保存位置;
6、调用MediaRecoder对象的Prepare()方法准备录制;
7、调用MediaRecoder对象的start()方法开始录制;
8、结束后调用MediaRecoder对象的stop()方法停止录制,并调用release()方法释放资源。 示例如下:
public class TestActivity extends BaseActivity { private ActivityChatBinding testBinding; private MediaRecorder mediaRecorder; private boolean isRecorded; @Override public void initView() { testBinding = ActivityTestBinding.inflate(getLayoutInflater()); setContentView(testBinding.getRoot()); initMsgAndSth(); checkPermission(); } private void initMsgAndSth(){ String record_Home = this.getFilesDir()+"/Sample"; //声明存储路径,用绝对路径什么都可以 //btnTalk就是个Button testBinding.btnTalk.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (isRecorded) stopRecordAudio(); else startRecordAudio(record_Home); } }); } private void stopRecordAudio() { //有的5.0机型上MediaRecorder.stop会报错,这里建议抓取一下异常 if(mediaRecorder !=null){ try { mediaRecorder.stop();//停止录音 mediaRecorder.release();//释放资源 mediaRecorder =null; }catch (Exception exception){ mediaRecorder.reset();//重置 mediaRecorder.release();//释放资源 mediaRecorder =null; } Toast.makeText(this,"停止录音",Toast.LENGTH_SHORT).show(); } } private void startRecordAudio(String path) { //文件夹一定要先创建,不然报错的bug信息中是找不到这里的 File audioFile = new File(path); if (!audioFile.exists()) { audioFile.mkdirs(); } else if (!audioFile.isDirectory()) { audioFile.delete(); audioFile.mkdirs(); } File file = new File(path + "Sample.amr"); if(!file.exists()){ try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } if(mediaRecorder == null){ mediaRecorder = new MediaRecorder(); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//设置麦克风 /* * 设置保存输出文件的格式:THREE_GPP/MPEG-4/RAW_AMR/Default THREE_GPP(3gp格式 * ,H263视频/ARM音频编码)、MPEG-4、RAW_AMR(只支持音频且音频编码要求为AMR_NB) */ mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);//设置音频文件编码格式 mediaRecorder.setOutputFile(path+"Sample.amr"); } try { mediaRecorder.prepare(); //start之前要先prepare mediaRecorder.start(); isRecorded = true; Toast.makeText(this,"开始录音",Toast.LENGTH_SHORT).show(); } catch (IllegalStateException el){ el.printStackTrace(); } catch (RuntimeException e){ e.printStackTrace(); } catch (Exception e){ e.printStackTrace(); } } /** * 简单的权限申请逻辑 */ private void checkPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { String[] permissions = new String[]{Manifest.permission.RECORD_AUDIO,Manifest.permission.WRITE_EXTERNAL_STORAGE}; for (String permission : permissions) { if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, permissions, 200); return; } } } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode,permissions,grantResults); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && requestCode == 200) { for (int i = 0; i < permissions.length; i++) { if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { Intent intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", getPackageName(), null); intent.setData(uri); startActivityForResult(intent, 200); return; } } } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK && requestCode == 200) { checkPermission(); } } }
相关API差异已经写的很详细了,布局很简单,这里就不贴出来了。这里要特别注意的是调用顺序不能改变,否则容易报错,且因为调用顺序不对而报错的提示信息也不一定足够去定位问题。
踩坑
按照前面的提示,觉得避开所有坑能愉快的玩耍了,结果运行报错,有的机型还不打印特定日志,只能自己去鼓捣。
1、Android Q:
有的时候根据报错分类,还是能抓到点蛛丝马迹。如果是Android Q的设备,报IO异常或者Permission Denied错误,则要检查下清单文件中application标签里有没有这句:
android:requestLegacyExternalStorage="true"
没有的话一定要加上。
原因在于安卓10开始,要想访问外部存储的所有文件,除了动态申请权限 和 权限申明外,必须在主工程AndroidManifest.xml中加上这句,用于申请外部存储所有文件的权限。
2、RuntimeException:setAudioSource failed
如果程序运行看到
RuntimeException: setAudioSource failed
报错,请确保申请权限相关逻辑正确,还有清单文件中相关权限的申请,但如果(虽然是极少概率,但我碰到了)添加权限后,依旧还报这个错,请进入手机设置-应用,找到你发布上去的应用,给其授权。部分机型在调试过程中除了第一次会提示授权外,再次安装则不会再提示,这就相当于用户没有授予相关的录音和sdcard读写权限,程序依然会报错。所以,建议每次开始进行录音等逻辑前,进行一次逻辑判断。
此外还有一种情况会出现此报错,在录音结束后没有调用mediaRecorder.release()去释放资源,而又处于stop状态,这时候再去prepare、start容易报此错,此时报错打印堆栈与原先堆栈报错信息差别不大,较难定位,因此要格外注意。
3、MediaRecorder: stop failed
在调用start()后,马上进行调用stop()的操作,由于没有生成有效的音频或是视频数据,会报此错误。这个情景在即时通讯过程中很常见,可以通过让其线程睡眠小段时间(建议最少1秒),再stop()。官方文档注释对此也有解释:Note that a RuntimeException is intentionally thrown to the application, if no valid audio/video data has been received when stop() is called.