当前位置:   article > 正文

基于Linphone android sdk开发Android软话机_android linphone库

android linphone库

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'
  • 基本代码开发 
  1. package com.matt.linphonelibrary.core
  2. import android.annotation.SuppressLint
  3. import android.content.Context
  4. import android.os.Handler
  5. import android.os.Looper
  6. import android.telephony.PhoneStateListener
  7. import android.telephony.TelephonyManager
  8. import android.util.Log
  9. import android.view.TextureView
  10. import com.matt.linphonelibrary.R
  11. import com.matt.linphonelibrary.callback.PhoneCallback
  12. import com.matt.linphonelibrary.callback.RegistrationCallback
  13. import com.matt.linphonelibrary.utils.AudioRouteUtils
  14. import com.matt.linphonelibrary.utils.LinphoneUtils
  15. import com.matt.linphonelibrary.utils.VideoZoomHelper
  16. import org.linphone.core.*
  17. import java.io.File
  18. import java.util.*
  19. class LinphoneManager private constructor(private val context: Context) {
  20. private val TAG = javaClass.simpleName
  21. private var core: Core
  22. private var corePreferences: CorePreferences
  23. private var coreIsStart = false
  24. var registrationCallback: RegistrationCallback? = null
  25. var phoneCallback: PhoneCallback? = null
  26. init {
  27. //日志收集
  28. Factory.instance().setLogCollectionPath(context.filesDir.absolutePath)
  29. Factory.instance().enableLogCollection(LogCollectionState.Enabled)
  30. corePreferences = CorePreferences(context)
  31. corePreferences.copyAssetsFromPackage()
  32. val config = Factory.instance().createConfigWithFactory(
  33. corePreferences.configPath,
  34. corePreferences.factoryConfigPath
  35. )
  36. corePreferences.config = config
  37. val appName = context.getString(R.string.app_name)
  38. Factory.instance().setDebugMode(corePreferences.debugLogs, appName)
  39. core = Factory.instance().createCoreWithConfig(config, context)
  40. }
  41. private var previousCallState = Call.State.Idle
  42. private val coreListener = object : CoreListenerStub() {
  43. override fun onGlobalStateChanged(core: Core, state: GlobalState?, message: String) {
  44. if (state === GlobalState.On) {
  45. }
  46. }
  47. //登录状态回调
  48. override fun onRegistrationStateChanged(
  49. core: Core,
  50. cfg: ProxyConfig,
  51. state: RegistrationState,
  52. message: String
  53. ) {
  54. when (state) {
  55. RegistrationState.None -> registrationCallback?.registrationNone()
  56. RegistrationState.Progress -> registrationCallback?.registrationProgress()
  57. RegistrationState.Ok -> registrationCallback?.registrationOk()
  58. RegistrationState.Cleared -> registrationCallback?.registrationCleared()
  59. RegistrationState.Failed -> registrationCallback?.registrationFailed()
  60. }
  61. }
  62. //电话状态回调
  63. override fun onCallStateChanged(
  64. core: Core,
  65. call: Call,
  66. state: Call.State,
  67. message: String
  68. ) {
  69. Log.i(TAG, "[Context] Call state changed [$state]")
  70. when (state) {
  71. Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> {
  72. if (gsmCallActive) {
  73. Log.w(
  74. TAG,
  75. "[Context] Refusing the call with reason busy because a GSM call is active"
  76. )
  77. call.decline(Reason.Busy)
  78. return
  79. }
  80. phoneCallback?.incomingCall(call)
  81. gsmCallActive = true
  82. //自动接听
  83. if (corePreferences.autoAnswerEnabled) {
  84. val autoAnswerDelay = corePreferences.autoAnswerDelay
  85. if (autoAnswerDelay == 0) {
  86. Log.w(TAG, "[Context] Auto answering call immediately")
  87. answerCall(call)
  88. } else {
  89. Log.i(
  90. TAG,
  91. "[Context] Scheduling auto answering in $autoAnswerDelay milliseconds"
  92. )
  93. val mainThreadHandler = Handler(Looper.getMainLooper())
  94. mainThreadHandler.postDelayed({
  95. Log.w(TAG, "[Context] Auto answering call")
  96. answerCall(call)
  97. }, autoAnswerDelay.toLong())
  98. }
  99. }
  100. }
  101. Call.State.OutgoingInit -> {
  102. phoneCallback?.outgoingInit(call)
  103. gsmCallActive = true
  104. }
  105. Call.State.OutgoingProgress -> {
  106. if (core.callsNb == 1 && corePreferences.routeAudioToBluetoothIfAvailable) {
  107. AudioRouteUtils.routeAudioToBluetooth(core, call)
  108. }
  109. }
  110. Call.State.Connected -> phoneCallback?.callConnected(call)
  111. Call.State.StreamsRunning -> {
  112. // Do not automatically route audio to bluetooth after first call
  113. if (core.callsNb == 1) {
  114. // Only try to route bluetooth / headphone / headset when the call is in StreamsRunning for the first time
  115. if (previousCallState == Call.State.Connected) {
  116. Log.i(
  117. TAG,
  118. "[Context] First call going into StreamsRunning state for the first time, trying to route audio to headset or bluetooth if available"
  119. )
  120. if (AudioRouteUtils.isHeadsetAudioRouteAvailable(core)) {
  121. AudioRouteUtils.routeAudioToHeadset(core, call)
  122. } else if (corePreferences.routeAudioToBluetoothIfAvailable && AudioRouteUtils.isBluetoothAudioRouteAvailable(
  123. core
  124. )
  125. ) {
  126. AudioRouteUtils.routeAudioToBluetooth(core, call)
  127. }
  128. }
  129. }
  130. if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled && call.currentParams.videoEnabled()) {
  131. // Do not turn speaker on when video is enabled if headset or bluetooth is used
  132. if (!AudioRouteUtils.isHeadsetAudioRouteAvailable(core) &&
  133. !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed(core, call)
  134. ) {
  135. Log.i(
  136. TAG,
  137. "[Context] Video enabled and no wired headset not bluetooth in use, routing audio to speaker"
  138. )
  139. AudioRouteUtils.routeAudioToSpeaker(core, call)
  140. }
  141. }
  142. }
  143. Call.State.End, Call.State.Released, Call.State.Error -> {
  144. if (core.callsNb == 0) {
  145. when (state) {
  146. Call.State.End -> phoneCallback?.callEnd(call)
  147. Call.State.Released -> phoneCallback?.callReleased(call)
  148. Call.State.Error -> {
  149. val id = when (call.errorInfo.reason) {
  150. Reason.Busy -> R.string.call_error_user_busy
  151. Reason.IOError -> R.string.call_error_io_error
  152. Reason.NotAcceptable -> R.string.call_error_incompatible_media_params
  153. Reason.NotFound -> R.string.call_error_user_not_found
  154. Reason.Forbidden -> R.string.call_error_forbidden
  155. else -> R.string.call_error_unknown
  156. }
  157. phoneCallback?.error(context.getString(id))
  158. }
  159. }
  160. gsmCallActive = false
  161. }
  162. }
  163. }
  164. previousCallState = state
  165. }
  166. }
  167. /**
  168. * 启动linphone
  169. */
  170. fun start() {
  171. if (!coreIsStart) {
  172. coreIsStart = true
  173. Log.i(TAG, "[Context] Starting")
  174. core.addListener(coreListener)
  175. core.start()
  176. initLinphone()
  177. val telephonyManager =
  178. context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
  179. Log.i(TAG, "[Context] Registering phone state listener")
  180. telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
  181. }
  182. }
  183. /**
  184. * 停止linphone
  185. */
  186. fun stop() {
  187. coreIsStart = false
  188. val telephonyManager =
  189. context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
  190. Log.i(TAG, "[Context] Unregistering phone state listener")
  191. telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
  192. core.removeListener(coreListener)
  193. core.stop()
  194. }
  195. /**
  196. * 注册到服务器
  197. *
  198. * @param username 账号名
  199. * @param password 密码
  200. * @param domain IP地址:端口号
  201. */
  202. fun createProxyConfig(
  203. username: String,
  204. password: String,
  205. domain: String,
  206. type: TransportType? = TransportType.Udp
  207. ) {
  208. core.clearProxyConfig()
  209. val accountCreator = core.createAccountCreator(corePreferences.xmlRpcServerUrl)
  210. accountCreator.language = Locale.getDefault().language
  211. accountCreator.reset()
  212. accountCreator.username = username
  213. accountCreator.password = password
  214. accountCreator.domain = domain
  215. accountCreator.displayName = username
  216. accountCreator.transport = type
  217. accountCreator.createProxyConfig()
  218. }
  219. /**
  220. * 取消注册
  221. */
  222. fun removeInvalidProxyConfig() {
  223. core.clearProxyConfig()
  224. }
  225. /**
  226. * 拨打电话
  227. * @param to String
  228. * @param isVideoCall Boolean
  229. */
  230. fun startCall(to: String, isVideoCall: Boolean) {
  231. try {
  232. val addressToCall = core.interpretUrl(to)
  233. addressToCall?.displayName = to
  234. val params = core.createCallParams(null)
  235. //启用通话录音
  236. // params?.recordFile = LinphoneUtils.getRecordingFilePathForAddress(context, addressToCall!!)
  237. //启动低宽带模式
  238. if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) {
  239. Log.w(TAG, "[Context] Enabling low bandwidth mode!")
  240. params?.enableLowBandwidth(true)
  241. }
  242. if (isVideoCall) {
  243. params?.enableVideo(true)
  244. core.enableVideoCapture(true)
  245. core.enableVideoDisplay(true)
  246. } else {
  247. params?.enableVideo(false)
  248. }
  249. if (params != null) {
  250. core.inviteAddressWithParams(addressToCall!!, params)
  251. } else {
  252. core.inviteAddress(addressToCall!!)
  253. }
  254. } catch (e: Exception) {
  255. e.printStackTrace()
  256. }
  257. }
  258. /**
  259. * 接听来电
  260. *
  261. */
  262. fun answerCall(call: Call) {
  263. Log.i(TAG, "[Context] Answering call $call")
  264. val params = core.createCallParams(call)
  265. //启用通话录音
  266. // params?.recordFile = LinphoneUtils.getRecordingFilePathForAddress(context, call.remoteAddress)
  267. if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) {
  268. Log.w(TAG, "[Context] Enabling low bandwidth mode!")
  269. params?.enableLowBandwidth(true)
  270. }
  271. params?.enableVideo(isVideoCall(call))
  272. call.acceptWithParams(params)
  273. }
  274. /**
  275. * 谢绝电话
  276. * @param call Call
  277. */
  278. fun declineCall(call: Call) {
  279. val voiceMailUri = corePreferences.voiceMailUri
  280. if (voiceMailUri != null && corePreferences.redirectDeclinedCallToVoiceMail) {
  281. val voiceMailAddress = core.interpretUrl(voiceMailUri)
  282. if (voiceMailAddress != null) {
  283. Log.i(TAG, "[Context] Redirecting call $call to voice mail URI: $voiceMailUri")
  284. call.redirectTo(voiceMailAddress)
  285. }
  286. } else {
  287. Log.i(TAG, "[Context] Declining call $call")
  288. call.decline(Reason.Declined)
  289. }
  290. }
  291. /**
  292. * 挂断电话
  293. */
  294. fun terminateCall(call: Call) {
  295. Log.i(TAG, "[Context] Terminating call $call")
  296. call.terminate()
  297. }
  298. fun micEnabled() = core.micEnabled()
  299. fun speakerEnabled() = core.outputAudioDevice?.type == AudioDevice.Type.Speaker
  300. /**
  301. * 启动麦克风
  302. * @param micEnabled Boolean
  303. */
  304. fun enableMic(micEnabled: Boolean) {
  305. core.enableMic(micEnabled)
  306. }
  307. /**
  308. * 扬声器或听筒
  309. * @param SpeakerEnabled Boolean
  310. */
  311. fun enableSpeaker(SpeakerEnabled: Boolean) {
  312. if (SpeakerEnabled) {
  313. AudioRouteUtils.routeAudioToEarpiece(core)
  314. } else {
  315. AudioRouteUtils.routeAudioToSpeaker(core)
  316. }
  317. }
  318. /**
  319. * 是否是视频电话
  320. * @return Boolean
  321. */
  322. fun isVideoCall(call: Call): Boolean {
  323. val remoteParams = call.remoteParams
  324. return remoteParams != null && remoteParams.videoEnabled()
  325. }
  326. /**
  327. * 设置视频界面
  328. * @param videoRendering TextureView 对方界面
  329. * @param videoPreview CaptureTextureView 自己界面
  330. */
  331. fun setVideoWindowId(videoRendering: TextureView, videoPreview: TextureView) {
  332. core.nativeVideoWindowId = videoRendering
  333. core.nativePreviewWindowId = videoPreview
  334. }
  335. /**
  336. * 设置视频电话可缩放
  337. * @param context Context
  338. * @param videoRendering TextureView
  339. */
  340. fun setVideoZoom(context: Context, videoRendering: TextureView) {
  341. VideoZoomHelper(context, videoRendering, core)
  342. }
  343. fun switchCamera() {
  344. val currentDevice = core.videoDevice
  345. Log.i(TAG, "[Context] Current camera device is $currentDevice")
  346. for (camera in core.videoDevicesList) {
  347. if (camera != currentDevice && camera != "StaticImage: Static picture") {
  348. Log.i(TAG, "[Context] New camera device will be $camera")
  349. core.videoDevice = camera
  350. break
  351. }
  352. }
  353. // val conference = core.conference
  354. // if (conference == null || !conference.isIn) {
  355. // val call = core.currentCall
  356. // if (call == null) {
  357. // Log.w(TAG, "[Context] Switching camera while not in call")
  358. // return
  359. // }
  360. // call.update(null)
  361. // }
  362. }
  363. //初始化一些操作
  364. private fun initLinphone() {
  365. configureCore()
  366. initUserCertificates()
  367. }
  368. private fun configureCore() {
  369. // 来电铃声
  370. core.isNativeRingingEnabled = false
  371. // 来电振动
  372. core.isVibrationOnIncomingCallEnabled = true
  373. core.enableEchoCancellation(true) //回声消除
  374. core.enableAdaptiveRateControl(true) //自适应码率控制
  375. }
  376. private var gsmCallActive = false
  377. private val phoneStateListener = object : PhoneStateListener() {
  378. override fun onCallStateChanged(state: Int, phoneNumber: String?) {
  379. gsmCallActive = when (state) {
  380. TelephonyManager.CALL_STATE_OFFHOOK -> {
  381. Log.i(TAG, "[Context] Phone state is off hook")
  382. true
  383. }
  384. TelephonyManager.CALL_STATE_RINGING -> {
  385. Log.i(TAG, "[Context] Phone state is ringing")
  386. true
  387. }
  388. TelephonyManager.CALL_STATE_IDLE -> {
  389. Log.i(TAG, "[Context] Phone state is idle")
  390. false
  391. }
  392. else -> {
  393. Log.i(TAG, "[Context] Phone state is unexpected: $state")
  394. false
  395. }
  396. }
  397. }
  398. }
  399. //设置存放用户x509证书的目录路径
  400. private fun initUserCertificates() {
  401. val userCertsPath = corePreferences!!.userCertificatesPath
  402. val f = File(userCertsPath)
  403. if (!f.exists()) {
  404. if (!f.mkdir()) {
  405. Log.e(TAG, "[Context] $userCertsPath can't be created.")
  406. }
  407. }
  408. core.userCertificatesPath = userCertsPath
  409. }
  410. companion object {
  411. // For Singleton instantiation
  412. @SuppressLint("StaticFieldLeak")
  413. @Volatile
  414. private var instance: LinphoneManager? = null
  415. fun getInstance(context: Context) =
  416. instance ?: synchronized(this) {
  417. instance ?: LinphoneManager(context).also { instance = it }
  418. }
  419. }
  420. }

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配合声音才好听

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

闽ICP备14008679号