背景:
业务需要在 android 设备上添加支持通过网关拨打客户电话,并根据客户按键反馈执行相应的操作
平台:
RK3399 + Android 7.1 + pjproject-2.4
步骤:
对 sip 这块小白一枚,接到任务后做了如下操作:
1 了解 pjsip 、DTMF,并编译出所需要的库
这里参考了:《 https://www.cnblogs.com/lijingcheng/p/4454928.html DTMF三种模式(SIPINFO,RFC2833,INBAND)》介绍,了解了什么是 DTMF,内容如下:
1、DTMF(双音多频)定义:由高频音和低频音的两个正弦波合成表示数字按键(0~9 * # A B C D)。
2、SIP中检测DTMF数据的方法:SIPINFO、RFC2833、INBAND
1)SIPINFO
为带外检测方式,通过SIP信令通道传输DTMF数据。没有统一的实现标准,目前以Cisco SIPINFO为标准,通过SIPINFO包中的signal字段识别DTMF按键。注意当DTMF为“*”时不同的标准实现对应的signal=*或signal=10。SIPINFO的好处就是不影响RTP数据包的传输,但可能会造成不同步。
2)RFC2833
为带内检测方式,通过RTP传输,由特殊的rtpPayloadType即TeleponeEvent来标示RFC2833数据包。同一个DTMF按键通常会对应多个RTP包,这些RTP数据包的时间戳均相同,此可以作为识别同一个按键的判断依据,最后一包RTP数据包的end标志置1表示DTMF数据结束。另外,很多SIP UA 包括IAD都提供TeleponeEvent的设置功能如3CX Phone,Billion-IAD,ZTE-IAD等默认的TeleponeEvent都为101,但可以人为修改,这时要求在进行RFC2833 DTMF检测之前需事先获取SDP协商的TeleponeEvent参数。
3)INBAND
为带内检测方式,而且与普通的RTP语音包混在一起传送。在进行INBAND DTMF检测时唯一的办法就是提取RTP数据包进行频谱分析,经过频谱分析得到高频和低频的频率,然后查表得到对应的按键,进行频谱分析的算法一般为Goertzel,这种算法的实现也很简单,网上有很多可以下到,但建议采用定点算法,浮点算法效率很低。
在选择压缩比很高码率很低的codec,比如G.723.1和G.729A等,建议不要使用INBAND模式,因为INBAND DTMF数据在进行复杂编解码后会产生失真,造成DTMF检测发生偏差或失败。
另外,还特别需要注意的一点就是很多SIP UA中INBAND都是伴随着RFC2833和SIPINFO同时发生的,这时需要区别对待,最好选择RFC2833和SIPINFO
2 抓sip通话过程中手机按键的数据包
通过 Wireshark 分析,在sip通过过程中,对方按键时,网关回传给 Android 设备的 DTMF 属于 RFC2833 的模式,截图如下:
3 pjsip 是否支持DTMF?
知道了当前网关是以 RFC2833 方式回传用户按键信息了,接下来就是一波网络检索的操作,要先确认下 pjsip 是否支持 DTMF?
找到一篇博客《 https://www.twblogs.net/a/5b8076cb2b71772165a7c25b PJSIP 檢測通話過程中對方手機發送過來的in-bnad DTMF(no rfc2833)按鍵信息》了解到 pjsip 不支持 in-band DTMF no rfc2833 这种 DTMF,然后看了下该文档中记录的操作,大概意思就是:检测 SIP 客户端接收到的数据流 ——> 用 Spandsp的库分析该数据流获取 DIMF 数据 ——> 添加回调得到按键信息
奈何我对 sip 的了解应该还没有该博主多,看了下添加的 code 也较繁琐,就没有去实验了。但是其实现的大体思路和我一开始想的一样,若是 pjsip 不支持的话,就直接对数据流进行分析
4 开始寻找数据流的旅程
了解到 pjsip 不支持 DTMF 情况,暂敲定的方案是找到 rtp 获取数据流的函数,看下是否容易进行分析。
在这里为了方便实验看数据,我在 ./pjlib/include/pj/log.h 添加了如下code(要同步修改编译文件,加上 -llog 库),方便自己打 log:
#include <android/log.h>
#define SEEINER_TAG "twz_sip"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,SEEINER_TAG,__VA_ARGS__)
//#define LOGE(...)
折腾到快要下班的时间,终于截取到了 DTMF 数据流,用Android设备通过网关打自己手机,Android设备log同步打印出我手机的拨号按键信息,中间的寻找过程就不做叙述了,直接上结果:
1 transport_udp.c 中 on_rx_rtp 函数能获取每一帧的 RTP 数据
2 on_rx_rtp 会将取到的数据丢给 ioqueue_common_abs.c 文件中的 pj_ioqueue_recvfrom 函数进行分析
3 pj_ioqueue_recvfrom 函数将收到的数据进一步丢给 sock_bsd.c 文件中的 pj_sock_recvfrom 函数进行分析
4 在 pj_sock_recvfrom 函数中调用 recvfrom函数进一步对数据进行处理,这里打印 recvfrom 返回值 len 发现,每当我手机拨号时就会收到几帧 len = 16 的字节数据,对比之前抓包的 RTP EVENT 数据,怀疑这 16 字节包含按键信息,后经过打 log 发现,其第 13 个字节即为拨号键,第14字节标志点击某个键结束(这里第14 字节对我来说有两个作用:1 判断按了某个键,且用户已按完该键了 2 可以分析用户连续按了某个键次数)
即以上4步RTP 数据流通过程:
on_rx_rtp ——> pj_ioqueue_recvfrom ——> pj_sock_recvfrom ——> recvfrom
5 在 RTP 流中找到了按键数据后,因为对 pjsip 不够了解,且为了最小化对 sip 业务流程影响,我直接在 pj_sock_recvfrom 函数里添加了回调函数,并在 sock_bsd.c 中实现了 注册该回调的函数
6 在 pjsua2_wrap.cpp jni 文件实现回调函数,再写个 jni 函数回调 java 函数将每次的按键数据给实时的上报上去
至此,Android设备端就可以通过 pjsip 实时获取通话过程中用户的按键信息了