基于Linphone android sdk开发Android软话机

2023-12-23 03:15

本文主要是介绍基于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 }}}}

3.封装好的源码 

网上已经有对linphone android sdk开发好的产品

LinphoneCall封装linphone android sdk的软话机

4.优化的配置

对于部分设备可能存在啸叫、噪音的问题,可以修改assets/linphone_factory 文件下的语音参数,默认已经配置了一些,如果不能满足你的要求,可以添加下面的一些参数。

回声消除
  • echocancellation=1:回声消除这个必须=1,否则会听到自己说话的声音
  • ec_tail_len= 100:尾长表示回声时长,越长需要cpu处理能力越强
  • ec_delay=0:延时,表示回声从话筒到扬声器时间,默认不写
  • ec_framesize=128:采样数,肯定是刚好一个采样周期最好,默认不写
回声抑制
  • echolimiter=0:等于0时不开会有空洞的声音,建议不开
  • el_type=mic:这个选full 和 mic 表示抑制哪个设备
  • eq_location=hp:这个表示均衡器用在哪个设备
  • speaker_agc_enabled=0:这个表示是否启用扬声器增益
  • el_thres=0.001:系统响应的阈值 意思在哪个阈值以上系统有响应处理
  • el_force=600 :控制收音范围 值越大收音越广,意思能否收到很远的背景音
  • el_sustain=50:控制发声到沉默时间,用于控制声音是否拉长,意思说完一个字是否被拉长丢包时希望拉长避免断断续续
降噪
  • noisegate=1 :这个表示开启降噪音,不开会有背景音
  • ng_thres=0.03:这个表示声音这个阈值以上都可以通过,用于判断哪些是噪音
  • ng_floorgain=0.03:这个表示低于阈值的声音进行增益,用于补偿声音太小被吃掉
网络抖动延时丢包
  • audio_jitt_comp=160:这个参数用于抖动处理,值越大处理抖动越好,但声音延时较大 理论值是80根据实际调整160
  • nortp_timeout=20:这个参数用于丢包处理,值越小丢包越快声音不会断很长时间,同时要跟el_sustain配合声音才好听

这篇关于基于Linphone android sdk开发Android软话机的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/526506

相关文章

Android使用ImageView.ScaleType实现图片的缩放与裁剪功能

《Android使用ImageView.ScaleType实现图片的缩放与裁剪功能》ImageView是最常用的控件之一,它用于展示各种类型的图片,为了能够根据需求调整图片的显示效果,Android提... 目录什么是 ImageView.ScaleType?FIT_XYFIT_STARTFIT_CENTE

Go语言开发实现查询IP信息的MCP服务器

《Go语言开发实现查询IP信息的MCP服务器》随着MCP的快速普及和广泛应用,MCP服务器也层出不穷,本文将详细介绍如何在Go语言中使用go-mcp库来开发一个查询IP信息的MCP... 目录前言mcp-ip-geo 服务器目录结构说明查询 IP 信息功能实现工具实现工具管理查询单个 IP 信息工具的实现服

Android实现在线预览office文档的示例详解

《Android实现在线预览office文档的示例详解》在移动端展示在线Office文档(如Word、Excel、PPT)是一项常见需求,这篇文章为大家重点介绍了两种方案的实现方法,希望对大家有一定的... 目录一、项目概述二、相关技术知识三、实现思路3.1 方案一:WebView + Office Onl

Android实现两台手机屏幕共享和远程控制功能

《Android实现两台手机屏幕共享和远程控制功能》在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值,本项目旨在实现两台Android手... 目录一、项目概述二、相关知识2.1 MediaProjection API2.2 Socket 网络

Android实现悬浮按钮功能

《Android实现悬浮按钮功能》在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(FloatingButton),用来快速启动工具、展示未读信息或快捷操作,所以本文给大家介绍... 目录一、项目概述二、相关技术知识三、实现思路四、整合代码4.1 Java 代码(MainActivi

Android Mainline基础简介

《AndroidMainline基础简介》AndroidMainline是通过模块化更新Android核心组件的框架,可能提高安全性,本文给大家介绍AndroidMainline基础简介,感兴趣的朋... 目录关键要点什么是 android Mainline?Android Mainline 的工作原理关键

如何解决idea的Module:‘:app‘platform‘android-32‘not found.问题

《如何解决idea的Module:‘:app‘platform‘android-32‘notfound.问题》:本文主要介绍如何解决idea的Module:‘:app‘platform‘andr... 目录idea的Module:‘:app‘pwww.chinasem.cnlatform‘android-32

使用Python开发一个带EPUB转换功能的Markdown编辑器

《使用Python开发一个带EPUB转换功能的Markdown编辑器》Markdown因其简单易用和强大的格式支持,成为了写作者、开发者及内容创作者的首选格式,本文将通过Python开发一个Markd... 目录应用概览代码结构与核心组件1. 初始化与布局 (__init__)2. 工具栏 (setup_t

Android实现打开本地pdf文件的两种方式

《Android实现打开本地pdf文件的两种方式》在现代应用中,PDF格式因其跨平台、稳定性好、展示内容一致等特点,在Android平台上,如何高效地打开本地PDF文件,不仅关系到用户体验,也直接影响... 目录一、项目概述二、相关知识2.1 PDF文件基本概述2.2 android 文件访问与存储权限2.

Spring Shell 命令行实现交互式Shell应用开发

《SpringShell命令行实现交互式Shell应用开发》本文主要介绍了SpringShell命令行实现交互式Shell应用开发,能够帮助开发者快速构建功能丰富的命令行应用程序,具有一定的参考价... 目录引言一、Spring Shell概述二、创建命令类三、命令参数处理四、命令分组与帮助系统五、自定义S