当前位置:   article > 正文

Android SIP软电话,通话录音,VoIP电话,linphone电话_sip安卓客户端

sip安卓客户端

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

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

  方案一:点击拨号时,调用系统的拨号功能,同时应用内注册通话广播,检测通话状态,接通、挂断来决定开始录音和停止录音,录音可以使用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. <receiver
  2. android:name=".callrecord.PhoneStateListener"
  3. android:enabled="true"
  4. android:exported="true">
  5. <intent-filter>
  6. <action android:name="android.intent.action.PHONE_STATE" />
  7. </intent-filter>
  8. </receiver>
  9. <service
  10. android:name=".callrecord.MediaRecorderService"
  11. android:enabled="true"
  12. android:exported="true"
  13. android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
  14. <intent-filter>
  15. <action android:name="android.accessibilityservice.AccessibilityService" />
  16. </intent-filter>
  17. <meta-data
  18. android:name="android.accessibilityservice"
  19. android:resource="@xml/accessibility_service_config" />
  20. </service>

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<RecordBean> getLocalRecord() {
  5. List<ContactsBean> contacts = readContacts();
  6. List<RecordBean> 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. <service
  2. android:name=".linphone.LinphoneService"
  3. android:enabled="true"
  4. android:exported="true"
  5. 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<String> 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<String> 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传送门---

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/128746
推荐阅读
相关标签
  

闽ICP备14008679号