基于Linphone开发Android音视频通话

语言: CN / TW / HK

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账号,如下图:

image.png

对于我们来说,就是设置SIP账户,需要填入几个参数: - 用户名:就是SIP账户号码或名称。 - 密码:该SIP账户对应的密码。 - 域名:填写SIP服务器(IPPBX)的IP地址或域名。 - 显示名:该SIP账户的显示名,是可选的。 - 传输:该SIP服务器支持传输协议,一般是UDP,也可以根据需要选择TCP或者TLS。

注册成功之后呢,软电话APP会有提示信息,左上角显示连接状态,如下图。

image.png

然后,输入对方的SIP账户,就可以通话了,如下图。

image.png

1.3 相关文档

下面是Linphone开发可能会用到的一些资料: - Linphone官网 :http://www.linphone.org/technical-corner/liblinphone - 官网文档:http://wiki.linphone.org/xwiki/wiki/public/view/Lib/Getting%20started/Android/ - 官方Android Demo:http://gitlab.linphone.org/BC/public/linphone-android - 各个版本的aar库:http://linphone.org/releases/maven_repository/org/linphone/linphone-sdk-android/

2,快速上手

2.1 编译App

首先,使用 Android Studio打开项目,然后构建/安装应用程序即可,可能编译过程中会比较慢。当然,也可以使用命令方式进行编译:

./gradlew assembleDebug //或者 ./gradlew installDebug

2.2 编译SDK

在Android应用程序开发中,引入第三方库的方式有源码依赖和sdk依赖。当然,我们也可以把sdk的代码下载下来,然后执行本地编译。

git clone http://gitlab.linphone.org/BC/public/linphone-sdk.git --recursive

然后安装官方文档的说明编译sdk。

2.3 集成Linphone

首先,需要引入linphone依赖,可以直接下载aar包执行本地以来,也可以使用gradle方式引入。此处,我们使用别人已经编译好的sdk:

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

CoreManager

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

implementation 'androidx.media:media:1.2.0'

然后,我们新建一个LinphoneManager类用来管理Linphone sdk,比如将Linphone注册到服务器、拨打语音电话等。

``` class LinphoneManager private constructor(private val context: Context) {

...  //省略其他代码

/**
 * 注册到服务器
 *
 * @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() } }

... //省略其他代码

} ```

CoreService

接下来就是CoreService类,该类的作用是一个保活服务,在来电时会调用震动方法和启动通知,所以必须在AndroidManifest.xml里注册。

<service android:name="org.linphone.core.tools.service.CoreService" android:foregroundServiceType="phoneCall|camera|microphone" android:label="@string/app_name" android:stopWithTask="false" />

官方Demo那样继承CoreService然后自己实现 。

``` class CoreService : CoreService() {

override fun onCreate() {
    super.onCreate()
    Log.i("[Service] Created")
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    Log.i("[Service] Ensuring Core exists")
    if (corePreferences.keepServiceAlive) {
        Log.i("[Service] Starting as foreground to keep app alive in background")
        if (!ensureCoreExists(applicationContext, pushReceived = false, service = this, useAutoStartDescription = false)) {
            coreContext.notificationsManager.startForeground(this, false)
        }
    } else if (intent?.extras?.get("StartForeground") == true) {
        Log.i("[Service] Starting as foreground due to device boot or app update")
        if (!ensureCoreExists(applicationContext, pushReceived = false, service = this, useAutoStartDescription = true)) {
            coreContext.notificationsManager.startForeground(this, true)
        }
        coreContext.checkIfForegroundServiceNotificationCanBeRemovedAfterDelay(5000)
    }
    return super.onStartCommand(intent, flags, startId)
}

override fun createServiceNotificationChannel() {
    // Done elsewhere
}

override fun showForegroundServiceNotification() {
    Log.i("[Service] Starting service as foreground")
    coreContext.notificationsManager.startCallForeground(this)
}

override fun hideForegroundServiceNotification() {
    Log.i("[Service] Stopping service as foreground")
    coreContext.notificationsManager.stopCallForeground()
}

override fun onTaskRemoved(rootIntent: Intent?) {
    if (!corePreferences.keepServiceAlive) {
        if (coreContext.core.isInBackground) {
            Log.i("[Service] Task removed, stopping Core")
            coreContext.stop()
        } else {
            Log.w("[Service] Task removed but Core in not in background, skipping")
        }
    } else {
        Log.i("[Service] Task removed but we were asked to keep the service alive, so doing nothing")
    }
    super.onTaskRemoved(rootIntent)
}

override fun onDestroy() {
    if (LinphoneApplication.contextExists()) {
        Log.i("[Service] Stopping")
        coreContext.notificationsManager.serviceDestroyed()
    }
    super.onDestroy()
}

} ```

3,其他优化

对于部分设备可能存在啸叫、噪音的问题,可以修改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配合声音才好听

源码参考: http://github.com/MattLjp/LinphoneCall