iOS 實(shí)現(xiàn)Voip網(wǎng)絡(luò)電話

前言

Voip即網(wǎng)絡(luò)電話,voice over internet Protocol,將模擬的聲音訊號經(jīng)過壓縮與封包之后,以數(shù)據(jù)封包的形式在IP網(wǎng)絡(luò)進(jìn)行語音訊號的傳輸,通俗來說就是互聯(lián)網(wǎng)電話或IP電話。Voip網(wǎng)絡(luò)電話,中文就是“通過Ip數(shù)據(jù)包發(fā)送實(shí)現(xiàn)的語音業(yè)務(wù)”,它使你可以通過互聯(lián)網(wǎng)免費(fèi)或是資源很低的傳送語音,傳真,視頻和數(shù)據(jù)等業(yè)務(wù)。

基本原理

  • Voip的基本原理是通過語音的壓縮算法對語音數(shù)據(jù)編碼進(jìn)行壓縮處理,然后把這些語音數(shù)據(jù)按TCP/IP標(biāo)準(zhǔn)進(jìn)行打包,經(jīng)過IP網(wǎng)絡(luò)把數(shù)據(jù)包送至接收地,再把這些語音數(shù)據(jù)包串起來,經(jīng)過解壓處理后,恢復(fù)成原來的語音信號,從而達(dá)到由互聯(lián)網(wǎng)傳送語音的目的。

  • Voip電話的核心與關(guān)鍵設(shè)備是Voip網(wǎng)關(guān)。

  • 在整個IP電話系統(tǒng)中,網(wǎng)關(guān)設(shè)立在世界上各個地區(qū),完成當(dāng)?shù)仉娫捑W(wǎng)(PSTN)與Internet的接入與轉(zhuǎn)換處理等功能。終端設(shè)備將模擬語音信號傳到Voip網(wǎng)關(guān),網(wǎng)關(guān)接收到了標(biāo)準(zhǔn)電話信號以后,經(jīng)過數(shù)據(jù)自,編碼,壓縮處理,按IP協(xié)議打包到Internet上,根據(jù)傳輸路由,通過Internet發(fā)送到對應(yīng)網(wǎng)關(guān),對端的網(wǎng)關(guān)接收到了Internet傳來的IP包,經(jīng)過解壓處理后還原成模擬語音信號再轉(zhuǎn)到電話網(wǎng)系統(tǒng)(PSTN)

  • 網(wǎng)關(guān)因生產(chǎn)廠商不同而有所不同,但基本的組成模塊是一樣的,包括數(shù)據(jù)處理主機(jī),語音模塊,數(shù)據(jù)處理模塊,數(shù)據(jù)接續(xù)模塊和管理軟件模塊等。

  • 網(wǎng)關(guān)具有路由管理功能,它把各地區(qū)電話區(qū)號映射為相應(yīng)地區(qū)網(wǎng)關(guān)的IP地址,這些信息存放在一個數(shù)據(jù)庫中。數(shù)據(jù)接續(xù)處理軟件將完成呼叫處理,數(shù)字語音打包,路由管理等功能。在用戶撥打VOIP電話時(shí),網(wǎng)關(guān)根據(jù)電話區(qū)號數(shù)據(jù)庫資料,確定相應(yīng)網(wǎng)關(guān)的IP地址,并將此IP地址加入IP數(shù)據(jù)包中,同時(shí)選擇最佳路由,以減少傳輸延時(shí),IP數(shù)據(jù)包經(jīng)Internet到達(dá)目的地的網(wǎng)關(guān)。

常用的Voip協(xié)議(Control Protocol)

H.323

  • H.323是一種ITU-T標(biāo)準(zhǔn),最初用于局域網(wǎng)上的多媒體會議,后來擴(kuò)展至覆蓋Voip。該標(biāo)準(zhǔn)既包括了點(diǎn)對點(diǎn)通信也包括了多點(diǎn)會議。H.232定義了四種邏輯組成部分:終端,網(wǎng)關(guān),網(wǎng)守以及多點(diǎn)控制單元。
  • 終端,網(wǎng)關(guān)和MCU均視為終端點(diǎn)。
  • 會話發(fā)起協(xié)議(SIP) 是簡歷Voip鏈接的IETF標(biāo)準(zhǔn)。SIP是一種應(yīng)用層控制協(xié)議,用于和一個或多個參與者創(chuàng)建,修改和終止會話。SIP的結(jié)構(gòu)與HTTP(客戶端-服務(wù)器協(xié)議)相似。客戶機(jī)發(fā)出請求,并發(fā)送給服務(wù)器,服務(wù)器處理這些請求給客戶機(jī)發(fā)送一個響應(yīng),該請求與響應(yīng)行程一次事務(wù)。
  • 媒體網(wǎng)關(guān)控制協(xié)議MGCP 定義了呼叫控制單元與電話網(wǎng)關(guān)之間的通信服務(wù)。
  • 媒體網(wǎng)關(guān)控制協(xié)議是IETF和IU-T共同努力的結(jié)果。是一種用于控制物理上分開的多媒體網(wǎng)關(guān)的協(xié)議單元的協(xié)議,從而可以從媒體轉(zhuǎn)化中分離呼叫控制。

SIP協(xié)議

  • Voip一般通過SIP作為應(yīng)用層的信令控制協(xié)議【session intiation protocol】, SIP通過創(chuàng)建會話,斷開會話從而實(shí)現(xiàn)會話的管理。SIP可以創(chuàng)建多個會話,多個會話可以同時(shí)工作,對多會話進(jìn)行管理從而實(shí)現(xiàn)多會話的功能。SIP在傳輸層可以采用UDP協(xié)議也可以采用TCP協(xié)議,但是對于對講的特點(diǎn)來說大部分采用的是UDP協(xié)議,UDP協(xié)議簡單并且更加符合對講的即時(shí)性的特點(diǎn),但是在網(wǎng)絡(luò)不好情況下會產(chǎn)生丟包。

  • SIP會話建立之后RTCP和RTP分別負(fù)責(zé)會話上媒體傳輸?shù)目刂坪兔襟w傳輸。RTP上傳輸?shù)拿襟w的編碼方式為會話建立時(shí)客戶端和服務(wù)器之間協(xié)商的編碼方式,RTP協(xié)議和RTP控制協(xié)議RTCP一起使用,而且它是建立在用戶數(shù)據(jù)協(xié)議UDP上的。

關(guān)鍵技術(shù)

VOIP網(wǎng)絡(luò)電話的關(guān)鍵技術(shù)包括:信令技術(shù),編碼技術(shù),實(shí)時(shí)傳輸技術(shù),服務(wù)質(zhì)量保證技術(shù),以及網(wǎng)絡(luò)傳輸技術(shù)等

信令技術(shù)

  • 信令技術(shù)保證電話呼叫的順利實(shí)現(xiàn)和語音質(zhì)量,被廣泛接受的Voip控制信令體系包括ITUT的H.323系列和IETF的會話初始化協(xié)議SIP

  • ITU的H.232協(xié)議定義了在無業(yè)務(wù)質(zhì)量保證的因特網(wǎng)或其他分組網(wǎng)上多媒體通信的協(xié)議以及其規(guī)程。H.323標(biāo)準(zhǔn)是局域網(wǎng),廣域網(wǎng)和internet上的多媒體提供技術(shù)基礎(chǔ)保障

  • SIP是一種應(yīng)用層協(xié)議,可以用UDP或TCP作為傳輸協(xié)議。與H.323不同的是:SIP是一種基于文本的協(xié)議,用SIP規(guī)則資源定位語言描述(SIP Uniform Resource Locators),這樣易于實(shí)現(xiàn)和調(diào)試,更重要的是靈活性和擴(kuò)展性好。由于SIP僅作用于初始化呼叫,而不是傳輸媒體數(shù)據(jù),因而造成的附加傳輸代價(jià)也不大。與H.323相比,SIP還有建立呼叫快,支持傳送電話號碼的特點(diǎn)。

編碼技術(shù)

  • 語音壓縮編碼技術(shù)是voip網(wǎng)絡(luò)電話技術(shù)的一個重要組成部分。主要的編碼技術(shù)有ITU-T定義的G.729 G.723等,其中G.729 可將經(jīng)過采樣的64kbit/s 話音以幾乎不失真的質(zhì)量壓縮至8kbit/s。由于在分組交換網(wǎng)絡(luò)中,業(yè)務(wù)質(zhì)量不能得到很好保證,因而需要話音的編碼具有一定的靈活性,即編碼速率、編碼尺度的可變可適應(yīng)性。G723.1 采用5.3/6.3K bit/s 雙速率話音編碼,其話音質(zhì)量好,但是處理時(shí)延較大,它是已標(biāo)準(zhǔn)化的最低速率的話音編碼算法。

實(shí)時(shí)傳輸技術(shù)

  • 實(shí)時(shí)傳輸技術(shù)主要是采用RTP實(shí)時(shí)傳輸協(xié)議。RTP是提供端到端的包括音頻在內(nèi)的實(shí)時(shí)數(shù)據(jù)傳送的協(xié)議,RTP標(biāo)準(zhǔn)定義了兩個子協(xié)議,RTP和RTCP

  • 數(shù)據(jù)傳輸協(xié)議RTP,用于實(shí)時(shí)傳輸數(shù)據(jù)。改協(xié)議提供的信息包括:時(shí)間戳(用于同步),序列號(用于丟包和重排序檢測),以及負(fù)載格式(用于說明數(shù)據(jù)的編碼格式)

  • 控制協(xié)議RTCP,用于服務(wù)質(zhì)量反饋和同步流媒體。相對于RTP來說,RTCP所占的帶寬非常小,通常只有5%

RTP
  • RTP數(shù)據(jù)協(xié)議負(fù)責(zé)對流媒體數(shù)據(jù)進(jìn)行封包并實(shí)現(xiàn)媒體流的實(shí)時(shí)傳輸,每一個RTP數(shù)據(jù)報(bào)都由頭部Header和負(fù)載Payload兩個部分組成,其中頭部前12個字節(jié)的含義是固定的,而負(fù)載則可以是音頻或視頻數(shù)據(jù)

  • RTP頭部格式

image.png

V:RTP協(xié)議的版本號, 占2位,當(dāng)前協(xié)議版本號為2

P:填充標(biāo)志, 占1位,如果P = 1, 則在該報(bào)文的尾部填充一個或多個額外的八位組,它們不是有效載荷的一部分

X:擴(kuò)展標(biāo)志,占1位,如果X =1, 則在RTP報(bào)頭后跟有一個擴(kuò)展報(bào)頭

CC:CSRC計(jì)數(shù)器,占4位,指示CSRC表示符的個數(shù)

M:標(biāo)記,占1位,不同的有效載荷有不同的含義,對于食品,標(biāo)記一幀的結(jié)束,對于音頻,標(biāo)記會話的開始

PT:有效荷載類型,占7位, 用于說明RTP報(bào)文中有效載荷的類型,如GSM音頻,JPEM圖像等,在流媒體中大部分是用來區(qū)分音頻流和視頻流的,這樣便于客戶端進(jìn)行解析。

序列號:占16位,用于標(biāo)識發(fā)送者所發(fā)送的RTP報(bào)文的序列號,每次發(fā)送一個報(bào)文,序列號加1。 這個字段當(dāng)下層的承載協(xié)議用UDP的時(shí)候,網(wǎng)絡(luò)狀態(tài)不好的時(shí)候可以用來檢查丟包,同時(shí)出現(xiàn)網(wǎng)絡(luò)抖動的情況可以用來對數(shù)據(jù)進(jìn)行重新排序,序列號的初始值是隨機(jī)的,同時(shí)音頻包和視頻包的sequence是分別記數(shù)的。

時(shí)戳(Timestamp):占32位,必須使用90kHZ時(shí)鐘頻率。時(shí)戳反應(yīng)了該RTP報(bào)文的第一個八位組的采樣時(shí)刻。接受者使用時(shí)戳來極端延遲和延遲抖動,并進(jìn)行同步控制。

同步信源(SSRC)標(biāo)識符:占32位,用于表示同步信源。該標(biāo)識符是隨機(jī)選擇的,參加同一視頻會議的兩個同步信源不能有相同的SSRC

特約信源(CSRC)標(biāo)識符:每個CSRC標(biāo)識符占32位,可以有0-15個,每個CSRC標(biāo)識了包含在該RTP報(bào)文有效載荷中的所有特約信源

網(wǎng)絡(luò)傳輸技術(shù)

  • Voip網(wǎng)絡(luò)電話中網(wǎng)絡(luò)傳輸技術(shù)主要是TCP和UDP,此外還包括網(wǎng)關(guān)互聯(lián)技術(shù),路由選擇技術(shù),網(wǎng)絡(luò)管理技術(shù)以及安全認(rèn)證和計(jì)費(fèi)技術(shù)等

服務(wù)質(zhì)量技術(shù)

Voip網(wǎng)絡(luò)電話中主要采用資源預(yù)留協(xié)議RSVP 以及進(jìn)行服務(wù)質(zhì)量監(jiān)控的實(shí)時(shí)傳輸控制協(xié)議RTCP來避免網(wǎng)絡(luò)擁塞,保障通話質(zhì)量

小型Voip電話系統(tǒng)的建立

一般而言,部署Voip系統(tǒng)時(shí)涉及三個主要組成部分:IPPBX,VOIP電話和Voip運(yùn)營商網(wǎng)絡(luò)。我們總是通過Voip網(wǎng)絡(luò)和傳統(tǒng)的PSTN電話系統(tǒng)進(jìn)行互聯(lián)。當(dāng)然,我們也可以通過部署Voip網(wǎng)關(guān)(VoIP Gateway)直接連接傳統(tǒng)電話網(wǎng)絡(luò),而不需要單獨(dú)的voip服務(wù)商。
本文主要采用miniSIPService搭建Voip服務(wù)系統(tǒng)。MSS是專業(yè)的純軟件方式的PBX,能運(yùn)行在Windows以及Linux等系統(tǒng)。它能支持我們目前需要的各種特性。

在iOS上開發(fā)Voip應(yīng)用

目前基于SIP協(xié)議的Voip是應(yīng)用最廣泛的,iOS上的Voip應(yīng)用通常使用開源協(xié)議棧進(jìn)行開發(fā),成熟的開源SIP協(xié)議棧有PJSIP和Linphone,本文將通過PJSIP來實(shí)現(xiàn)一個簡單的語音通話App

PJSIP開源庫的組成部分

PJSIP -Open Source SIP stack[開源的SIP協(xié)議棧]
PJMEDIA - Open Source Media Stack[開源的媒體棧]
PJNATH - Open Source NAT Traversal Helper Library[開源的NAT-T輔助庫]
PJLIB-UTIL - Auxiliary Library[輔助工具庫]
PJLIB - Ultra Portable Base Framework Library[基礎(chǔ)框架庫]

PJSUA API使用以及說明
這里使用的API基本都是來自pjsua,這個是建立在pjsip基礎(chǔ)上的一層純C封裝,下面展示了pjsip初始化到撥打電話和掛斷電話的Api調(diào)用邏輯

image.png

pjsua接口使用時(shí),需要創(chuàng)建,初始化,開始和銷毀的操作:pjsua_create,pjsua_init,pjsua_start,pjsua_destory

pjsua_transport_create創(chuàng)建sip信令發(fā)送和接受需要的相關(guān)socket等資源

pjsua_acc_add添加撥打電話賬號,賬號類似于我們的手機(jī)號碼,可以起到定位的功能。

撥打電話的掛斷電話:pjsua_call_make_call,pjsua_call_hangup_all

Pjsip只是完成兩個功能

1.使用sip信令協(xié)商雙方使用音頻,視頻通話使用的rtp,rtcp的socket端口,視頻編碼器和音頻編碼器的類型和相關(guān)的編碼參數(shù),使用的網(wǎng)絡(luò)類型。

2.完成音頻,視頻通話的socket通道,傳出音頻和視頻數(shù)據(jù)

服務(wù)器的安裝

1.安裝MySQL

去官方下載最新版本的MySQL,本文使用最新的MySQL Community Server 5.6.25 GA版,Mac用戶方便起見可以使用dmg格式的。由于OS X的路徑問題,后續(xù)啟動Kamailio時(shí)會發(fā)生找不到庫的情況,所以需要創(chuàng)建庫的軟鏈接,即安裝完畢后終端執(zhí)行命令:

# sudo ln -s/usr/local/mysql/lib/libmysqlclient.18.dylib /usr/lib/libmysqlclient.18.dylib

最后我們就可以在系統(tǒng)偏好設(shè)置里將MySQL Server打開了。


image.png
2.源碼下載
  # git clone --branch 4.2 --single-branch \

     git://git.sip-router.org/kamailiokamailio

  # cd kamailio
  

我是直接根據(jù)鏈接下載的,下載之后直接解壓,鏈接地址

3.編譯安裝

在執(zhí)行完步驟2的基礎(chǔ)上,首先添加MySQL的環(huán)境變量:

# export PATH=$PATH:/usr/local/mysql-5.6.25-osx10.8-x86_64/bin

然后執(zhí)行下列命令進(jìn)行編譯與安裝

# make include_modules=db_mysql cfg

# sudo make all

# sudo make install

make all的時(shí)候如果不使用sudo提權(quán)的話,可能就失敗了。雖然sudo不是好的做法,但是方便起見這里直接sudo。這邊下去會出現(xiàn)的一個問題是mysql的module編譯失敗。提示“mysql/mysql.h”找不到。這時(shí)候要去kamailio-4.3.0/modules/db_mysql下,把源碼頭文件引用<mysql/mysql.h>都改成<mysql.h>。

這時(shí)候你應(yīng)該可以完成make all了。接下去install,這一步也不會出問題了。

4.Kamailio的配置

修改kamctlrc文本,執(zhí)行

# sudo vim /usr/local/etc/kamailio/kamctlrc

去掉SIP_DOMAIN前的#符號,改成自己的服務(wù)器地址。
我的是SIP_DOMAIN=127.0.0.1。即本機(jī)IP。
然后去掉 DBENGINE=MYSQL前的注釋語句,選定mysql數(shù)據(jù)庫。

然后再修改kamailio.cfg文本,執(zhí)行

# sudo vim /usr/local/etc/kamailio/kamailio.cfg

在文本的開頭加上一行:


#!defineWITH_MYSQL

5.創(chuàng)建數(shù)據(jù)庫,并開啟服務(wù)器

執(zhí)行命令來創(chuàng)建數(shù)據(jù)庫

# /usr/local/sbin/kamdbctl create

如果提示輸入密碼,此時(shí)注意密碼是默認(rèn)在kamdbctl的配置文件中的,通過下面命令可以查看

cd /usr/local/etc/kamailio/
vim kamctlrc

你可以看到服務(wù)器的一些設(shè)置,如果密碼自己設(shè)置錯誤,或者不知,則可以將下面幾行打開并進(jìn)行重新執(zhí)行數(shù)據(jù)庫創(chuàng)建的命令。


image.png

下面報(bào)錯:


/usr/local/lib/kamailio/kamctl/kamdbctl.mysql: line 105: mysql: 
> command not found

解決的辦法:


# ln -s /usr/local/mysql/bin/mysql /usr/bin

創(chuàng)建完數(shù)據(jù)庫后需要添加用戶

我們這里添加三個用戶user1, user2以及user3,密碼都是123:

cuilinhaodeMacBook-Pro:sbin cuilinhao$ ./kamctl add user1 123
mysql: [Warning] Using a password on the command line interface can be insecure.
mysql: [Warning] Using a password on the command line interface can be insecure.
new user 'user1' added
cuilinhaodeMacBook-Pro:sbin cuilinhao$ ./kamctl add user2 123
mysql: [Warning] Using a password on the command line interface can be insecure.
mysql: [Warning] Using a password on the command line interface can be insecure.
new user 'user2' added

使用工具查看,如下圖


image.png

添加完用戶后需要開啟SIP Server:


cuilinhaodeMacBook-Pro:sbin cuilinhao$ ./kamctl start

\E[37;33mINFO: Starting Kamailio :

如果這一步出問題注意檢查步驟1中的庫的軟鏈接是否建立。除了軟連接的問題,其實(shí)更重要的一個問題是會提示


\E[37;31mERROR: PID file /var/run/kamailio/kamailio.pid does not exist -- Kamailio start failed
cuilinhaodeMacBook-Pro:sbin cuilinhao$ vim /var/run/kamailio/kamailio.pid
cuilinhaodeMacBook-Pro:sbin cuilinhao$ cd /var/run/

這里有一個權(quán)限問題,而并非是某個模塊沒有編譯。使用


kamailio -M 8 -E -e -dd

可以查看具體失敗的信息,也可以去系統(tǒng)日志看。具體的錯誤原因是ERROR: init_unix_sock: bind: No such file or directory。一個簡單的解決辦法是sudo vim /usr/local/etc/kamailio/kamctlrc,去掉 PID_FILE=/var/run/kamailio/kamailio.pid前的#號,然后在/var/run下新建一個歸屬于當(dāng)前Mac用戶的kamailio文件夾。


uilinhaodeMacBook-Pro:sbin cuilinhao$ ls -lh /var/run/

查找kamalio,

cuilinhaodeMacBook-Pro:sbin cuilinhao$ ls -lh /var/run/ |grep kam
drwxr-xr-x  6 root             daemon            192B 12 21 17:43 kamailio
cuilinhaodeMacBook-Pro:sbin cuilinhao$ ls -lh /var/run/kamailio/
total 8
-rw-r--r--  1 root  daemon     5B 12 21 17:43 kamailio.pid
srw-------  1 root  daemon     0B 12 21 17:43 kamailio_ctl
prw-rw----  1 root  daemon     0B 12 21 17:43 kamailio_rpc.fifo
srw-rw----  1 root  daemon     0B 12 21 17:43 kamailio_rpc.sock

重置一下kamalio的權(quán)限等級

cuilinhaodeMacBook-Pro:sbin cuilinhao$ whoami
cuilinhao
cuilinhaodeMacBook-Pro:sbin cuilinhao$ chow -R cuilinhao:staff /var/run/kamailio/
-bash: chow: command not found
cuilinhaodeMacBook-Pro:sbin cuilinhao$ chown -R cuilinhao:staff /var/run/kamailio/
chown: /var/run/kamailio//kamailio_ctl: Operation not permitted
chown: /var/run/kamailio//kamailio_rpc.fifo: Operation not permitted
chown: /var/run/kamailio//kamailio_rpc.sock: Operation not permitted
chown: /var/run/kamailio//kamailio.pid: Operation not permitted
chown: /var/run/kamailio/: Operation not permitted
cuilinhaodeMacBook-Pro:sbin cuilinhao$ sudo chown -R cuilinhao:staff /var/run/kamailio/

chown: /var/run/kamailio//kamailio_ctl: Operation not permitted
chown: /var/run/kamailio//kamailio_rpc.fifo: Operation not permitted
chown: /var/run/kamailio//kamailio_rpc.sock: Operation not permitted
chown: /var/run/kamailio//kamailio.pid: Operation not permitted
chown: /var/run/kamailio/: Operation not permitted
cuilinhaodeMacBook-Pro:sbin cuilinhao$ sudo chown -R cuilinhao:staff /var/run/kamailio/
cuilinhaodeMacBook-Pro:sbin cuilinhao$ ls -lh /var/run/ |grep kam
drwxr-xr-x  6 cuilinhao        staff             192B 12 21 17:43 kamailio
cuilinhaodeMacBook-Pro:sbin cuilinhao$ ./kamctl stop

\E[37;33mINFO: Stopping Kamailio :
./kamctl: line 2013: kill: (1728) - Operation not permitted
\E[37;33mINFO: stopped

再次開啟如下:

cuilinhaodeMacBook-Pro:sbin cuilinhao$ ./kamctl start

\E[37;33mINFO: Starting Kamailio :
 1728   ??  S      0:00.02 ./kamailio -P /var/run/kamailio/kamailio.pid -f /usr/local/etc/kamailio//kamailio.cfg
 1729   ??  S      0:00.00 ./kamailio -P /var/run/kamailio/kamailio.pid -f /usr/local/etc/kamailio//kamailio.cfg
 1730   ??  S      0:00.00 ./kamailio -P /var/run/kamailio/kamailio.pid -f /usr/local/etc/kamailio//kamailio.cfg
 

則服務(wù)器開啟成功。

現(xiàn)在自己電腦啟動服務(wù)器


  cd /usr/local/sbin/
  ls
cuilinhaodeMacBook-Pro:sbin cuilinhao$ sudo ./kamctl start

\E[37;33mINFO: Starting Kamailio :
\E[37;33mINFO: started (pid: 1926)
cuilinhaodeMacBook-Pro:sbin cuilinhao$
  

自己電腦mySql 密碼 root
Kamailio 密碼 Kamailiorw

Demo演示 核心代碼

初始化

初始化pjsip,該方法在APPDelegate中調(diào)用,主要是對通話進(jìn)行配置,來電回調(diào)設(shè)置以及媒體先關(guān)的配置
代碼如下:

///初始化通話配置
        pjsua_config config;
        pjsua_config_default (&config);
        //登錄狀態(tài)改變回調(diào)
        //config.cb.on_reg_state2 = &on_reg_state2;
        config.cb.on_reg_state = &on_reg_state;
        
        
        //來電回調(diào)
        config.cb.on_incoming_call = &on_incoming_call;
        //媒體狀態(tài)回調(diào)(通話建立后,要播放RTP流)
        config.cb.on_call_media_state = &on_call_media_state;
        //設(shè)置通話狀態(tài)改變回調(diào)
        config.cb.on_call_state = &on_call_state;
        
        //媒體相關(guān)配置
        pjsua_media_config media_config;
        pjsua_media_config_default(&media_config);
        media_config.clock_rate = 16000;
        media_config.snd_clock_rate = 16000;
        media_config.ec_tail_len = 0;
        
        
        //初始化日志配置
        pjsua_logging_config log_config;
        pjsua_logging_config_default(&log_config);
        
        //日記等級0不打印日記 4打印詳情日記
        //log_config.console_level = 0;
        //status = pjsua_init(&config, &log_config, NULL);
        //判斷是否初始化成功
        //if (status != PJ_SUCCESS)
        //{
          //  NSLog(@"創(chuàng)初始化pjsua配置失敗");
        //}
        
        log_config.msg_logging = PJ_TRUE;
        log_config.console_level = 4;
        log_config.level = 5;
        
        //初始化pjsua配置
        status = pjsua_init(&config, &log_config, &media_config);
        //判斷是否初始化成功
        if (status != PJ_SUCCESS)
        {
            NSLog(@"---初始化pjsua配置失敗----");
        }
        
        //初始化UDP
        {
            pjsua_transport_config config;
            pjsua_transport_config_default(&config);
            
            status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &config, NULL);
            if (status != PJ_SUCCESS)
            {
                NSLog(@"--添加UDP傳輸失敗---");
            }
        }
        

配置成功之后可以看到下面打印結(jié)果。


image.png

回調(diào)函數(shù)配置


///登錄狀態(tài)改變回調(diào)
static void on_reg_state2(pjsua_acc_id acc_id, pjsua_reg_info *info)
{
   
    
    if (info->renew != 0) {
        //--info里面是一個結(jié)構(gòu)體 cbparm也是一個結(jié)構(gòu)體, 然后cbparm中包含code屬性,這是一個c語言的寫法
        if (info->cbparam->code == 200) {
            NSLog(@"登錄成功");
        }
        else{
            NSLog(@"登錄失敗code:%d ",info->cbparam->code);
        }
    }
    else{
        if (info->cbparam->code == 200)
        {
            NSLog(@"SIP退出登錄成功");
        }
    }
}

///來電回調(diào)
static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, pjsip_rx_data *rdata){
    //獲取來電信息
    pjsua_call_info info;
    pjsua_call_get_info(call_id, &info);
    NSString *callStr = [NSString stringWithUTF8String:info.remote_info.ptr];
    //這里發(fā)送一個通知
    [[NSNotificationCenter defaultCenter] postNotificationName:@"calling" object:nil userInfo:@{@"calledCAcount":callStr}];
    NSLog(@"%@",callStr);
}

///呼叫回調(diào)
static void on_call_media_state(pjsua_call_id call_id)
{
    //獲取呼叫信息
    pjsua_call_info info;
    pjsua_call_get_info(call_id, &info);
    
    if (info.media_status == PJSUA_CALL_MEDIA_ACTIVE)
    {//呼叫接通
        
        //建立單向媒體流從源到匯
        pjsua_conf_connect(info.conf_slot, 0);
        pjsua_conf_connect(0, info.conf_slot);
        
        NSLog(@"呼叫成功,等待對方接聽");
    }
}

//通話狀態(tài)改變回調(diào)
static void on_call_state(pjsua_call_id call_id, pjsip_event *e)
{
    
    // 通話狀態(tài):CALLING
    // 通話狀態(tài):EARLY
    // 通話狀態(tài):EARLY
    // 呼叫成功,等待對方接聽
    // 通話狀態(tài):CONNECTING
    // 通話狀態(tài):CONFIRMED
    // DISCONNCTD  對方掛斷
    //獲取通話信息
    pjsua_call_info ci;
    pjsua_call_get_info(call_id, &ci);
    
    NSString *status = [NSString stringWithUTF8String:ci.state_text.ptr];
    NSLog(@"通話狀態(tài):%@",status);
    
}


登錄界面實(shí)現(xiàn)

首先要監(jiān)聽注冊相關(guān)通知,在SIP中,注冊就是將客戶端的相關(guān)信息注冊到服務(wù)器的一個類似路由表一樣的列表中,這樣其他客戶端呼叫時(shí),經(jīng)過服務(wù)器的路由就可以找到正確的客戶端地址,從而建立P2P的鏈接。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleRegisterStatus:) name:@"SIPRegisterStatusNotification" object:nil];
    
}

登錄按鈕觸摸事件中,配置并完成對服務(wù)器的注冊:


//配置賬號信息
    pjsua_acc_config config;
    pjsua_acc_config_default(&config);
    
    char accountChar[50];
    strcpy(accountChar, [accountsString UTF8String]);
    char passwordChar[50];
    strcpy(passwordChar, [passwordString UTF8String]);
    
    //設(shè)置賬號格式:  sip:賬號@服務(wù)地址
    char sipAccount[50];
    sprintf(sipAccount, "sip:%s@%s", accountChar, [ipString UTF8String]);
    config.id = pj_str(sipAccount);
    
    //設(shè)置服務(wù)器格式: sip:服務(wù)器地址
    char serviceId[50];
    sprintf(serviceId, "sip:%s",[ipString UTF8String]);
    
    NSLog(@"----服務(wù)器地址---%s", [ipString UTF8String]);
    
    config.reg_uri = pj_str(serviceId);
    
    //不設(shè)置超時(shí)時(shí)間
    config.reg_retry_interval = 0;
    
    //注冊賬號個數(shù) 最多8個
    config.cred_count = 1;
    //注冊方案
    config.cred_info[0].scheme = pj_str("Digest");
    //符號“*”
    config.cred_info[0].realm = pj_str("*");
    //賬號
    config.cred_info[0].username = pj_str(accountChar);
    //數(shù)據(jù)類型
    config.cred_info[0].data_type = PJSIP_CRED_DATA_PLAIN_PASSWD;
    //密碼
    config.cred_info[0].data = pj_str(passwordChar);
    status = pjsua_acc_add(&config, PJ_TRUE, &_acc_id);
    if (status != PJ_SUCCESS)
    {
        NSLog(@"---登錄SIP電話失敗");
        return NO;
    }
    
  • 在設(shè)置中config.reg_retry_interval應(yīng)設(shè)置為0,這個是注冊失敗時(shí),進(jìn)行重試的時(shí)間間隔,如果不設(shè)置為0則它會不斷進(jìn)行嘗試注冊(用戶名密碼驗(yàn)證失敗也會如此),這里為了不做復(fù)雜的類似超市這樣的處理,直接將它設(shè)為0
  • 如果登錄成功之后就會進(jìn)行回調(diào)
pjsip_status_code status = [notification.userInfo[@"status"] intValue];
    NSString *statusText = notification.userInfo[@"status_text"];
    
    
    //---
    //賬號ID
    //pjsua_acc_id acc_id = [notification.userInfo[@"acc_id"] intValue];
    
    if (status != PJSIP_SC_OK)
    {
        NSLog(@"--------登錄失敗,錯誤信息: %d(%@)", status, statusText);
        return;
    }
    
    NSLog(@"----->>>登錄回調(diào)成功");
    
    UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController *dialViewController = [sb instantiateViewControllerWithIdentifier:@"DialVC"];
    
    
    CATransition *transition = [[CATransition alloc] init];
    
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
    transition.type = kCATransitionFade;
    transition.duration  = 0.5;
    transition.removedOnCompletion = YES;
    
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    [keyWindow.layer addAnimation:transition forKey:@"change_view_controller"];
    
    keyWindow.rootViewController = dialViewController;
    

通過通知帶回來的信息,來判斷是否登錄成功,如果登錄成功則直接跳轉(zhuǎn)到撥打電話界面。

撥打電話實(shí)現(xiàn)

主要是調(diào)用pjsua_call_make_call方法,根據(jù)用戶名進(jìn)行呼叫,呼叫分一下幾個狀態(tài):

  • PJSIP_INV_STATE_DISCONNECTED 呼叫狀態(tài)

  • PJSIP_INV_STATE_CALLING 呼叫中 狀態(tài)

  • PJSIP_INV_STATE_CONNECTING 正在連接狀態(tài)

  • PJSIP_INV_STATE_CONFIRMED 掛斷狀態(tài)

char accountChar[50];
    sprintf(accountChar,"sip:%s@%s",[accountsString UTF8String],[self.ip UTF8String]);
    pj_str_t url = pj_str(accountChar);
    
    //初始化呼叫
    pjsua_call_setting  call_set;
    pjsua_call_setting_default(&call_set);
    
    pj_status_t status = pjsua_call_make_call(_acc_id, &url, &call_set, NULL, NULL, NULL);
    if (status != PJ_SUCCESS)
    {
        NSLog(@"呼叫失敗");
    }
    

接聽功能實(shí)現(xiàn)

首先要添加監(jiān)聽通知

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _phoneLab.text = self.phoneNum;
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleCallStatuschange:) name:@"SIPCallStatusChangedNotification" object:nil];
}

接電話直接調(diào)用pjsua_call_answer方法,改方法中需要將接聽的ID直接傳進(jìn)去就可以進(jìn)行通話

- (void)answerCall:(pjsua_call_id)callId
{
    pjsua_call_answer(callId, 200, NULL, NULL);
}

如果掛斷,可以直接調(diào)用pjsua_call_hangup_all()方法就可以了。

    //獲賬戶信息
    pjsua_call_info config;
    pjsua_call_get_info(_acc_id, &config);
    
    pjsua_call_hangup_all();
    

Demo:https://github.com/cuilinhao/Voip_Project.git

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容