当前位置: 首页 > news >正文

基于Linphone android sdk开发Android软话机

 

1.Linphone简介

1.1 简介

LinPhone是一个遵循GPL协议的开源网络电话或者IP语音电话(VOIP)系统,其主要如下。使用linphone,开发者可以在互联网上随意的通信,包括语音、视频、即时文本消息。linphone使用SIP协议,是一个标准的开源网络电话系统,能将linphone与任何基于SIP的VoIP运营商连接起来,包括我们自己开发的免费的基于SIP的Audio/Video服务器。

LinPhone是一款自由软件(或者开源软件),你可以随意的下载和在LinPhone的基础上二次开发。LinPhone是可用于Linux, Windows, MacOSX 桌面电脑以及Android, iPhone, Blackberry移动设备。

学习LinPhone的源码,开源从以下几个部分着手: Java层框架实现的SIP三层协议架构: 传输层,事务层,语法编解码层; linphone动态库C源码实现的SIP功能: 注册,请求,请求超时,邀请会话,挂断电话,邀请视频,收发短信... linphone动态库C源码实现的音视频编解码功能; Android平台上的音视频捕获,播放功能;

1.2 基本使用

如果是Android系统用户,可以从谷歌应用商店安装或者从这个链接下载Linphone 。安装完成后,点击左上角的菜单按钮,选择进入助手界面。在助手界面,可以设定SIP账户或者Linphone账号,如下图:

2.基于linphone android sdk开发linphone

引入sdk依赖 

dependencies {
    //linphone
    debugImplementation "org.linphone:linphone-sdk-android-debug:5.0.0"
    releaseImplementation "org.linphone:linphone-sdk-android:5.0.0"

为了方便调用,我们需要对Linphone进行简单的封装。首先,按照官方文档的介绍,创建一个CoreManager类,此类是sdk里面的管理类,用来控制来电铃声和启动CoreService,无特殊需求不需调用。需要注意的是,启动来电铃声需要导入media包,否则不会有来电铃声,如下

implementation 'androidx.media:media:1.2.0'

基本代码开发: 

package com.matt.linphonelibrary.coreimport android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.telephony.PhoneStateListener
import android.telephony.TelephonyManager
import android.util.Log
import android.view.TextureView
import com.matt.linphonelibrary.R
import com.matt.linphonelibrary.callback.PhoneCallback
import com.matt.linphonelibrary.callback.RegistrationCallback
import com.matt.linphonelibrary.utils.AudioRouteUtils
import com.matt.linphonelibrary.utils.LinphoneUtils
import com.matt.linphonelibrary.utils.VideoZoomHelper
import org.linphone.core.*
import java.io.File
import java.util.*class LinphoneManager private constructor(private val context: Context) {private val TAG = javaClass.simpleNameprivate var core: Coreprivate var corePreferences: CorePreferencesprivate var coreIsStart = falsevar registrationCallback: RegistrationCallback? = nullvar phoneCallback: PhoneCallback? = nullinit {//日志收集Factory.instance().setLogCollectionPath(context.filesDir.absolutePath)Factory.instance().enableLogCollection(LogCollectionState.Enabled)corePreferences = CorePreferences(context)corePreferences.copyAssetsFromPackage()val config = Factory.instance().createConfigWithFactory(corePreferences.configPath,corePreferences.factoryConfigPath)corePreferences.config = configval appName = context.getString(R.string.app_name)Factory.instance().setDebugMode(corePreferences.debugLogs, appName)core = Factory.instance().createCoreWithConfig(config, context)}private var previousCallState = Call.State.Idleprivate val coreListener = object : CoreListenerStub() {override fun onGlobalStateChanged(core: Core, state: GlobalState?, message: String) {if (state === GlobalState.On) {}}//登录状态回调override fun onRegistrationStateChanged(core: Core,cfg: ProxyConfig,state: RegistrationState,message: String) {when (state) {RegistrationState.None -> registrationCallback?.registrationNone()RegistrationState.Progress -> registrationCallback?.registrationProgress()RegistrationState.Ok -> registrationCallback?.registrationOk()RegistrationState.Cleared -> registrationCallback?.registrationCleared()RegistrationState.Failed -> registrationCallback?.registrationFailed()}}//电话状态回调override fun onCallStateChanged(core: Core,call: Call,state: Call.State,message: String) {Log.i(TAG, "[Context] Call state changed [$state]")when (state) {Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> {if (gsmCallActive) {Log.w(TAG,"[Context] Refusing the call with reason busy because a GSM call is active")call.decline(Reason.Busy)return}phoneCallback?.incomingCall(call)gsmCallActive = true//自动接听if (corePreferences.autoAnswerEnabled) {val autoAnswerDelay = corePreferences.autoAnswerDelayif (autoAnswerDelay == 0) {Log.w(TAG, "[Context] Auto answering call immediately")answerCall(call)} else {Log.i(TAG,"[Context] Scheduling auto answering in $autoAnswerDelay milliseconds")val mainThreadHandler = Handler(Looper.getMainLooper())mainThreadHandler.postDelayed({Log.w(TAG, "[Context] Auto answering call")answerCall(call)}, autoAnswerDelay.toLong())}}}Call.State.OutgoingInit -> {phoneCallback?.outgoingInit(call)gsmCallActive = true}Call.State.OutgoingProgress -> {if (core.callsNb == 1 && corePreferences.routeAudioToBluetoothIfAvailable) {AudioRouteUtils.routeAudioToBluetooth(core, call)}}Call.State.Connected -> phoneCallback?.callConnected(call)Call.State.StreamsRunning -> {// Do not automatically route audio to bluetooth after first callif (core.callsNb == 1) {// Only try to route bluetooth / headphone / headset when the call is in StreamsRunning for the first timeif (previousCallState == Call.State.Connected) {Log.i(TAG,"[Context] First call going into StreamsRunning state for the first time, trying to route audio to headset or bluetooth if available")if (AudioRouteUtils.isHeadsetAudioRouteAvailable(core)) {AudioRouteUtils.routeAudioToHeadset(core, call)} else if (corePreferences.routeAudioToBluetoothIfAvailable && AudioRouteUtils.isBluetoothAudioRouteAvailable(core)) {AudioRouteUtils.routeAudioToBluetooth(core, call)}}}if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled && call.currentParams.videoEnabled()) {// Do not turn speaker on when video is enabled if headset or bluetooth is usedif (!AudioRouteUtils.isHeadsetAudioRouteAvailable(core) &&!AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed(core, call)) {Log.i(TAG,"[Context] Video enabled and no wired headset not bluetooth in use, routing audio to speaker")AudioRouteUtils.routeAudioToSpeaker(core, call)}}}Call.State.End, Call.State.Released, Call.State.Error -> {if (core.callsNb == 0) {when (state) {Call.State.End -> phoneCallback?.callEnd(call)Call.State.Released -> phoneCallback?.callReleased(call)Call.State.Error -> {val id = when (call.errorInfo.reason) {Reason.Busy -> R.string.call_error_user_busyReason.IOError -> R.string.call_error_io_errorReason.NotAcceptable -> R.string.call_error_incompatible_media_paramsReason.NotFound -> R.string.call_error_user_not_foundReason.Forbidden -> R.string.call_error_forbiddenelse -> R.string.call_error_unknown}phoneCallback?.error(context.getString(id))}}gsmCallActive = false}}}previousCallState = state}}/*** 启动linphone*/fun start() {if (!coreIsStart) {coreIsStart = trueLog.i(TAG, "[Context] Starting")core.addListener(coreListener)core.start()initLinphone()val telephonyManager =context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManagerLog.i(TAG, "[Context] Registering phone state listener")telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)}}/*** 停止linphone*/fun stop() {coreIsStart = falseval telephonyManager =context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManagerLog.i(TAG, "[Context] Unregistering phone state listener")telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)core.removeListener(coreListener)core.stop()}/*** 注册到服务器** @param username     账号名* @param password      密码* @param domain     IP地址:端口号*/fun createProxyConfig(username: String,password: String,domain: String,type: TransportType? = TransportType.Udp) {core.clearProxyConfig()val accountCreator = core.createAccountCreator(corePreferences.xmlRpcServerUrl)accountCreator.language = Locale.getDefault().languageaccountCreator.reset()accountCreator.username = usernameaccountCreator.password = passwordaccountCreator.domain = domainaccountCreator.displayName = usernameaccountCreator.transport = typeaccountCreator.createProxyConfig()}/*** 取消注册*/fun removeInvalidProxyConfig() {core.clearProxyConfig()}/*** 拨打电话* @param to String* @param isVideoCall Boolean*/fun startCall(to: String, isVideoCall: Boolean) {try {val addressToCall = core.interpretUrl(to)addressToCall?.displayName = toval params = core.createCallParams(null)//启用通话录音
//            params?.recordFile = LinphoneUtils.getRecordingFilePathForAddress(context, addressToCall!!)//启动低宽带模式if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) {Log.w(TAG, "[Context] Enabling low bandwidth mode!")params?.enableLowBandwidth(true)}if (isVideoCall) {params?.enableVideo(true)core.enableVideoCapture(true)core.enableVideoDisplay(true)} else {params?.enableVideo(false)}if (params != null) {core.inviteAddressWithParams(addressToCall!!, params)} else {core.inviteAddress(addressToCall!!)}} catch (e: Exception) {e.printStackTrace()}}/*** 接听来电**/fun answerCall(call: Call) {Log.i(TAG, "[Context] Answering call $call")val params = core.createCallParams(call)//启用通话录音
//        params?.recordFile = LinphoneUtils.getRecordingFilePathForAddress(context, call.remoteAddress)if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) {Log.w(TAG, "[Context] Enabling low bandwidth mode!")params?.enableLowBandwidth(true)}params?.enableVideo(isVideoCall(call))call.acceptWithParams(params)}/*** 谢绝电话* @param call Call*/fun declineCall(call: Call) {val voiceMailUri = corePreferences.voiceMailUriif (voiceMailUri != null && corePreferences.redirectDeclinedCallToVoiceMail) {val voiceMailAddress = core.interpretUrl(voiceMailUri)if (voiceMailAddress != null) {Log.i(TAG, "[Context] Redirecting call $call to voice mail URI: $voiceMailUri")call.redirectTo(voiceMailAddress)}} else {Log.i(TAG, "[Context] Declining call $call")call.decline(Reason.Declined)}}/*** 挂断电话*/fun terminateCall(call: Call) {Log.i(TAG, "[Context] Terminating call $call")call.terminate()}fun micEnabled() = core.micEnabled()fun speakerEnabled() = core.outputAudioDevice?.type == AudioDevice.Type.Speaker/*** 启动麦克风* @param micEnabled Boolean*/fun enableMic(micEnabled: Boolean) {core.enableMic(micEnabled)}/*** 扬声器或听筒* @param SpeakerEnabled Boolean*/fun enableSpeaker(SpeakerEnabled: Boolean) {if (SpeakerEnabled) {AudioRouteUtils.routeAudioToEarpiece(core)} else {AudioRouteUtils.routeAudioToSpeaker(core)}}/*** 是否是视频电话* @return Boolean*/fun isVideoCall(call: Call): Boolean {val remoteParams = call.remoteParamsreturn remoteParams != null && remoteParams.videoEnabled()}/*** 设置视频界面* @param videoRendering TextureView 对方界面* @param videoPreview CaptureTextureView 自己界面*/fun setVideoWindowId(videoRendering: TextureView, videoPreview: TextureView) {core.nativeVideoWindowId = videoRenderingcore.nativePreviewWindowId = videoPreview}/*** 设置视频电话可缩放* @param context Context* @param videoRendering TextureView*/fun setVideoZoom(context: Context, videoRendering: TextureView) {VideoZoomHelper(context, videoRendering, core)}fun switchCamera() {val currentDevice = core.videoDeviceLog.i(TAG, "[Context] Current camera device is $currentDevice")for (camera in core.videoDevicesList) {if (camera != currentDevice && camera != "StaticImage: Static picture") {Log.i(TAG, "[Context] New camera device will be $camera")core.videoDevice = camerabreak}}//        val conference = core.conference
//        if (conference == null || !conference.isIn) {
//            val call = core.currentCall
//            if (call == null) {
//                Log.w(TAG, "[Context] Switching camera while not in call")
//                return
//            }
//            call.update(null)
//        }}//初始化一些操作private fun initLinphone() {configureCore()initUserCertificates()}private fun configureCore() {// 来电铃声core.isNativeRingingEnabled = false// 来电振动core.isVibrationOnIncomingCallEnabled = truecore.enableEchoCancellation(true) //回声消除core.enableAdaptiveRateControl(true) //自适应码率控制}private var gsmCallActive = falseprivate val phoneStateListener = object : PhoneStateListener() {override fun onCallStateChanged(state: Int, phoneNumber: String?) {gsmCallActive = when (state) {TelephonyManager.CALL_STATE_OFFHOOK -> {Log.i(TAG, "[Context] Phone state is off hook")true}TelephonyManager.CALL_STATE_RINGING -> {Log.i(TAG, "[Context] Phone state is ringing")true}TelephonyManager.CALL_STATE_IDLE -> {Log.i(TAG, "[Context] Phone state is idle")false}else -> {Log.i(TAG, "[Context] Phone state is unexpected: $state")false}}}}//设置存放用户x509证书的目录路径private fun initUserCertificates() {val userCertsPath = corePreferences!!.userCertificatesPathval f = File(userCertsPath)if (!f.exists()) {if (!f.mkdir()) {Log.e(TAG, "[Context] $userCertsPath can't be created.")}}core.userCertificatesPath = userCertsPath}companion object {// For Singleton instantiation@SuppressLint("StaticFieldLeak")@Volatileprivate var instance: LinphoneManager? = nullfun getInstance(context: Context) =instance ?: synchronized(this) {instance ?: LinphoneManager(context).also { instance = it }}}}

相关文章:

基于Linphone android sdk开发Android软话机

1.Linphone简介 1.1 简介 LinPhone是一个遵循GPL协议的开源网络电话或者IP语音电话(VOIP)系统,其主要如下。使用linphone,开发者可以在互联网上随意的通信,包括语音、视频、即时文本消息。linphone使用SIP协议&#…...

[论文分享]TimeDRL:多元时间序列的解纠缠表示学习

论文题目:TimeDRL: Disentangled Representation Learning for Multivariate Time-Series 论文地址:https://arxiv.org/abs/2312.04142 代码地址:暂无 关键要点:多元时间序列,自监督表征学习,分类和预测 摘…...

分享一个好看的vs主题

最近发现了一个很好看的vs主题(个人认为挺好看的),想要分享给大家。 主题的名字叫NightOwl,和vscode的主题颜色挺像的。操作方法也十分简单,首先我们先在最上面哪一行找到扩展。 然后点击管理扩展,再搜索栏…...

什么是云呼叫中心?

云呼叫中心作为一种高效的企业呼叫管理方案,越来越受到企业的青睐,常被用于管理客服和销售业务。那么,云呼叫中心到底是什么? 什么是云呼叫中心? 云呼叫中心是一种基于互联网的呼叫管理系统,与传统的呼叫…...

还在用nvm?来试试更快的node版本管理工具——fnm

前言 📫 大家好,我是南木元元,热衷分享有趣实用的文章,希望大家多多支持,一起进步! 🍅 个人主页:南木元元 目录 什么是node版本管理 常见的node版本管理工具 fnm是什么 安装fnm …...

【Hadoop精讲】HDFS详解

目录 理论知识点 角色功能 元数据持久化 安全模式 SecondaryNameNode(SNN) 副本放置策略 HDFS写流程 HDFS读流程 HA高可用 CPA原则 Paxos算法 HA解决方案 HDFS-Fedration解决方案(联邦机制) 理论知识点 角色功能 元数据持久化 另一台机器就…...

企业需要哪些数字化管理系统?

企业需要哪些数字化管理系统? ✅企业引进管理系统肯定是为了帮助整合和管理大量的数据,从而优化业务流程,提高工作效率和生产力。 ❌但是,如果各个系统之间不互通、无法互相关联数据的话,反而会增加工作量和时间成本…...

【vue】开发常见问题及解决方案

有一些问题不限于 Vue,还适应于其他类型的 SPA 项目。 1. 页面权限控制和登陆验证页面权限控制 页面权限控制是什么意思呢? 就是一个网站有不同的角色,比如管理员和普通用户,要求不同的角色能访问的页面是不一样的。如果一个页…...

飞天使-k8s知识点3-卸载yum 安装的k8s

要彻底卸载使用yum安装的 Kubernetes 集群,您可以按照以下步骤进行操作: 停止 Kubernetes 服务: sudo systemctl stop kubelet sudo systemctl stop docker 卸载 Kubernetes 组件: sudo yum remove -y kubelet kubeadm kubectl…...

ZooKeeper 集群搭建

文章目录 ZooKeeper 概述选举机制搭建前准备分布式配置分布式安装解压缩并重命名配置环境配置服务器编号配置文件 操作集群编写脚本运行脚本搭建过程中常见错误 ZooKeeper 概述 Zookeeper 是一个开源的分布式服务协调框架,由Apache软件基金会开发和维护。以下是对Z…...

Meson:现代的构建系统

Meson是一款现代化、高性能的开源构建系统,旨在提供简单、快速和可读性强的构建脚本。Meson被设计为跨平台的,支持多种编程语言,包括C、C、Fortran、Python等。其目标是替代传统的构建工具,如Autotools和CMake,提供更简…...

【大模型AIGC系列课程 5-2】视觉-语言大模型原理

重磅推荐专栏: 《大模型AIGC》;《课程大纲》 本专栏致力于探索和讨论当今最前沿的技术趋势和应用领域,包括但不限于ChatGPT和Stable Diffusion等。我们将深入研究大型模型的开发和应用,以及与之相关的人工智能生成内容(AIGC)技术。通过深入的技术解析和实践经验分享,旨在…...

震惊!难怪别人家的孩子越来越聪明,原来竟是因为它

前段时间工作调动给孩子换了个新学校,刚开始担心她不能适应新学校的授课方式,但任课老师对她评价很高,夸她上课很专注。 为了训练孩子的专注力,作为家长可没少下功夫,画画,下五子棋等益智游戏的兴趣班没少…...

Linux操作系统(UMASK+SUID+SGID+STICK)

UMASK反掩码 如何查看反掩码:直接在终端窗口运行 umask root用户反掩码:0022 普通用户反掩码:0002 UMASK的作用:确定目录,文件的缺省权限值 以root身份创建目录,观察目录的9位权限值 以root身份创建普通文件…...

Java 中单例模式的常见实现方式

目录 一、什么是单例模式? 二、单例模式有什么作用? 三、常见的创建单例模式的方式 1、饿汉式创建 2、懒汉式创建 3、DCL(Double Checked Lock)双检锁方式创建 3.1、synchronized 同步锁的基本使用 3.2、使用 DCL 中存在的疑…...

【C语言】自定义类型之联合和枚举

目录 1. 前言2. 联合体2.1 联合体类型的声明2.2 联合体的特点2.3 相同成员的结构体和联合体对比2.4 联合体大小的计算2.4 判断当前机器的大小端 3. 枚举3.1 枚举类型的声明3.2 枚举类型的优点3.3 枚举类型的使用 1. 前言 在之前的博客中介绍了自定义类型中的结构体,…...

使用Mosquitto/python3进行MQTT连接

一、简介 MQTT(消息队列遥测传输)是ISO 标准(ISO/IEC PRF 20922)下基于发布/订阅范式的消息协议。它工作在 TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议,为此,它需要一个消息中间件。 …...

JavaWeb笔记之前端开发HTML

一、引言 1.1HTML概念 网页,是网站中的一个页面,通常是网页是构成网站的基本元素,是承载各种网站应用的平台。通俗的说,网站就是由网页组成的。通常我们看到的网页都是以htm或html后缀结尾的文件,俗称 HTML文件。 …...

通过IP地址定位解决被薅羊毛问题

随着互联网的普及,线上交易和优惠活动日益增多,这也为一些不法分子提供了可乘之机。他们利用技术手段,通过大量注册账号或使用虚假IP地址进行异常操作,以获取更多的优惠或利益,这种行为被称为“薅羊毛”。对于企业和平…...

Leetcode 122 买卖股票的最佳时机 II

题意理解: 已知:一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格 如何哪个时间点买入,哪个时间点卖出,多次交易,能够收益最大化 目的:收益最大化 解题思路: 使用贪心…...

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

聊聊 Pulsar:Producer 源码解析

一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台,以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中,Producer(生产者) 是连接客户端应用与消息队列的第一步。生产者…...

Opencv中的addweighted函数

一.addweighted函数作用 addweighted()是OpenCV库中用于图像处理的函数,主要功能是将两个输入图像(尺寸和类型相同)按照指定的权重进行加权叠加(图像融合),并添加一个标量值&#x…...

2025 后端自学UNIAPP【项目实战:旅游项目】6、我的收藏页面

代码框架视图 1、先添加一个获取收藏景点的列表请求 【在文件my_api.js文件中添加】 // 引入公共的请求封装 import http from ./my_http.js// 登录接口(适配服务端返回 Token) export const login async (code, avatar) > {const res await http…...

Matlab | matlab常用命令总结

常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...

根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:

根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...

七、数据库的完整性

七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...

【Linux】Linux安装并配置RabbitMQ

目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...

PH热榜 | 2025-06-08

1. Thiings 标语:一套超过1900个免费AI生成的3D图标集合 介绍:Thiings是一个不断扩展的免费AI生成3D图标库,目前已有超过1900个图标。你可以按照主题浏览,生成自己的图标,或者下载整个图标集。所有图标都可以在个人或…...