基於Linphone開發Android音視訊通話

xiangzhihong發表於2022-06-13

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賬號,如下圖:
在這裡插入圖片描述

對於我們來說,就是設定SIP賬戶,需要填入幾個引數:

  • 使用者名稱:就是SIP賬戶號碼或名稱。
  • 密碼:該SIP賬戶對應的密碼。
  • 域名:填寫SIP伺服器(IPPBX)的IP地址或域名。
  • 顯示名:該SIP賬戶的顯示名,是可選的。
  • 傳輸:該SIP伺服器支援傳輸協議,一般是UDP,也可以根據需要選擇TCP或者TLS。

註冊成功之後呢,軟電話APP會有提示資訊,左上角顯示連線狀態,如下圖。
在這裡插入圖片描述

然後,輸入對方的SIP賬戶,就可以通話了,如下圖。
在這裡插入圖片描述

1.3 相關文件

下面是Linphone開發可能會用到的一些資料:

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

相關文章