赞
踩
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平台上的音视频捕获,播放功能;
如果是Android系统用户,可以从谷歌应用商店安装或者从这个链接下载Linphone 。安装完成后,点击左上角的菜单按钮,选择进入助手界面。在助手界面,可以设定SIP账户或者Linphone账号,如下图:图片来自网路
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.core
-
- import 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.simpleName
-
- private var core: Core
- private var corePreferences: CorePreferences
- private var coreIsStart = false
- var registrationCallback: RegistrationCallback? = null
- var phoneCallback: PhoneCallback? = null
-
-
- init {
- //日志收集
- 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 = config
-
- val appName = context.getString(R.string.app_name)
- Factory.instance().setDebugMode(corePreferences.debugLogs, appName)
-
- core = Factory.instance().createCoreWithConfig(config, context)
- }
-
- private var previousCallState = Call.State.Idle
-
- private 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.autoAnswerDelay
- if (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 call
- if (core.callsNb == 1) {
- // Only try to route bluetooth / headphone / headset when the call is in StreamsRunning for the first time
- if (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 used
- if (!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_busy
- Reason.IOError -> R.string.call_error_io_error
- Reason.NotAcceptable -> R.string.call_error_incompatible_media_params
- Reason.NotFound -> R.string.call_error_user_not_found
- Reason.Forbidden -> R.string.call_error_forbidden
- else -> R.string.call_error_unknown
- }
- phoneCallback?.error(context.getString(id))
- }
- }
- gsmCallActive = false
- }
- }
- }
- previousCallState = state
- }
- }
-
- /**
- * 启动linphone
- */
- fun start() {
- if (!coreIsStart) {
- coreIsStart = true
- Log.i(TAG, "[Context] Starting")
- core.addListener(coreListener)
- core.start()
-
- initLinphone()
-
- val telephonyManager =
- context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
- Log.i(TAG, "[Context] Registering phone state listener")
- telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
- }
- }
-
- /**
- * 停止linphone
- */
- fun stop() {
- coreIsStart = false
- val telephonyManager =
- context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
-
- Log.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().language
- accountCreator.reset()
-
- accountCreator.username = username
- accountCreator.password = password
- accountCreator.domain = domain
- accountCreator.displayName = username
- accountCreator.transport = type
-
- accountCreator.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 = to
- val 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.voiceMailUri
- if (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.remoteParams
- return remoteParams != null && remoteParams.videoEnabled()
- }
-
-
- /**
- * 设置视频界面
- * @param videoRendering TextureView 对方界面
- * @param videoPreview CaptureTextureView 自己界面
- */
- fun setVideoWindowId(videoRendering: TextureView, videoPreview: TextureView) {
- core.nativeVideoWindowId = videoRendering
- core.nativePreviewWindowId = videoPreview
- }
-
- /**
- * 设置视频电话可缩放
- * @param context Context
- * @param videoRendering TextureView
- */
- fun setVideoZoom(context: Context, videoRendering: TextureView) {
- VideoZoomHelper(context, videoRendering, core)
- }
-
- fun switchCamera() {
- val currentDevice = core.videoDevice
- Log.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 = camera
- break
- }
- }
-
- // 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 = true
- core.enableEchoCancellation(true) //回声消除
- core.enableAdaptiveRateControl(true) //自适应码率控制
-
- }
-
- private var gsmCallActive = false
- private 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!!.userCertificatesPath
- val 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")
- @Volatile
- private var instance: LinphoneManager? = null
- fun getInstance(context: Context) =
- instance ?: synchronized(this) {
- instance ?: LinphoneManager(context).also { instance = it }
- }
-
- }
-
- }
网上已经有对linphone android sdk开发好的产品
LinphoneCall封装linphone android sdk的软话机
对于部分设备可能存在啸叫、噪音的问题,可以修改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配合声音才好听
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。