• Android SIP软电话,通话录音,VoIP电话,linphone电话


    各位大佬好,我又来记笔记了~~

    公司又提新需求了,需要开发一个能通话(呼叫客户的手机号码)自动录音的模块。刚接触这个是蒙的,经过一番研究,可实现通话录音的方式大致有下面几种:

      方案一:点击拨号时,调用系统的拨号功能,同时应用内注册通话广播,检测通话状态,接通、挂断来决定开始录音和停止录音,录音可以使用MediaRecorder和AudioRecorder。

            优缺点:实现方式简单,开发容易。但是缺点也有,受Android系统版本影响大,每次打开应用都需要进设置页面开启“无障碍”权限才能录音(目前Android8.0的不用),录音对方的声音较小。不过适当优化下 也能用。

     方案二:刷机,获取设备root权限,把应用修改为“系统”级别应用,就可以正常录制通话(跟手机自带的通话录音一样),具体怎么刷机自行百度

             优缺点:参考手机自带的通话录音功能,效果还是非常好的,但是只能用于一些定制的设备。如正常的一些手机、pad用户就不得行了,因为客户不可能会去刷机来兼容我们的应用。

     方案三: SIP软电话,集成第三方的VoIP网络电话,实现网络通话并录音,效果也还行。如linphone框架,也是本文要讲的。

          优缺点:使用SIP软电话,前提是要有SIP服务器(网上有很多免费的SIP服务器),后面说具体的实现逻辑,通话录音还可以,双方声音都比较大。

     方案四:呼叫时,点击开启系统的录音进行录制,返回我们应用时,把系统的录音文件拿出来展示或上传服务器,哈哈 最笨的方案了,适配主流的机型(前提是手机支持通话录音,获取录音文件的路径各机型适配一下)。

          优缺点:兼容性差,不推荐了。

              

     本文主要记录的是 《方案一》 和《方案三》,下面 只介绍关键步骤,详见文末demo

       方案一:

          大致步骤:   1、权限申请

                            2、注册广播,开启服务进行录音

                            3、开始拨号 

                            4、查看通话记录,播放录音文件

                         

    需要的权限,项目全部权限在这了,有的可能用不到。

    1. <uses-permission android:name="android.permission.INTERNET" />
    2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    3. <uses-permission android:name="android.permission.RECORD_AUDIO" />
    4. <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    5. <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    6. <uses-permission android:name="android.permission.CALL_PHONE" />
    7. <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
    8. <uses-permission android:name="android.permission.READ_CALL_LOG" />
    9. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    10. <uses-permission
    11. android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
    12. tools:ignore="ScopedStorage" />
    13. <uses-permission
    14. android:name="android.permission.MODIFY_PHONE_STATE"
    15. tools:ignore="ProtectedPermissions" />
    16. <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    17. <uses-permission
    18. android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
    19. tools:ignore="ProtectedPermissions" />
    20. <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    注册广播:

    AndroidManifest文件添加 PhoneStateListener和MediaRecorderService

    1. android:name=".callrecord.PhoneStateListener"
    2. android:enabled="true"
    3. android:exported="true">
    4. "android.intent.action.PHONE_STATE" />
    5. android:name=".callrecord.MediaRecorderService"
    6. android:enabled="true"
    7. android:exported="true"
    8. android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
    9. "android.accessibilityservice.AccessibilityService" />
    10. android:name="android.accessibilityservice"
    11. android:resource="@xml/accessibility_service_config" />

    PhoneStateListener类:

    1. /**
    2. * @ClassName PhoneStateListener
    3. * @Description TODO
    4. * @Author HK.W 通话录音广播
    5. * @Date 2022/10/15 22:13
    6. */
    7. public class PhoneStateListener extends BroadcastReceiver {
    8. private static final String TAG = "通话状态监听";
    9. static boolean incoming_flag;
    10. private Context mContext;
    11. @Override
    12. public void onReceive(Context ctx, Intent intent) {
    13. mContext = ctx;
    14. String event = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
    15. Log.d(TAG, "通话状态:state:" + event);
    16. if (event.equals(TelephonyManager.EXTRA_STATE_RINGING)) {
    17. Log.d(TAG, "-->RINGING--正在响铃");
    18. incoming_flag = true;
    19. } else if (event.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)) {
    20. Log.d(TAG, "-->EXTRA_STATE_OFFHOOK--正在通话");
    21. startService(ctx, intent);
    22. } else if (event.equals(TelephonyManager.EXTRA_STATE_IDLE)) {
    23. Log.d(TAG, "-->EXTRA_STATE_IDLE--电话挂断--空闲");
    24. ctx.stopService(new Intent(ctx, MediaRecorderService.class));
    25. //AudioRecordUtil.getInstance().stopRecording();
    26. AudioRecorder.getInstance().stopRecord();
    27. }
    28. }
    29. private void startService(Context context, Intent intent) {
    30. Log.d(TAG, "-->startService--打开服务-检查权限");
    31. String[] PERMISSIONS = {Manifest.permission.RECORD_AUDIO,
    32. Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};
    33. if (hasPermissions(context, PERMISSIONS)) {
    34. Log.d(TAG, "-->startService--打开服务-权限已打开");
    35. intent.setClass(context, MediaRecorderService.class);
    36. intent.putExtra("incoming_flag", incoming_flag);
    37. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    38. context.startForegroundService(intent);
    39. } else {
    40. context.startService(intent);
    41. }
    42. } else {
    43. Log.d(TAG, "-->startService--打开服务-权限未打开");
    44. }
    45. }
    46. public static boolean hasPermissions(Context context, String... permissions) {
    47. if (context != null && permissions != null) {
    48. for (String permission : permissions) {
    49. if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
    50. return false;
    51. }
    52. }
    53. }
    54. return true;
    55. }
    56. }

    MediaRecorderService类:

    1. public class MediaRecorderService extends AccessibilityService {
    2. private static final String TAG = "通话状态监听";
    3. NotificationManagerCompat notificationManager;
    4. private boolean incoming_flag;
    5. private String number;
    6. @Override
    7. public void onInterrupt() {
    8. }
    9. @Override
    10. public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
    11. }
    12. @Override
    13. public int onStartCommand(Intent intent, int flags, int startId) {
    14. if (intent != null) {
    15. Log.d(TAG, "-->startService--进入录音服务");
    16. number = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
    17. incoming_flag = intent.getBooleanExtra("incoming_flag", false);
    18. String phone = SpUtils.getInstance().getString(this, "phone", "Unknown");
    19. AudioRecorder.getInstance().createDefaultAudio(phone);
    20. AudioRecorder.getInstance().startRecord(new RecordStreamListener() {
    21. @Override
    22. public void recordOfByte(byte[] data, int begin, int end) {
    23. Log.d(TAG, "data:" + data);
    24. }
    25. });
    26. notificationBuilder();
    27. }
    28. return START_STICKY;
    29. }
    30. private void notificationBuilder() {
    31. Log.d(TAG, "-->startService--录音服务--打开通知栏,让服务进入前台,避免被杀掉");
    32. if (Build.VERSION.SDK_INT >= 26) {
    33. String CHANNEL_ID = "my_channel_01";
    34. NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Channel title",
    35. NotificationManager.IMPORTANCE_DEFAULT);
    36. ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
    37. Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
    38. .setContentTitle("")
    39. .setContentText("").build();
    40. startForeground(1, notification);
    41. } else {
    42. NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "CHANNEL_ID")
    43. .setSmallIcon(R.mipmap.ic_launcher)
    44. .setContentTitle("Recording")
    45. .setPriority(NotificationCompat.PRIORITY_DEFAULT)
    46. .setOngoing(true);
    47. notificationManager = NotificationManagerCompat.from(this);
    48. notificationManager.notify(1, builder.build());
    49. }
    50. }
    51. @Override
    52. public void onDestroy() {
    53. super.onDestroy();
    54. Log.d(TAG, "-->startService--录音服务--服务被销毁---onDestroy()");
    55. stopRecording();
    56. }
    57. private void stopRecording() {
    58. Log.d(TAG, "-->startService--录音服务--停止录音");
    59. if (Build.VERSION.SDK_INT >= 26) {
    60. stopForeground(true);
    61. } else {
    62. NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    63. notificationManager.cancel(1);
    64. }
    65. }
    66. }

    功能相关页面截图:

     拨号:

    1. private void callPhone(String phoneNumber) {
    2. Intent intentPhone = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" +
    3. phoneEt.getText().toString()));
    4. startActivity(intentPhone);
    5. }

     开始录音

    1. @Override
    2. public int onStartCommand(Intent intent, int flags, int startId) {
    3. if (intent != null) {
    4. Log.d(TAG, "-->startService--进入录音服务");
    5. number = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
    6. incoming_flag = intent.getBooleanExtra("incoming_flag", false);
    7. String phone = SpUtils.getInstance().getString(this, "phone", "Unknown");
    8. //开始录音
    9. AudioRecorder.getInstance().createDefaultAudio(phone);
    10. AudioRecorder.getInstance().startRecord(new RecordStreamListener() {
    11. @Override
    12. public void recordOfByte(byte[] data, int begin, int end) {
    13. Log.d(TAG, "data:" + data);
    14. }
    15. });
    16. notificationBuilder();
    17. }
    18. return START_STICKY;
    19. }

    停止录音:

    1. public class PhoneStateListener extends BroadcastReceiver {
    2. private static final String TAG = "通话状态监听";
    3. static boolean incoming_flag;
    4. private Context mContext;
    5. @Override
    6. public void onReceive(Context ctx, Intent intent) {
    7. mContext = ctx;
    8. String event = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
    9. Log.d(TAG, "通话状态:state:" + event);
    10. if (event.equals(TelephonyManager.EXTRA_STATE_RINGING)) {
    11. Log.d(TAG, "-->RINGING--正在响铃");
    12. incoming_flag = true;
    13. } else if (event.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)) {
    14. Log.d(TAG, "-->EXTRA_STATE_OFFHOOK--正在通话");
    15. startService(ctx, intent);
    16. } else if (event.equals(TelephonyManager.EXTRA_STATE_IDLE)) {
    17. Log.d(TAG, "-->EXTRA_STATE_IDLE--电话挂断--空闲");
    18. ctx.stopService(new Intent(ctx, MediaRecorderService.class));
    19. //AudioRecordUtil.getInstance().stopRecording();
    20. //为什么不在服务里面停止录音?有的机型挂断电话后没有马上销毁服务,所以在状态这里直接停止录音
    21. AudioRecorder.getInstance().stopRecord();
    22. }
    23. }

    本文demo 录音文件保存在根目录anyi.phone/record 文件下。

    获取通话记录对应的录音文件:

    1. /**
    2. * 获取录音文件路径 --通话记录
    3. */
    4. private List getLocalRecord() {
    5. List contacts = readContacts();
    6. List list = new ArrayList<>();
    7. JSONArray allFiles = getAllFiles("", "wav");
    8. //Log.d("allFiles", "allFiles:" + allFiles.toString());
    9. if (null != allFiles) {
    10. for (int i = 0; i < allFiles.length(); i++) {
    11. try {
    12. JSONObject jsonObject = allFiles.getJSONObject(i);
    13. String name = jsonObject.getString("name");
    14. String path = jsonObject.getString("path");
    15. String[] split1 = name.split("-");
    16. if (split1.length > 0) {
    17. RecordBean recordBean = new RecordBean();
    18. recordBean.setNumber(split1[0]);
    19. recordBean.setPath(path);
    20. recordBean.setDate(new SimpleDateFormat("HH:mm").format(new Date(Long.parseLong(split1[1]))));
    21. if (contacts.size() > 0) {
    22. for (ContactsBean b : contacts) {
    23. if (split1[0].equals(b.getNumber())) {
    24. recordBean.setCachedName(b.getName());
    25. }
    26. }
    27. } else {
    28. recordBean.setCachedName("未知");
    29. }
    30. list.add(recordBean);
    31. }
    32. } catch (JSONException e) {
    33. e.printStackTrace();
    34. }
    35. }
    36. Collections.reverse(list);
    37. return list;
    38. }
    39. return list;
    40. }
    41. public static JSONArray getAllFiles(String dirPath, String _type) {
    42. dirPath = "/storage/emulated/0/anyi.phone/record/";
    43. File f = new File(dirPath);
    44. if (!f.exists()) {//判断路径是否存在
    45. return null;
    46. }
    47. File[] files = f.listFiles();
    48. if (files == null) {//判断权限
    49. return null;
    50. }
    51. JSONArray fileList = new JSONArray();
    52. for (File _file : files) {//遍历目录
    53. if (_file.isFile() && (_file.getName().endsWith("amr")||_file.getName().endsWith("wav"))) {
    54. String _name = _file.getName();
    55. String filePath = _file.getAbsolutePath();//获取文件路径
    56. String fileName = _file.getName().substring(0, _name.length() - 4);//获取文件名
    57. try {
    58. JSONObject _fInfo = new JSONObject();
    59. _fInfo.put("name", fileName);
    60. _fInfo.put("path", filePath);
    61. fileList.put(_fInfo);
    62. } catch (Exception e) {
    63. }
    64. } else if (_file.isDirectory()) {//查询子目录
    65. //getAllFiles(_file.getAbsolutePath(), _type);
    66. } else {
    67. }
    68. }
    69. return fileList;
    70. }

    播放:

    1. private void initPlay() {
    2. mediaPlayer = new MediaPlayer();
    3. }
    4. private void startPlay(String path) {
    5. if (TextUtils.isEmpty(path)) {
    6. Toast.makeText(this, "文件路径不存在", Toast.LENGTH_LONG).show();
    7. return;
    8. }
    9. mediaPlayer.reset(); //清空里面的其他歌曲
    10. try {
    11. mediaPlayer.setDataSource(path);
    12. mediaPlayer.prepare(); //准备就绪
    13. mediaPlayer.start(); //开始唱歌
    14. } catch (IOException e) {
    15. e.printStackTrace();
    16. }
    17. }

    方案三,SIP通话录音,linphone 为例,只调试了音频通话,视频通话未调试

     前提准备

    准备一个SIP服务器地址和一个账号密码。可以自己搭建SIP服务器或者网上找一个SIP服务器注册 一个账号密码。下面是网上找的资源,没试过。因为我们公司是购买的有SIP话机服务器的。

    1. 免费sip账号注册地址 http://serweb.iptel.org/user/reg/index.php
    2. 免费sip服务器 iptel.org
    3. 免费sip客户端 http://www.fring.com

            

    正文:

    1、把linphone-sdk-android-4.3.0-beta.aar包放在项目libs,提取码: nq6q。

    2、配置文件注册服务:

    1. android:name=".linphone.LinphoneService"
    2. android:enabled="true"
    3. android:exported="true"
    4. android:label="@string/app_name" />

    3.在启动页 启动SIP相关服务,

    启动页:

    1. public class LauncherActivity extends AppCompatActivity {
    2. private static final String TAG = "XXPermissions";
    3. private Handler mHandler;
    4. @Override
    5. protected void onCreate(Bundle savedInstanceState) {
    6. super.onCreate(savedInstanceState);
    7. setContentView(R.layout.activity_launcher);
    8. mHandler = new Handler();
    9. }
    10. @Override
    11. protected void onStart() {
    12. super.onStart();
    13. getPermission();
    14. }
    15. private void getPermission() {
    16. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    17. XXPermissions.with(this)
    18. .permission(allPermission)
    19. .request(new OnPermissionCallback() {
    20. @Override
    21. public void onGranted(List permissions, boolean all) {
    22. if (all) {
    23. if (LinphoneService.isReady()) {
    24. onServiceReady();
    25. } else {
    26. startService(new Intent().setClass(LauncherActivity.this, LinphoneService.class));
    27. new ServiceWaitThread().start();
    28. }
    29. }
    30. }
    31. @Override
    32. public void onDenied(List permissions, boolean never) {
    33. if (never) {
    34. Log.e(TAG, "onDenied:被永久拒绝授权,请手动授予权限 ");
    35. } else {
    36. Log.e(TAG, "onDenied: 权限获取失败");
    37. }
    38. }
    39. });
    40. } else {
    41. if (LinphoneService.isReady()) {
    42. onServiceReady();
    43. } else {
    44. startService(new Intent().setClass(LauncherActivity.this, LinphoneService.class));
    45. new ServiceWaitThread().start();
    46. }
    47. }
    48. }
    49. private void onServiceReady() {
    50. Intent intent = new Intent();
    51. intent.setClass(LauncherActivity.this, MainActivity.class);
    52. if (getIntent() != null && getIntent().getExtras() != null) {
    53. intent.putExtras(getIntent().getExtras());
    54. }
    55. intent.setAction(getIntent().getAction());
    56. intent.setType(getIntent().getType());
    57. startActivity(intent);
    58. }
    59. private class ServiceWaitThread extends Thread {
    60. public void run() {
    61. while (!LinphoneService.isReady()) {
    62. try {
    63. sleep(30);
    64. } catch (InterruptedException e) {
    65. throw new RuntimeException("waiting thread sleep() has been interrupted");
    66. }
    67. }
    68. mHandler.post(new Runnable() {
    69. @Override
    70. public void run() {
    71. onServiceReady();
    72. }
    73. });
    74. }
    75. }
    76. }

    首页activity  onResume()方法中检测 账号是否注册,未注册跳转到注册页面:

    1. @Override
    2. protected void onResume() {
    3. super.onResume();
    4. Log.d(TAG, "onResume()");
    5. LinphoneService.getCore().addListener(mCoreListener);
    6. ProxyConfig proxyConfig = LinphoneService.getCore().getDefaultProxyConfig();
    7. if (proxyConfig != null) {
    8. updateLed(proxyConfig.getState());
    9. } else {
    10. startActivity(new Intent(this, ConfigureAccountActivity.class));
    11. }
    12. }

    注册:

    1. /**
    2. * 注册
    3. */
    4. private void configureAccount() {
    5. mAccountCreator.setUsername(mUsername.getText().toString());
    6. mAccountCreator.setDomain(mDomain.getText().toString());
    7. mAccountCreator.setPassword(mPassword.getText().toString());
    8. switch (mTransport.getCheckedRadioButtonId()) {
    9. case R.id.transport_udp:
    10. mAccountCreator.setTransport(TransportType.Udp);
    11. break;
    12. case R.id.transport_tcp:
    13. mAccountCreator.setTransport(TransportType.Tcp);
    14. break;
    15. case R.id.transport_tls:
    16. mAccountCreator.setTransport(TransportType.Tls);
    17. break;
    18. }
    19. ProxyConfig cfg = mAccountCreator.createProxyConfig();
    20. LinphoneService.getCore().setDefaultProxyConfig(cfg);
    21. }
    22. public void listener(){
    23. mCoreListener = new CoreListenerStub() {
    24. /**
    25. * 监听注册是否成功
    26. * @param core
    27. * @param cfg
    28. * @param state
    29. * @param message
    30. */
    31. @Override
    32. public void onRegistrationStateChanged(Core core, ProxyConfig cfg, RegistrationState state, String message) {
    33. registerPr.setVisibility(View.GONE);
    34. if (state == RegistrationState.Ok) {
    35. finish();
    36. } else if (state == RegistrationState.Failed) {
    37. Toast.makeText(ConfigureAccountActivity.this, "Failure: " + message, Toast.LENGTH_LONG).show();
    38. }
    39. }
    40. };
    41. }

    注册成功开始通话:

    1. private void sipCallIng() {
    2. Core core = LinphoneService.getCore();
    3. Address addressToCall = core.interpretUrl(phoneEt.getText().toString());
    4. CallParams params = core.createCallParams(null);
    5. params.enableVideo(false);
    6. if (addressToCall != null) {
    7. String filePath = AudioRecordUtil.getInstance().getFilename(phoneEt.getText().toString(), ".wav");
    8. android.util.Log.d("linPhone--", "开始呼叫--号码--filePath = " + filePath);
    9. //重要:通话前需要设置录音文件,要不不会录音,
    10. params.setRecordFile(filePath);
    11. core.inviteAddressWithParams(addressToCall, params);
    12. Intent intent = new Intent(getActivity(), CallActivity.class);
    13. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    14. startActivity(intent);
    15. }
    16. }

    开始录音:

    1. /**
    2. * ---通话接通--开始录音
    3. */
    4. private void startRecord() {
    5. android.util.Log.d("linPhone--", "接通或者拒绝");
    6. android.util.Log.d("linPhone--", "开始录音:录音地址:" + core.getRecordFile());
    7. call.startRecording();
    8. }

    停止录音:

    1. /**
    2. * ---通话挂断--停止录音--销毁页面
    3. */
    4. private void stopRecord() {
    5. android.util.Log.d("linPhone--", "挂断,未接");
    6. android.util.Log.d("linPhone--", "停止录音");
    7. call.stopRecording();//停止录音
    8. finish();//挂断电话-销毁页面
    9. }

    后面就是拿到录音文件播放,-----具体就不说了,

    研究SIP也用了大量时间和下载了很多大佬的资源,也花费了很多积分,

    so  想要demo的朋友们也希望支持一下,

    demo需要积分下载,具体多少由平台分配。

    本文demo成功实现了两种主流的通话录音方式,应该是能满足你们的业务需求的,

    demo传送门---

  • 相关阅读:
    使用hyper-V 编译和调试Android13(android-13.0.0_r3)源码
    带池化注意力 Strip Pooling | Rethinking Spatial Pooling for Scene Parsing
    Linux 文件 & 目录管理 & 链接
    C++软件异常排查从入门到精通
    学习Git---20分钟git快速上手
    Linux中查看并删除端口
    【C#项目】使用百度ai人脸库实现人脸识别
    HCIP练习03(重发布)
    视野修炼-技术周刊第56期
    我做了几年的Android应用层开发,为什么还要去学习安卓系统知识?
  • 原文地址:https://blog.csdn.net/m13984458297/article/details/127534688