IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android SIP软电话,通话录音,VoIP电话,linphone电话 -> 正文阅读

[移动开发]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-permission
        android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />
    <uses-permission
        android:name="android.permission.MODIFY_PHONE_STATE"
        tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission
        android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
        tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

注册广播:

AndroidManifest文件添加?PhoneStateListener和MediaRecorderService

<receiver
    android:name=".callrecord.PhoneStateListener"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.PHONE_STATE" />
    </intent-filter>
</receiver>

 <service
            android: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-data
                android: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;

    @Override
    public 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;

    @Override
    public void onInterrupt() {

    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {

    }

    @Override
    public 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() {
                @Override
                public 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());
        }
    }

    @Override
    public 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);
    }

?开始录音

 @Override
    public 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() {
                @Override
                public 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;

    @Override
    public 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、配置文件注册服务:

 <service
            android: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;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_launcher);
        mHandler = new Handler();
    }

    @Override
    protected 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() {
                        @Override
                        public 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();
                                }
                            }
                        }

                        @Override
                        public 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() {
                @Override
                public void run() {
                    onServiceReady();
                }
            });
        }
    }
}

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

@Override
    protected 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
             */
            @Override
            public 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传送门---

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-10-31 12:09:06  更:2022-10-31 12:12:59 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 21:22:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码