1. 程式人生 > >Android Sip學習(二)Android VoIP系統實現原理

Android Sip學習(二)Android VoIP系統實現原理


  隨著我國三網融合的推進,VoIP與IPTV(Interactive Personality TV)一起成為這一龐大工程的重要標誌。而目前手機中,VoIP的解決方案並不是很多,特別是在Google公司推出的開源作業系統Android中。儘管該系統推出時間不長,憑藉強大的功能、良好的介面、廣泛的商業支援,為使用者帶來很好的體驗,成為2010年最熱門且發展最快的手機作業系統。因此,兩者的結合,將是未來的發展趨勢。本文提出一種基於PJSIP協議棧的解決方案,通過Android本地開發工具(NDK),實現一個高效、穩定且功能強大的VoIP系統,具有較高的參考和實用價值。

1 VoIP設計方案

1.1 設計目標



  本方案所設計的系統包含以下功能:首先,完成使用者終端(如手機)中語音資料的採集與編碼,並通過RTP(實時傳輸協議)/RTCP(RTP傳輸控制協議)進行傳輸和控制;其次,完成會話的控制,包括會話的註冊、發起、維護與結束、登出等;再次,作為一個應用程式,必須實現一個良好的介面,與使用者互動;最後,作為一個開放系統,需具有良好的可擴充套件性。

1.2 總體設計

  本方案基本上符合Android的NDK框架的開發規範,將系統分為4層,如圖1所示。最上層為應用層,該層將在Android SDK的框架內,採用Java語言來實現;第二層為JNI層,SIP協議棧有很多種實現,其中,採用C語言的SIP協議棧在效率、速度、系統佔用方面有著超越其他庫(如Java協議棧)的優勢,因此,該方案將在第三層採用純C語言實現的PJSIP協議棧。為了讓Java應用層能呼叫協議棧層,在兩層之間需要一個銜接的橋樑,這就是JNI層。最後一層是驅動層,這部分一般是由手機廠商來實現的,本文將不做重點介紹。

QQ截圖20130808163146.jpg


2 VoIP的具體實現

  這裡將實現一個完整的VoIP系統,包括協議棧的實現、JNI的編寫以及上層UI的設計實現等。

  2.1 SIP協議棧及UA

  SIP協議棧直接關係到整個系統的質量與效率,本文將採用純C語言開發的PJSIP庫。該庫採用C語言開發,且原始碼開放,在相容性與效率上有明顯優勢,不僅體積小(完整的SIP封裝也不過150 KB),同時還實現了一個記憶體池,使得安全係數與執行效率大為提高,該系統所採用的就是優化後的PJSIP庫。

  2.1.1 PJSIP協議棧

  PJSIP協議棧遵循標準的SIP協議,採用分層架構:SIP/SDP訊息編碼解析層、傳輸管理層、SIP終端、事務層、會話層以及應用層等。由於SIP協議採用文字訊息傳送請求和響應,所以首先需要將SIP訊息按照編碼和解析,這就是SIP/SDP訊息編碼解析層所完成的功能。傳輸管理層用來管理使用者代理與伺服器之間的請求和相應;SIP終端是PJSIP中轉機制的實現,它主要負責管理各個SIP組建,例如像SIP終端例項註冊元件,分發訊息到事務層、會話層及應用層,回傳處理結果,管理定時器、I/O佇列等;事務層通過狀態機機制管理SIP信令,每一次狀態機狀態的改變都將觸發回撥函式;會話層負責會話的發起與響應,一般與應用層結合在一起,用於使用者互動,不同的平臺有不同的實現,本文使用Andriod的GUI來實現。



  PJSIP是一個高度封裝的庫,實際上它是通過PJSUA子庫來實現應用的。一個完整的PJSUA生命週期,首先需要初始化,通過函式init()來實現。在這個函式中,將建立代理、初始化變數和堆疊,以及建立一個UDP傳輸並在最後啟動代理;第二步將為UA新增使用者,如果需要的話,還要向伺服器註冊使用者;當用戶新增成功後,此時可以建立一個呼叫連線,發起會話;當會話連線成功後,就可以使用SRTP協議實時傳輸加密後的資料,進行通話。最後的過程是掛起或銷燬呼叫。

  2.1.2 UA原理

  UA(User Agency)是協議棧的具體實現,PJSIP通過封裝了的PJSUA來實現,在這一點上,大部分的SIP庫都大同小異,在此將介紹UA的工作原理。

  一個典型的UA包含UAC(User Agency Client)和UAS(User Agency Server)兩部分。會話由UAC發起。當呼叫發起時,UAC將首先發送“IN-VITE”訊息給SIP代理伺服器,伺服器收到“INVITE”訊息後將返回一個應答“200 OK”,並回答“ACK”進行確認,同時通知主叫使用者(即會話發起使用者)上線通話。如果主叫端(使用者端)主動結束會話,UAC將返回“BYE”訊息,同時通知伺服器;如果使用者端收到伺服器傳來的“BY-E”訊息,回答“200”,並結束會話。

  伺服器端,UAS收到UAC(使用者端)發來的“INVITE”訊息,首先從訊息中提取出主、被叫物件,然後檢查當前是否有空閒通道,若沒有則返回“486 BUSY HERE”(即系統忙)訊息;接著將檢查被叫使用者是否在服務區,如果被叫物件不在服務範圍,則返回“404 NOT FOUND”(即使用者不在服務區);若被叫使用者成功上線,則返回“200 OK”,同時準備開始會話。

  SIP協議棧一般使用SIP統一資源定位符(URL)來標識,它根據URL來定址,如叢集使用者“200”,“300”分別對應SIP使用者為“[email protected]. 1.100”,“300@192.168.1.100”。本文中也使用這種方式來測試通訊。

  2.2 JNI的實現

  PJSIP庫和Java類連線是通過JNI來實現的,這也是Android NDK的實現機制,JNI是SUN公司推出的用於Java呼叫其他語言的介面。

  首先需要一箇中間類,這個類中主要建立一些方法用於呼叫C/C++本地函式。它們的型別均為“publicstatic native int”,以便其他的Java類能夠呼叫。

  2.2.1 新建PJSIP類

  為各個待實現的類新建一個包,可以命名為“com.android.VoIP.pjsip”,在該包中新增該系統相關的一些類,主要有如下6個類:

  QQ截圖20130808163146.jpg


  這些類分別為上節中原理各個步驟的實現。這部分僅僅是為C庫的呼叫提供一個介面,因此具體的實現將放在本地C/C++程式中。

  2.2.2 標頭檔案的生成

  C庫與Java間還需通過一個字尾為“.h”的標頭檔案來銜接,這個標頭檔案可以手動編寫,也可以通過“Javah”來生成,該工具包含在JDK中,是由SUN公司提供的。

  Javah生產的標頭檔案包含一定的規則,例如,本例中,它將生成的函式宣告為“Java_com_android_IMSandroid_pjsip_**”的形式,以便在呼叫C庫時能正確識別。

  由於Java中的資料型別與C/C++不盡相同,因此還必須注意引數傳遞時引數型別的轉換。本文所涉及到的Java資料型別有String和int型,Javah生成的標頭檔案中會先定義好需要傳遞的引數型別以及函式返回型別,例如方法“add_account(String sip_user,Stringsip_dom-ain,String sip_passwd)”,在標頭檔案中將定義函式的形式為“JNIEXPORT jint JNICALL Java_com_android_IMSandroid_pjsip_add_lac-count(JNIEnv*,jclass,jstring,jstring,jstring)”,其中JNIEXPORT為JNI外部函式宣告,jint是“jni.h”中定義C語言中整形的對應型別,JNCALL是JNI關鍵字。比較特殊的是JNIEnv,它是一個指向型別為JNIEnv_的一個特殊JNI資料結構的指標,它的每個元素都指向一個JNI函式的指標,jclass會根據引用Java類的不同而不同,本文中“pjsip.class”是靜態類,因此這裡的jclass指的是類本身,如果是非靜態類則指的是物件。後面幾個就是pjsip類需要傳遞的引數,根據“jni.h”的定義,String型別對應jstring,int對應jint。然而這只是函式申明與類中方法的形式對應,引數的具體傳遞還需要相應的轉化,具體實現將在下一節詳細介紹。

  2.2.3 JNI介面函式的實現

  建立了pjsip庫類和標頭檔案之後,必須應用一個庫介面函式,這部分是pjsip介面的實現,限於篇幅,本文只講解幾個重要函式的實現。

  (1)init函式

  首先是init函式,對應的介面函式為JNICALL Java_com_android_IMSandroid_pjsip_init。該函式在系統初始化時呼叫,其作用是配置相關引數,併發起一個pjsua應用。它先通過函式“pjsua_create()”建立一個“pjsua”應用,然後通過三個函式“pjsua_config_default(&cfg)”,“pjsua_logging_config_default(&log_cfg”),“pjsua_media_config_default(&media_cfg)”配置其相關引數,其中cfg是pjsua的相關引數,主要是狀態改變時的回撥函式;log_cfg用來配置log級別;media_cfg中包含時鐘頻率、聲道數目等相關引數。

  完成配置之後就可以使用pjsua_init(&cfg,&log_cfg,&media_cfg)將先前配置的引數初始化。在初始化之後,還需為pjsua新增一個udp傳輸,這一步是通過pjsua_transport_create(PJSIP_TRANSPORT_UDP,&cfg,NULL)來實現的,cfg中包含指定的通訊埠,3GPP建議使用5060。

  需要注意的是,配置完以上引數之後,還需指定SPEEX編碼優先順序,一般將其設為最大,可以通過函式pisua_codec_set_priority(&-speex_codec_id,255)來實現。所有配置完成之後,就可以發起pjsua,即最後呼叫pjsua_start()。成功的話,本函式的返回型別為PJ_SUCCESS。

  (2)make_call函式

  另一個很重要的函式是make_call,其在本介面檔案中對應的函式為Java_com_Android_IMSandroid_pjsip_make_lcall,這個函式一般在發起會話時呼叫,它與上一個函式在結構上最大的不同在於本函式需要傳遞一個字串引數,前面提到,Java與C/C++在引數結構上並不完全相同,因此這裡需要將Java傳遞過來的String型別的引數轉化,可以通過“url_ptr=(char*)env-》GetStringUTFChars(url,&iscopy)”來實現。env-》GetStringUTFChars在“jni.h”中定義,其功能是將jsting型別(Java)的url複製到char*型別(C)的url_ptr中,以此來完成引數型別的轉換。

  為了保證傳遞地址的有效性,還需要使用pjsua_verify_sip_url(url_ptr)驗證,這個函式主要驗證url_ptr是符合SIP的規則,即是否是一個合法的SIP地址。然而char*型的地址pjsua中還是不能直接使用的,這是因為pjsua重新封裝了引數型別,所以最後還需要將其轉化成pj_ str_t型別,pjlib提供pj_str()函式可以完成轉化。在完成了引數的轉化後,呼叫“pjsua_call_make_call()”,將發起會話。

  (3)hangup函式和pjsua_destroy函式

  這兩個函式用來銷燬和結束通話會話,一般在需結束的時候呼叫,它們在介面函式中對應Java_com_android_

  IMSandroid_pjsip_hangup和Java_com_android_IMSandroid_pjsip_destroy,由於沒有引數傳遞,也沒有其他的呼叫,因此這兩個函式非常簡單,基本上直接呼叫pjsua提供的pjsua_call_hangup_all()和pjsua_destroy()就能實現。pisua中這兩個函式將完成記憶體釋放、賬戶登出等工作。

  (4)add_account函式

  該函式在基本的pjsua中並不是必須的,但如果要使用SIP伺服器的話,就必須實現該函式,它在介面函式中對應“Java_com_android_I-MSandroid_pjsip_add_1account”,同“make_call”一樣,也需要傳遞引數,不同的是,它傳遞三個引數,只是原理大體一樣。

  首先它將引數轉化後保持到cfg,通過“pjsua_acc_add(&cfg,PJ_TRUE,&ace_id)”將引數新增到pjsua。pjsua將以其中的sip伺服器為目的地址,註冊會話發起申請,經過一系列的操作之後,與目的地址發起會話。

  2.2.4 主程式與使用者介面

  系統的主程式是一個標準的Android應用程式,它使用Java語言開發,符合SDK規範。與一般的SDK程式不同的是,該類中必須使用Syst-em.loadLibrary載入PJSIP庫檔案。形式如下:

  System.loadLibrary(“pjsip-jni”);

  其中,pjsip-jni就是上節中PJSIP協議棧生成的庫。

  主程式中的基本方式均按照上節中的過程,建立並初始化PJSUA;當call按鍵被觸發時發起會話,呼叫make-call()方法;當用戶接受通話時,點選hang或cancel按鍵,觸發hang()或採用destry()方法等。

  使用者介面是通過Android SDK來實現的,這部分幾乎全都是Java語言,由於UI不是本文的重點,因此只介紹一個簡單的介面,實際應用中使用者互動是非常重要的。為了實現所需的功能,至少需要一個文字框來提供SIP地址,以及兩個按鍵來控制會話發起和結束。另外,在呼叫與通話過程中,還需要一個頁面來顯示,這裡可以通過對話方塊來顯示,最後的介面如圖2所示。

  QQ截圖20130808163146.jpg
3 封裝與除錯

  為了能在Android平臺上方便地使用該系統,在實現了PJSIP協議棧、JNI介面以及UI之後,還需將上面所有的模組進行封裝。Android SDK提供了一些很有用的工具,如aapt等,由於本文重點不在AndriodSDK,所以可以採用整合開發工具(如整合在Eclipse中的ADT)來封裝。在工程libs(如果不存在則新建)目錄下新建一個名為armeabi的目錄,將上節生成的.so庫檔案放到該目錄下。ADT在封裝資源時會自動將該庫檔案封裝到apk檔案中,apk是Android作業系統中應用程式的封裝形式,在所有android平臺中均能使用。

  封裝後安裝到Android手機、MID或虛擬機器中,併發起會話。與開源SIP軟體Linphone通訊的結果如圖2所示。

4 結語

  通過測試表明,該系統能夠對發起並很好地控制SIP信令,該系統由於採用SIP協議,因此與所有采用這一協議的軟體均能通訊,如Lin-phone,Kphone等,功能測試中表現良好,實現了VoIP的基本需求。同時如果要增加功能,可以在Java類中新增相應的方法並在應用層呼叫即可,具有一定的可擴充套件性。

  由於手機等手持裝置在規格和配置上的差異,該系統在具體的裝置上使用時,介面略有不同,但是同系統架構的手機使用時並不影響功能,在HTC Desire和MOTO Milestone上測試均能正常使用。但是,當移植到不同的架構時(即使同時ARM架構),仍需做一定的優化,一般採取主流平臺的多種版本方式來解決,這也是所有多廠商移動裝置上一個無法避免的問題。