前言
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頭部格式

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)用邏輯

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打開了。

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)建的命令。

下面報(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
使用工具查看,如下圖

添加完用戶后需要開啟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é)果。

回調(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();