Android SIP软电话,通话录音,VoIP电话,linphone电话
各位大佬好,我又來記筆記了~~
公司又提新需求了,需要開發一個能通話(呼叫客戶的手機號碼)自動錄音的模塊。剛接觸這個是蒙的,經過一番研究,可實現通話錄音的方式大致有下面幾種:
? 方案一:點擊撥號時,調用系統的撥號功能,同時應用內注冊通話廣播,檢測通話狀態,接通、掛斷來決定開始錄音和停止錄音,錄音可以使用MediaRecorder和AudioRecorder。
? ? ? ? 優缺點:實現方式簡單,開發容易。但是缺點也有,受Android系統版本影響大,每次打開應用都需要進設置頁面開啟“無障礙”權限才能錄音(目前Android8.0的不用),錄音對方的聲音較小。不過適當優化下 也能用。
?方案二:刷機,獲取設備root權限,把應用修改為“系統”級別應用,就可以正常錄制通話(跟手機自帶的通話錄音一樣),具體怎么刷機自行百度
? ? ? ? ?優缺點:參考手機自帶的通話錄音功能,效果還是非常好的,但是只能用于一些定制的設備。如正常的一些手機、pad用戶就不得行了,因為客戶不可能會去刷機來兼容我們的應用。
?方案三:?SIP軟電話,集成第三方的VoIP網絡電話,實現網絡通話并錄音,效果也還行。如linphone框架,也是本文要講的。
? ? ? 優缺點:使用SIP軟電話,前提是要有SIP服務器(網上有很多免費的SIP服務器),后面說具體的實現邏輯,通話錄音還可以,雙方聲音都比較大。
?方案四:呼叫時,點擊開啟系統的錄音進行錄制,返回我們應用時,把系統的錄音文件拿出來展示或上傳服務器,哈哈 最笨的方案了,適配主流的機型(前提是手機支持通話錄音,獲取錄音文件的路徑各機型適配一下)。
? ? ? 優缺點:兼容性差,不推薦了。
? ? ? ? ??
?本文主要記錄的是 《方案一》 和《方案三》,下面?只介紹關鍵步驟,詳見文末demo
? ?方案一:
? ? ? 大致步驟:? ?1、權限申請
? ? ? ? ? ? ? ? ? ? ? ??2、注冊廣播,開啟服務進行錄音
? ? ? ? ? ? ? ? ? ? ?? ?3、開始撥號?
? ? ? ? ? ? ? ? ? ? ? ??4、查看通話記錄,播放錄音文件
? ? ? ? ? ? ? ? ? ? ?
需要的權限,項目全部權限在這了,有的可能用不到。
<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.READ_PHONE_STATE" /><uses-permission android:name="android.permission.FOREGROUND_SERVICE" /><uses-permission android:name="android.permission.CALL_PHONE" /><uses-permission android:name="android.permission.WRITE_CALL_LOG" /><uses-permission android:name="android.permission.READ_CALL_LOG" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permissionandroid:name="android.permission.MANAGE_EXTERNAL_STORAGE"tools:ignore="ScopedStorage" /><uses-permissionandroid:name="android.permission.MODIFY_PHONE_STATE"tools:ignore="ProtectedPermissions" /><uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /><uses-permissionandroid:name="android.permission.BIND_ACCESSIBILITY_SERVICE"tools:ignore="ProtectedPermissions" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />注冊廣播:
AndroidManifest文件添加?PhoneStateListener和MediaRecorderService
<receiverandroid:name=".callrecord.PhoneStateListener"android:enabled="true"android:exported="true"><intent-filter><action android:name="android.intent.action.PHONE_STATE" /></intent-filter> </receiver><serviceandroid:name=".callrecord.MediaRecorderService"android:enabled="true"android:exported="true"android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"><intent-filter><action android:name="android.accessibilityservice.AccessibilityService" /></intent-filter><meta-dataandroid:name="android.accessibilityservice"android:resource="@xml/accessibility_service_config" /></service>PhoneStateListener類:
/*** @ClassName PhoneStateListener* @Description TODO* @Author HK.W 通話錄音廣播* @Date 2022/10/15 22:13*/ public class PhoneStateListener extends BroadcastReceiver {private static final String TAG = "通話狀態監聽";static boolean incoming_flag;private Context mContext;@Overridepublic void onReceive(Context ctx, Intent intent) {mContext = ctx;String event = intent.getStringExtra(TelephonyManager.EXTRA_STATE);Log.d(TAG, "通話狀態:state:" + event);if (event.equals(TelephonyManager.EXTRA_STATE_RINGING)) {Log.d(TAG, "-->RINGING--正在響鈴");incoming_flag = true;} else if (event.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)) {Log.d(TAG, "-->EXTRA_STATE_OFFHOOK--正在通話");startService(ctx, intent);} else if (event.equals(TelephonyManager.EXTRA_STATE_IDLE)) {Log.d(TAG, "-->EXTRA_STATE_IDLE--電話掛斷--空閑");ctx.stopService(new Intent(ctx, MediaRecorderService.class));//AudioRecordUtil.getInstance().stopRecording();AudioRecorder.getInstance().stopRecord();}}private void startService(Context context, Intent intent) {Log.d(TAG, "-->startService--打開服務-檢查權限");String[] PERMISSIONS = {Manifest.permission.RECORD_AUDIO,Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};if (hasPermissions(context, PERMISSIONS)) {Log.d(TAG, "-->startService--打開服務-權限已打開");intent.setClass(context, MediaRecorderService.class);intent.putExtra("incoming_flag", incoming_flag);if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {context.startForegroundService(intent);} else {context.startService(intent);}} else {Log.d(TAG, "-->startService--打開服務-權限未打開");}}public static boolean hasPermissions(Context context, String... permissions) {if (context != null && permissions != null) {for (String permission : permissions) {if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {return false;}}}return true;} }MediaRecorderService類:
public class MediaRecorderService extends AccessibilityService {private static final String TAG = "通話狀態監聽";NotificationManagerCompat notificationManager;private boolean incoming_flag;private String number;@Overridepublic void onInterrupt() {}@Overridepublic void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {if (intent != null) {Log.d(TAG, "-->startService--進入錄音服務");number = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);incoming_flag = intent.getBooleanExtra("incoming_flag", false);String phone = SpUtils.getInstance().getString(this, "phone", "Unknown");AudioRecorder.getInstance().createDefaultAudio(phone);AudioRecorder.getInstance().startRecord(new RecordStreamListener() {@Overridepublic void recordOfByte(byte[] data, int begin, int end) {Log.d(TAG, "data:" + data);}});notificationBuilder();}return START_STICKY;}private void notificationBuilder() {Log.d(TAG, "-->startService--錄音服務--打開通知欄,讓服務進入前臺,避免被殺掉");if (Build.VERSION.SDK_INT >= 26) {String CHANNEL_ID = "my_channel_01";NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Channel title",NotificationManager.IMPORTANCE_DEFAULT);((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID).setContentTitle("").setContentText("").build();startForeground(1, notification);} else {NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "CHANNEL_ID").setSmallIcon(R.mipmap.ic_launcher).setContentTitle("Recording").setPriority(NotificationCompat.PRIORITY_DEFAULT).setOngoing(true);notificationManager = NotificationManagerCompat.from(this);notificationManager.notify(1, builder.build());}}@Overridepublic void onDestroy() {super.onDestroy();Log.d(TAG, "-->startService--錄音服務--服務被銷毀---onDestroy()");stopRecording();}private void stopRecording() {Log.d(TAG, "-->startService--錄音服務--停止錄音");if (Build.VERSION.SDK_INT >= 26) {stopForeground(true);} else {NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);notificationManager.cancel(1);}} }功能相關頁面截圖:
?撥號:
private void callPhone(String phoneNumber) {Intent intentPhone = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + phoneEt.getText().toString()));startActivity(intentPhone);}?開始錄音
@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {if (intent != null) {Log.d(TAG, "-->startService--進入錄音服務");number = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);incoming_flag = intent.getBooleanExtra("incoming_flag", false);String phone = SpUtils.getInstance().getString(this, "phone", "Unknown");//開始錄音AudioRecorder.getInstance().createDefaultAudio(phone);AudioRecorder.getInstance().startRecord(new RecordStreamListener() {@Overridepublic void recordOfByte(byte[] data, int begin, int end) {Log.d(TAG, "data:" + data);}});notificationBuilder();}return START_STICKY;}停止錄音:
public class PhoneStateListener extends BroadcastReceiver {private static final String TAG = "通話狀態監聽";static boolean incoming_flag;private Context mContext;@Overridepublic void onReceive(Context ctx, Intent intent) {mContext = ctx;String event = intent.getStringExtra(TelephonyManager.EXTRA_STATE);Log.d(TAG, "通話狀態:state:" + event);if (event.equals(TelephonyManager.EXTRA_STATE_RINGING)) {Log.d(TAG, "-->RINGING--正在響鈴");incoming_flag = true;} else if (event.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)) {Log.d(TAG, "-->EXTRA_STATE_OFFHOOK--正在通話");startService(ctx, intent);} else if (event.equals(TelephonyManager.EXTRA_STATE_IDLE)) {Log.d(TAG, "-->EXTRA_STATE_IDLE--電話掛斷--空閑");ctx.stopService(new Intent(ctx, MediaRecorderService.class));//AudioRecordUtil.getInstance().stopRecording();//為什么不在服務里面停止錄音?有的機型掛斷電話后沒有馬上銷毀服務,所以在狀態這里直接停止錄音AudioRecorder.getInstance().stopRecord();}}本文demo 錄音文件保存在根目錄anyi.phone/record 文件下。
獲取通話記錄對應的錄音文件:
/*** 獲取錄音文件路徑 --通話記錄*/private List<RecordBean> getLocalRecord() {List<ContactsBean> contacts = readContacts();List<RecordBean> list = new ArrayList<>();JSONArray allFiles = getAllFiles("", "wav");//Log.d("allFiles", "allFiles:" + allFiles.toString());if (null != allFiles) {for (int i = 0; i < allFiles.length(); i++) {try {JSONObject jsonObject = allFiles.getJSONObject(i);String name = jsonObject.getString("name");String path = jsonObject.getString("path");String[] split1 = name.split("-");if (split1.length > 0) {RecordBean recordBean = new RecordBean();recordBean.setNumber(split1[0]);recordBean.setPath(path);recordBean.setDate(new SimpleDateFormat("HH:mm").format(new Date(Long.parseLong(split1[1]))));if (contacts.size() > 0) {for (ContactsBean b : contacts) {if (split1[0].equals(b.getNumber())) {recordBean.setCachedName(b.getName());}}} else {recordBean.setCachedName("未知");}list.add(recordBean);}} catch (JSONException e) {e.printStackTrace();}}Collections.reverse(list);return list;}return list;}public static JSONArray getAllFiles(String dirPath, String _type) {dirPath = "/storage/emulated/0/anyi.phone/record/";File f = new File(dirPath);if (!f.exists()) {//判斷路徑是否存在return null;}File[] files = f.listFiles();if (files == null) {//判斷權限return null;}JSONArray fileList = new JSONArray();for (File _file : files) {//遍歷目錄if (_file.isFile() && (_file.getName().endsWith("amr")||_file.getName().endsWith("wav"))) {String _name = _file.getName();String filePath = _file.getAbsolutePath();//獲取文件路徑String fileName = _file.getName().substring(0, _name.length() - 4);//獲取文件名try {JSONObject _fInfo = new JSONObject();_fInfo.put("name", fileName);_fInfo.put("path", filePath);fileList.put(_fInfo);} catch (Exception e) {}} else if (_file.isDirectory()) {//查詢子目錄//getAllFiles(_file.getAbsolutePath(), _type);} else {}}return fileList;}播放:
private void initPlay() {mediaPlayer = new MediaPlayer();}private void startPlay(String path) {if (TextUtils.isEmpty(path)) {Toast.makeText(this, "文件路徑不存在", Toast.LENGTH_LONG).show();return;}mediaPlayer.reset(); //清空里面的其他歌曲try {mediaPlayer.setDataSource(path);mediaPlayer.prepare(); //準備就緒mediaPlayer.start(); //開始唱歌} catch (IOException e) {e.printStackTrace();}}方案三,SIP通話錄音,linphone 為例,只調試了音頻通話,視頻通話未調試
?前提準備
準備一個SIP服務器地址和一個賬號密碼。可以自己搭建SIP服務器或者網上找一個SIP服務器注冊 一個賬號密碼。下面是網上找的資源,沒試過。因為我們公司是購買的有SIP話機服務器的。
免費sip賬號注冊地址 http://serweb.iptel.org/user/reg/index.php 免費sip服務器 iptel.org 免費sip客戶端 http://www.fring.com? ? ? ??
正文:
1、把linphone-sdk-android-4.3.0-beta.aar包放在項目libs,提取碼: bhud 。
2、配置文件注冊服務:
<serviceandroid:name=".linphone.LinphoneService"android:enabled="true"android:exported="true"android:label="@string/app_name" />3.在啟動頁 啟動SIP相關服務,
啟動頁:
public class LauncherActivity extends AppCompatActivity {private static final String TAG = "XXPermissions";private Handler mHandler;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_launcher);mHandler = new Handler();}@Overrideprotected void onStart() {super.onStart();getPermission();}private void getPermission() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {XXPermissions.with(this).permission(allPermission).request(new OnPermissionCallback() {@Overridepublic void onGranted(List<String> permissions, boolean all) {if (all) {if (LinphoneService.isReady()) {onServiceReady();} else {startService(new Intent().setClass(LauncherActivity.this, LinphoneService.class));new ServiceWaitThread().start();}}}@Overridepublic void onDenied(List<String> permissions, boolean never) {if (never) {Log.e(TAG, "onDenied:被永久拒絕授權,請手動授予權限 ");} else {Log.e(TAG, "onDenied: 權限獲取失敗");}}});} else {if (LinphoneService.isReady()) {onServiceReady();} else {startService(new Intent().setClass(LauncherActivity.this, LinphoneService.class));new ServiceWaitThread().start();}}}private void onServiceReady() {Intent intent = new Intent();intent.setClass(LauncherActivity.this, MainActivity.class);if (getIntent() != null && getIntent().getExtras() != null) {intent.putExtras(getIntent().getExtras());}intent.setAction(getIntent().getAction());intent.setType(getIntent().getType());startActivity(intent);}private class ServiceWaitThread extends Thread {public void run() {while (!LinphoneService.isReady()) {try {sleep(30);} catch (InterruptedException e) {throw new RuntimeException("waiting thread sleep() has been interrupted");}}mHandler.post(new Runnable() {@Overridepublic void run() {onServiceReady();}});}} }首頁activity? onResume()方法中檢測 賬號是否注冊,未注冊跳轉到注冊頁面:
@Overrideprotected void onResume() {super.onResume();Log.d(TAG, "onResume()");LinphoneService.getCore().addListener(mCoreListener);ProxyConfig proxyConfig = LinphoneService.getCore().getDefaultProxyConfig();if (proxyConfig != null) {updateLed(proxyConfig.getState());} else {startActivity(new Intent(this, ConfigureAccountActivity.class));}}注冊:
/*** 注冊*/private void configureAccount() {mAccountCreator.setUsername(mUsername.getText().toString());mAccountCreator.setDomain(mDomain.getText().toString());mAccountCreator.setPassword(mPassword.getText().toString());switch (mTransport.getCheckedRadioButtonId()) {case R.id.transport_udp:mAccountCreator.setTransport(TransportType.Udp);break;case R.id.transport_tcp:mAccountCreator.setTransport(TransportType.Tcp);break;case R.id.transport_tls:mAccountCreator.setTransport(TransportType.Tls);break;}ProxyConfig cfg = mAccountCreator.createProxyConfig();LinphoneService.getCore().setDefaultProxyConfig(cfg);}public void listener(){ mCoreListener = new CoreListenerStub() {/*** 監聽注冊是否成功* @param core* @param cfg* @param state* @param message*/@Overridepublic void onRegistrationStateChanged(Core core, ProxyConfig cfg, RegistrationState state, String message) {registerPr.setVisibility(View.GONE);if (state == RegistrationState.Ok) {finish();} else if (state == RegistrationState.Failed) {Toast.makeText(ConfigureAccountActivity.this, "Failure: " + message, Toast.LENGTH_LONG).show();}}}; }注冊成功開始通話:
private void sipCallIng() {Core core = LinphoneService.getCore();Address addressToCall = core.interpretUrl(phoneEt.getText().toString());CallParams params = core.createCallParams(null);params.enableVideo(false);if (addressToCall != null) {String filePath = AudioRecordUtil.getInstance().getFilename(phoneEt.getText().toString(), ".wav");android.util.Log.d("linPhone--", "開始呼叫--號碼--filePath = " + filePath);//重要:通話前需要設置錄音文件,要不不會錄音,params.setRecordFile(filePath);core.inviteAddressWithParams(addressToCall, params);Intent intent = new Intent(getActivity(), CallActivity.class);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);startActivity(intent);}}開始錄音:
/*** ---通話接通--開始錄音*/private void startRecord() {android.util.Log.d("linPhone--", "接通或者拒絕");android.util.Log.d("linPhone--", "開始錄音:錄音地址:" + core.getRecordFile());call.startRecording();}停止錄音:
/*** ---通話掛斷--停止錄音--銷毀頁面*/private void stopRecord() {android.util.Log.d("linPhone--", "掛斷,未接");android.util.Log.d("linPhone--", "停止錄音");call.stopRecording();//停止錄音finish();//掛斷電話-銷毀頁面}后面就是拿到錄音文件播放,-----具體就不說了,
研究SIP也用了大量時間和下載了很多大佬的資源,也花費了很多積分,
so? 想要demo的朋友們也希望支持一下,
demo需要積分下載,具體多少由平臺分配。
本文demo成功實現了兩種主流的通話錄音方式,應該是能滿足你們的業務需求的,
demo傳送門---
總結
以上是生活随笔為你收集整理的Android SIP软电话,通话录音,VoIP电话,linphone电话的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 贵族机要第二次半修改装备简单分配
- 下一篇: android 刷rom,刷ROM是什么