基於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 - 官網文檔:https://wiki.linphone.org/xwiki/wiki/public/view/Lib/Getting%20started/Android/ - 官方Android Demo:https://gitlab.linphone.org/BC/public/linphone-android - 各個版本的aar庫:https://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 https://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配合聲音才好聽

源碼參考: https://github.com/MattLjp/LinphoneCall