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