iOS 实现Voip网络电话

前言

Voip即网络电话,voice over internet Protocol,将模拟的声音讯号经过压缩与封包之后,以数据封包的形式在IP网络进行语音讯号的传输,通俗来说就是互联网电话或IP电话。Voip网络电话,中文就是“通过Ip数据包发送实现的语音业务”,它使你可以通过互联网免费或是资源很低的传送语音,传真,视频和数据等业务。

基本原理

  • Voip的基本原理是通过语音的压缩算法对语音数据编码进行压缩处理,然后把这些语音数据按TCP/IP标准进行打包,经过IP网络把数据包送至接收地,再把这些语音数据包串起来,经过解压处理后,恢复成原来的语音信号,从而达到由互联网传送语音的目的。

  • Voip电话的核心与关键设备是Voip网关。

  • 在整个IP电话系统中,网关设立在世界上各个地区,完成当地电话网(PSTN)与Internet的接入与转换处理等功能。终端设备将模拟语音信号传到Voip网关,网关接收到了标准电话信号以后,经过数据自,编码,压缩处理,按IP协议打包到Internet上,根据传输路由,通过Internet发送到对应网关,对端的网关接收到了Internet传来的IP包,经过解压处理后还原成模拟语音信号再转到电话网系统(PSTN)

  • 网关因生产厂商不同而有所不同,但基本的组成模块是一样的,包括数据处理主机,语音模块,数据处理模块,数据接续模块和管理软件模块等。

  • 网关具有路由管理功能,它把各地区电话区号映射为相应地区网关的IP地址,这些信息存放在一个数据库中。数据接续处理软件将完成呼叫处理,数字语音打包,路由管理等功能。在用户拨打VOIP电话时,网关根据电话区号数据库资料,确定相应网关的IP地址,并将此IP地址加入IP数据包中,同时选择最佳路由,以减少传输延时,IP数据包经Internet到达目的地的网关。

常用的Voip协议(Control Protocol)

H.323

  • H.323是一种ITU-T标准,最初用于局域网上的多媒体会议,后来扩展至覆盖Voip。该标准既包括了点对点通信也包括了多点会议。H.232定义了四种逻辑组成部分:终端,网关,网守以及多点控制单元。
  • 终端,网关和MCU均视为终端点。
  • 会话发起协议(SIP) 是简历Voip链接的IETF标准。SIP是一种应用层控制协议,用于和一个或多个参与者创建,修改和终止会话。SIP的结构与HTTP(客户端-服务器协议)相似。客户机发出请求,并发送给服务器,服务器处理这些请求给客户机发送一个响应,该请求与响应行程一次事务。
  • 媒体网关控制协议MGCP 定义了呼叫控制单元与电话网关之间的通信服务。
  • 媒体网关控制协议是IETF和IU-T共同努力的结果。是一种用于控制物理上分开的多媒体网关的协议单元的协议,从而可以从媒体转化中分离呼叫控制。

SIP协议

  • Voip一般通过SIP作为应用层的信令控制协议【session intiation protocol】, SIP通过创建会话,断开会话从而实现会话的管理。SIP可以创建多个会话,多个会话可以同时工作,对多会话进行管理从而实现多会话的功能。SIP在传输层可以采用UDP协议也可以采用TCP协议,但是对于对讲的特点来说大部分采用的是UDP协议,UDP协议简单并且更加符合对讲的即时性的特点,但是在网络不好情况下会产生丢包。

  • SIP会话建立之后RTCP和RTP分别负责会话上媒体传输的控制和媒体传输。RTP上传输的媒体的编码方式为会话建立时客户端和服务器之间协商的编码方式,RTP协议和RTP控制协议RTCP一起使用,而且它是建立在用户数据协议UDP上的。

关键技术

VOIP网络电话的关键技术包括:信令技术,编码技术,实时传输技术,服务质量保证技术,以及网络传输技术等

信令技术

  • 信令技术保证电话呼叫的顺利实现和语音质量,被广泛接受的Voip控制信令体系包括ITUT的H.323系列和IETF的会话初始化协议SIP

  • ITU的H.232协议定义了在无业务质量保证的因特网或其他分组网上多媒体通信的协议以及其规程。H.323标准是局域网,广域网和internet上的多媒体提供技术基础保障

  • SIP是一种应用层协议,可以用UDP或TCP作为传输协议。与H.323不同的是:SIP是一种基于文本的协议,用SIP规则资源定位语言描述(SIP Uniform Resource Locators),这样易于实现和调试,更重要的是灵活性和扩展性好。由于SIP仅作用于初始化呼叫,而不是传输媒体数据,因而造成的附加传输代价也不大。与H.323相比,SIP还有建立呼叫快,支持传送电话号码的特点。

编码技术

  • 语音压缩编码技术是voip网络电话技术的一个重要组成部分。主要的编码技术有ITU-T定义的G.729 G.723等,其中G.729 可将经过采样的64kbit/s 话音以几乎不失真的质量压缩至8kbit/s。由于在分组交换网络中,业务质量不能得到很好保证,因而需要话音的编码具有一定的灵活性,即编码速率、编码尺度的可变可适应性。G723.1 采用5.3/6.3K bit/s 双速率话音编码,其话音质量好,但是处理时延较大,它是已标准化的最低速率的话音编码算法。

实时传输技术

  • 实时传输技术主要是采用RTP实时传输协议。RTP是提供端到端的包括音频在内的实时数据传送的协议,RTP标准定义了两个子协议,RTP和RTCP

  • 数据传输协议RTP,用于实时传输数据。改协议提供的信息包括:时间戳(用于同步),序列号(用于丢包和重排序检测),以及负载格式(用于说明数据的编码格式)

  • 控制协议RTCP,用于服务质量反馈和同步流媒体。相对于RTP来说,RTCP所占的带宽非常小,通常只有5%

RTP
  • RTP数据协议负责对流媒体数据进行封包并实现媒体流的实时传输,每一个RTP数据报都由头部Header和负载Payload两个部分组成,其中头部前12个字节的含义是固定的,而负载则可以是音频或视频数据

  • RTP头部格式

iOS 实现Voip网络电话_第1张图片
image.png

V:RTP协议的版本号, 占2位,当前协议版本号为2

P:填充标志, 占1位,如果P = 1, 则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分

X:扩展标志,占1位,如果X =1, 则在RTP报头后跟有一个扩展报头

CC:CSRC计数器,占4位,指示CSRC表示符的个数

M:标记,占1位,不同的有效载荷有不同的含义,对于食品,标记一帧的结束,对于音频,标记会话的开始

PT:有效荷载类型,占7位, 用于说明RTP报文中有效载荷的类型,如GSM音频,JPEM图像等,在流媒体中大部分是用来区分音频流和视频流的,这样便于客户端进行解析。

序列号:占16位,用于标识发送者所发送的RTP报文的序列号,每次发送一个报文,序列号加1。 这个字段当下层的承载协议用UDP的时候,网络状态不好的时候可以用来检查丢包,同时出现网络抖动的情况可以用来对数据进行重新排序,序列号的初始值是随机的,同时音频包和视频包的sequence是分别记数的。

时戳(Timestamp):占32位,必须使用90kHZ时钟频率。时戳反应了该RTP报文的第一个八位组的采样时刻。接受者使用时戳来极端延迟和延迟抖动,并进行同步控制。

同步信源(SSRC)标识符:占32位,用于表示同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC

特约信源(CSRC)标识符:每个CSRC标识符占32位,可以有0-15个,每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源

网络传输技术

  • Voip网络电话中网络传输技术主要是TCP和UDP,此外还包括网关互联技术,路由选择技术,网络管理技术以及安全认证和计费技术等

服务质量技术

Voip网络电话中主要采用资源预留协议RSVP 以及进行服务质量监控的实时传输控制协议RTCP来避免网络拥塞,保障通话质量

小型Voip电话系统的建立

一般而言,部署Voip系统时涉及三个主要组成部分:IPPBX,VOIP电话和Voip运营商网络。我们总是通过Voip网络和传统的PSTN电话系统进行互联。当然,我们也可以通过部署Voip网关(VoIP Gateway)直接连接传统电话网络,而不需要单独的voip服务商。
本文主要采用miniSIPService搭建Voip服务系统。MSS是专业的纯软件方式的PBX,能运行在Windows以及Linux等系统。它能支持我们目前需要的各种特性。

在iOS上开发Voip应用

目前基于SIP协议的Voip是应用最广泛的,iOS上的Voip应用通常使用开源协议栈进行开发,成熟的开源SIP协议栈有PJSIP和Linphone,本文将通过PJSIP来实现一个简单的语音通话App

PJSIP开源库的组成部分

PJSIP -Open Source SIP stack[开源的SIP协议栈]
PJMEDIA - Open Source Media Stack[开源的媒体栈]
PJNATH - Open Source NAT Traversal Helper Library[开源的NAT-T辅助库]
PJLIB-UTIL - Auxiliary Library[辅助工具库]
PJLIB - Ultra Portable Base Framework Library[基础框架库]

PJSUA API使用以及说明
这里使用的API基本都是来自pjsua,这个是建立在pjsip基础上的一层纯C封装,下面展示了pjsip初始化到拨打电话和挂断电话的Api调用逻辑

iOS 实现Voip网络电话_第2张图片
image.png

pjsua接口使用时,需要创建,初始化,开始和销毁的操作:pjsua_create,pjsua_init,pjsua_start,pjsua_destory

pjsua_transport_create创建sip信令发送和接受需要的相关socket等资源

pjsua_acc_add添加拨打电话账号,账号类似于我们的手机号码,可以起到定位的功能。

拨打电话的挂断电话:pjsua_call_make_call,pjsua_call_hangup_all

Pjsip只是完成两个功能

1.使用sip信令协商双方使用音频,视频通话使用的rtp,rtcp的socket端口,视频编码器和音频编码器的类型和相关的编码参数,使用的网络类型。

2.完成音频,视频通话的socket通道,传出音频和视频数据

服务器的安装

1.安装MySQL

去官方下载最新版本的MySQL,本文使用最新的MySQL Community Server 5.6.25 GA版,Mac用户方便起见可以使用dmg格式的。由于OS X的路径问题,后续启动Kamailio时会发生找不到库的情况,所以需要创建库的软链接,即安装完毕后终端执行命令:

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

最后我们就可以在系统偏好设置里将MySQL Server打开了。


iOS 实现Voip网络电话_第3张图片
image.png
2.源码下载
  # git clone --branch 4.2 --single-branch \

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

  # cd kamailio
  

我是直接根据链接下载的,下载之后直接解压,链接地址

3.编译安装

在执行完步骤2的基础上,首先添加MySQL的环境变量:

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

然后执行下列命令进行编译与安装

# make include_modules=db_mysql cfg

# sudo make all

# sudo make install

make all的时候如果不使用sudo提权的话,可能就失败了。虽然sudo不是好的做法,但是方便起见这里直接sudo。这边下去会出现的一个问题是mysql的module编译失败。提示“mysql/mysql.h”找不到。这时候要去kamailio-4.3.0/modules/db_mysql下,把源码头文件引用都改成

这时候你应该可以完成make all了。接下去install,这一步也不会出问题了。

4.Kamailio的配置

修改kamctlrc文本,执行

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

去掉SIP_DOMAIN前的#符号,改成自己的服务器地址。
我的是SIP_DOMAIN=127.0.0.1。即本机IP。
然后去掉 DBENGINE=MYSQL前的注释语句,选定mysql数据库。

然后再修改kamailio.cfg文本,执行

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

在文本的开头加上一行:


#!defineWITH_MYSQL

5.创建数据库,并开启服务器

执行命令来创建数据库

# /usr/local/sbin/kamdbctl create

如果提示输入密码,此时注意密码是默认在kamdbctl的配置文件中的,通过下面命令可以查看

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

你可以看到服务器的一些设置,如果密码自己设置错误,或者不知,则可以将下面几行打开并进行重新执行数据库创建的命令。


iOS 实现Voip网络电话_第4张图片
image.png

下面报错:


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

解决的办法:


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

创建完数据库后需要添加用户

我们这里添加三个用户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

使用工具查看,如下图


iOS 实现Voip网络电话_第5张图片
image.png

添加完用户后需要开启SIP Server:


cuilinhaodeMacBook-Pro:sbin cuilinhao$ ./kamctl start

\E[37;33mINFO: Starting Kamailio :

如果这一步出问题注意检查步骤1中的库的软链接是否建立。除了软连接的问题,其实更重要的一个问题是会提示


\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/

这里有一个权限问题,而并非是某个模块没有编译。使用


kamailio -M 8 -E -e -dd

可以查看具体失败的信息,也可以去系统日志看。具体的错误原因是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下新建一个归属于当前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的权限等级

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
 

则服务器开启成功。

现在自己电脑启动服务器


  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中调用,主要是对通话进行配置,来电回调设置以及媒体先关的配置
代码如下:

///初始化通话配置
        pjsua_config config;
        pjsua_config_default (&config);
        //登录状态改变回调
        //config.cb.on_reg_state2 = &on_reg_state2;
        config.cb.on_reg_state = &on_reg_state;
        
        
        //来电回调
        config.cb.on_incoming_call = &on_incoming_call;
        //媒体状态回调(通话建立后,要播放RTP流)
        config.cb.on_call_media_state = &on_call_media_state;
        //设置通话状态改变回调
        config.cb.on_call_state = &on_call_state;
        
        //媒体相关配置
        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(@"创初始化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传输失败---");
            }
        }
        

配置成功之后可以看到下面打印结果。


image.png

回调函数配置


///登录状态改变回调
static void on_reg_state2(pjsua_acc_id acc_id, pjsua_reg_info *info)
{
   
    
    if (info->renew != 0) {
        //--info里面是一个结构体 cbparm也是一个结构体, 然后cbparm中包含code属性,这是一个c语言的写法
        if (info->cbparam->code == 200) {
            NSLog(@"登录成功");
        }
        else{
            NSLog(@"登录失败code:%d ",info->cbparam->code);
        }
    }
    else{
        if (info->cbparam->code == 200)
        {
            NSLog(@"SIP退出登录成功");
        }
    }
}

///来电回调
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];
    //这里发送一个通知
    [[NSNotificationCenter defaultCenter] postNotificationName:@"calling" object:nil userInfo:@{@"calledCAcount":callStr}];
    NSLog(@"%@",callStr);
}

///呼叫回调
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(@"呼叫成功,等待对方接听");
    }
}

//通话状态改变回调
static void on_call_state(pjsua_call_id call_id, pjsip_event *e)
{
    
    // 通话状态:CALLING
    // 通话状态:EARLY
    // 通话状态:EARLY
    // 呼叫成功,等待对方接听
    // 通话状态:CONNECTING
    // 通话状态:CONFIRMED
    // DISCONNCTD  对方挂断
    //获取通话信息
    pjsua_call_info ci;
    pjsua_call_get_info(call_id, &ci);
    
    NSString *status = [NSString stringWithUTF8String:ci.state_text.ptr];
    NSLog(@"通话状态:%@",status);
    
}


登录界面实现

首先要监听注册相关通知,在SIP中,注册就是将客户端的相关信息注册到服务器的一个类似路由表一样的列表中,这样其他客户端呼叫时,经过服务器的路由就可以找到正确的客户端地址,从而建立P2P的链接。

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

登录按钮触摸事件中,配置并完成对服务器的注册:


//配置账号信息
    pjsua_acc_config config;
    pjsua_acc_config_default(&config);
    
    char accountChar[50];
    strcpy(accountChar, [accountsString UTF8String]);
    char passwordChar[50];
    strcpy(passwordChar, [passwordString UTF8String]);
    
    //设置账号格式:  sip:账号@服务地址
    char sipAccount[50];
    sprintf(sipAccount, "sip:%s@%s", accountChar, [ipString UTF8String]);
    config.id = pj_str(sipAccount);
    
    //设置服务器格式: sip:服务器地址
    char serviceId[50];
    sprintf(serviceId, "sip:%s",[ipString UTF8String]);
    
    NSLog(@"----服务器地址---%s", [ipString UTF8String]);
    
    config.reg_uri = pj_str(serviceId);
    
    //不设置超时时间
    config.reg_retry_interval = 0;
    
    //注册账号个数 最多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);
    //数据类型
    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;
    }
    
  • 在设置中config.reg_retry_interval应设置为0,这个是注册失败时,进行重试的时间间隔,如果不设置为0则它会不断进行尝试注册(用户名密码验证失败也会如此),这里为了不做复杂的类似超市这样的处理,直接将它设为0
  • 如果登录成功之后就会进行回调
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(@"----->>>登录回调成功");
    
    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;
    

通过通知带回来的信息,来判断是否登录成功,如果登录成功则直接跳转到拨打电话界面。

拨打电话实现

主要是调用pjsua_call_make_call方法,根据用户名进行呼叫,呼叫分一下几个状态:

  • PJSIP_INV_STATE_DISCONNECTED 呼叫状态

  • PJSIP_INV_STATE_CALLING 呼叫中 状态

  • PJSIP_INV_STATE_CONNECTING 正在连接状态

  • PJSIP_INV_STATE_CONFIRMED 挂断状态

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(@"呼叫失败");
    }
    

接听功能实现

首先要添加监听通知

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

接电话直接调用pjsua_call_answer方法,改方法中需要将接听的ID直接传进去就可以进行通话

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

如果挂断,可以直接调用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

你可能感兴趣的:(iOS 实现Voip网络电话)