最近做项目有用到在PJSIP客户端接收手机通过过程中发送过来的DTMF按键信息,刚开始查了一点资料发现pjsip有相应的方法,在pjsua_config cfg; 这个结构体的通过设置此回调函数cfg.cb.on_call_state貌似就可以实现 ,之后就开始实践了。用pjsip客户端+freeswitch+x-lite进行试验,试验相当顺利,心情那个激动啊。但是没激动多久,当我放到正式的运营环境用我的pjsip客户端+IMS运营商+真实手机进行测试时彻底傻眼了,根本检测不出来手机的按键信息,查了资料才发现手机发送的DTMF信息应该是属于in-band DTMF no rfc2833 ,悲催的是PJSIP官方并没有支持这中DTMF格式。
还好没放弃经过一个多月的瞎折腾总算弄出点结果出来了。
PJSIP官网的FAQ有对in-band DTMF no rfc2833问题解释,我就不扯淡了,直接给链接 http://trac.pjsip.org/repos/wiki/FAQ#tone-detect 。解释到是挺简单的,看起来很容易,但是对于我这中新手,我就只能呵呵了。查了快一个月的资料到是发现三四种方法,一一去试,没一个成功的,心情郁闷啊。最后还是厚着脸皮,凭着自己一手烂英语邮件去问外国友人。还好,外国友人都是相当热情的,等了一天就有人回复了,在这里要感谢 Eize Slange 这位大哥,不仅很快就给出了回复而且也给了相应的实现代码,非常感谢啊。
参照着给出的代码来进行实现果然有效,高兴啊!!!
我就简单介绍一下他给出的示例代码吧。
要取得in-band DTMF 按键信息就要检测sip客户端接收到的RTP流数据,并对RTP数据流使用Spandsp的库分析其中的DTMF按键信息,然后就大功告成了。
代码流程如下:
1.pjsip客户端层
在on_call_media_state 的回调函数中修改代码如下:
Note: pCallData / callData is some own class holding some info per call... callback: on_call_media_state: ... /* Handle media status */ switch (call_info.media_status) { case PJSUA_CALL_MEDIA_ACTIVE: ... pj_status_t result = PJ_SUCCESS; pjmedia_port* media_port = NULL; unsigned slot_port = 0; pjsua_dtmfdet_config dtmf_cfg; // Only attach once... if ( NULL == callData->dtmfData.media_port ) { dtmf_cfg.call_id = call_id; dtmf_cfg.filter_dialtone = PJ_FALSE; dtmf_cfg.twist = 8; // Twist is set to 8 dB by default. dtmf_cfg.reverse_twist = 4; // Reverse twist is set to 4 dB by default. This value can be safely increased up to 6 or 7 without a significant increase in talk-off to allow DTMFs that exceed this threshold to be detected. dtmf_cfg.threshold =-42; // Threshold is set to -42 dBm0 by default. dtmf_cfg.cb.on_dtmf_digit = &on_ib_dtmf_callback; log_writer(SLL_EVENT, "StackDll:on_call_media_state: Attach INBAND DTMF detection"); result = pjsua_attach_inband_dtmf_detector( call_id, app_config.media_cfg.clock_rate, app_config.media_cfg.channel_count, 160, // samples per frame 16, // bits per sample &dtmf_cfg, PJ_FALSE, // because this called from pjsua-callback &media_port, &slot_port ); if ( PJ_SUCCESS == result ) { callData->dtmfData.media_port = media_port; callData->dtmfData.slot_port = slot_port; } ... callback: on_stream_destroyed:: ... // Release/reset Inband DTMF pCallData = CallGetCallData(call_id); if ( NULL != pCallData ) { if ( NULL != pCallData->dtmfData.media_port ) { log_writer(SLL_LEVEL_2, "StackDll:on_stream_destroyed> Detach INBAND detector..."); pjsua_detach_inband_dtmf_detector( pCallData->dtmfData.media_port, pCallData->dtmfData.slot_port, PJ_FALSE); // because called from callback } pCallData->dtmfData.detectedOutbound = PJ_FALSE; pCallData->dtmfData.lastDigit = -1; pCallData->dtmfData.media_port = NULL; pCallData->dtmfData.slot_port = 0; } ... static void on_ib_dtmf_callback(pjsua_call_id call_id, int dtmf) { // Your callback code when inband DTMF digit has been detected. // Note: this is the same callback prototype as the normal DMTF callback of PJSIP, // but to distinguish between RFC2833 DTMF and this inband a dedicated // callback can be created. }
2.在pjsua.h新增如下:pjsua-lib\pjsua.h ----------------- ... /** * Functionalities around IN-BAND DTMF detection. */ typedef struct pjsua_dtmfdet_callback { void (*on_dtmf_digit)(pjsua_call_id call_id, int digit); } pjsua_dtmfdet_callback; typedef struct pjsua_dtmfdet_config { pjsua_call_id call_id; pj_bool_t filter_dialtone; int twist; // dB int reverse_twist; // dB int threshold; // dBm0 pjsua_dtmfdet_callback cb; } pjsua_dtmfdet_config; PJ_DECL(pj_status_t) pjsua_attach_inband_dtmf_detector ( pjsua_call_id call_id, unsigned clock_rate, unsigned channel_count, unsigned samples_per_frame, unsigned bits_per_sample, pjsua_dtmfdet_config* config, pj_bool_t needs_lock, pjmedia_port **media_port, /* OUT */ unsigned *slot_port /* OUT */ ); PJ_DEF(pj_status_t) pjsua_detach_inband_dtmf_detector ( pjmedia_port *media_port, unsigned slot_port, pj_bool_t needs_lock );
3.在pjsua_media.c中新增如下:
pjsua-lib\pjsua_media.c ----------------------- * Inband DTMF detection. */ PJ_DEF(pj_status_t) pjsua_attach_inband_dtmf_detector ( pjsua_call_id call_id, unsigned clock_rate, unsigned channel_count, unsigned samples_per_frame, unsigned bits_per_sample, pjsua_dtmfdet_config* config, pj_bool_t needs_lock, pjmedia_port **media_port, /* OUT */ unsigned *slot_port /* OUT */ ) { unsigned slot; pj_pool_t *pool; pjmedia_port *port; pj_status_t status; const pj_str_t name = pj_str("dtmfdet"); if ( needs_lock ) PJSUA_LOCK(); pool = pjsua_pool_create("dtmfdet", 1000, 1000); if (!pool) { if ( needs_lock ) PJSUA_UNLOCK(); return PJ_ENOMEM; } status = pjmedia_dtmfdet_port_create( pool, clock_rate, channel_count, samples_per_frame, bits_per_sample, config, &port); if (status != PJ_SUCCESS) { if ( needs_lock ) PJSUA_UNLOCK(); pj_pool_release(pool); return status; } status = pjmedia_conf_add_port(pjsua_var.mconf, pool, port, &name, &slot); if (status != PJ_SUCCESS) { pjmedia_port_destroy(port); if ( needs_lock ) PJSUA_UNLOCK(); pj_pool_release(pool); return status; } status = pjsua_conf_connect( pjsua_call_get_conf_port(call_id), slot ); if (status != PJ_SUCCESS) { pjmedia_conf_remove_port(pjsua_var.mconf, slot); pjmedia_port_destroy(port); if ( needs_lock ) PJSUA_UNLOCK(); pj_pool_release(pool); return status; } // Finalizing if (media_port)*media_port = port; if (slot_port) *slot_port = slot; if ( needs_lock ) PJSUA_UNLOCK(); return PJ_SUCCESS; } PJ_DEF(pj_status_t) pjsua_detach_inband_dtmf_detector ( pjmedia_port *media_port, unsigned slot_port, pj_bool_t needs_lock ) { if ( needs_lock ) PJSUA_LOCK(); pjmedia_conf_remove_port(pjsua_var.mconf, slot_port); pjmedia_port_destroy(media_port); if ( needs_lock ) PJSUA_UNLOCK(); return PJ_SUCCESS; }
4.在pjmedia库中新增文件如下:
And the new plugin, located in pjmedia/src I named it ib_dtmfdet_port.c. Here you interface towards SpanDSP, but I moved all required functions into a new file -------------------------------------------------------------------------------------- #include "pjmedia/ib_dtmfdet_port.h" //#include "pjmedia/dtmf_spandsp.h" //这个对我没用 #include
#include #include //下面两行新增spandsp的头文件包含 #include #include #define THIS_FILE "ib_dtmfdet_port.c" //#define SIGNATURE PJMEDIA_PORT_SIGNATURE('D','T','M','F') //这个编译不同过改为下面一行
#define SIGNATURE PJMEDIA_SIG_PORT_CB
struct dtmfdet_port{ pjmedia_port base; pjsua_dtmfdet_config cfg;// dtmf_rx_state_t state; //这个参数设置为这样的话编译一直不通过,好像是因为只定义了一个类型?不太清楚,不过换成下面一行就行了};static void dtmfdet_digits_rx (void *user_data, const char *digits, int len);static pj_status_t dtmfdet_put_frame (pjmedia_port *this_port, const pjmedia_frame *frame);static pj_status_t dtmfdet_get_frame (pjmedia_port *this_port, pjmedia_frame *frame);static pj_status_t dtmfdet_destroy (pjmedia_port *this_port);PJ_DEF(pj_status_t) pjmedia_dtmfdet_port_create( pj_pool_t *pool, unsigned clock_rate, unsigned channel_count, unsigned samples_per_frame, unsigned bits_per_sample, pjsua_dtmfdet_config* cfg, pjmedia_port **p_port){ const pj_str_t name = pj_str("dtmfdet"); struct dtmfdet_port *dport; PJ_ASSERT_RETURN(pool && clock_rate && p_port, PJ_EINVAL); PJ_ASSERT_RETURN(clock_rate == 8000, PJ_EINVAL); PJ_ASSERT_RETURN(bits_per_sample == 16, PJ_EINVAL); // Create and initialize port dport = PJ_POOL_ZALLOC_T(pool, struct dtmfdet_port); PJ_ASSERT_RETURN(dport != NULL, PJ_ENOMEM); //下面的都换掉 //pjmedia_port_info_init(&dport->base.info, &name, SIGNATURE, clock_rate, // channel_count, bits_per_sample, samples_per_frame); //dtmf_rx_init(&dport->state, dtmfdet_digits_rx, dport); // Configure port //dport->cfg = *cfg; //dtmf_rx_parms(&dport->state, dport->cfg.filter_dialtone, dport->cfg.twist, dport->cfg.reverse_twist, dport->cfg.threshold);
dtmf_rx_state_t *state;
dport->state = dtmf_rx_init(dport->state, dtmfdet_digits_rx, dport); //Configure port dport->cfg = *cfg; dtmf_rx_parms(dport->state, dport->cfg.filter_dialtone, dport->cfg.twist, dport->cfg.reverse_twist, dport->cfg.threshold); // Media port interface dport->base.put_frame = &dtmfdet_put_frame; dport->base.get_frame = &dtmfdet_get_frame; dport->base.on_destroy = &dtmfdet_destroy; *p_port = &dport->base; PJ_LOG(3, (THIS_FILE, "Inband DTMF detector '%.*s' created: filter_dialtone=%s, twist=%d, reverse_twist=%d, threshold=%d", (int) dport->base.info.name.slen, dport->base.info.name.ptr, dport->cfg.filter_dialtone ? "yes" : "no", dport->cfg.twist, dport->cfg.reverse_twist, dport->cfg.threshold )); return PJ_SUCCESS; } static void dtmfdet_digits_rx(void *user_data, const char *digits, int len) { struct dtmfdet_port *dport = (struct dtmfdet_port*) user_data; int i; PJ_LOG(3, (THIS_FILE, "Inband digit(s) received: '%.*s'", len, digits)); if (dport->cfg.cb.on_dtmf_digit != NULL) { for (i = 0; i < len; i++) { dport->cfg.cb.on_dtmf_digit(dport->cfg.call_id, digits[i]); } } } static pj_status_t dtmfdet_put_frame(pjmedia_port *this_port, const pjmedia_frame *frame) { struct dtmfdet_port *dport = (struct dtmfdet_port*) this_port; if (frame->type == PJMEDIA_FRAME_TYPE_AUDIO) {
// dtmf_rx(&dport->state, (const pj_int16_t*) frame->buf, dport->base.info.samples_per_frame); //换掉
dtmf_rx(dport->state, (const pj_int16_t*) frame->buf, 160/*dport->base.info.samples_per_frame*/); //最后一个参数我也不清楚,现在写这个应该没问题 } return PJ_SUCCESS; } static pj_status_t dtmfdet_get_frame(pjmedia_port *this_port, pjmedia_frame *frame) { PJ_UNUSED_ARG(this_port); PJ_UNUSED_ARG(frame); return PJ_EINVALIDOP; } static pj_status_t dtmfdet_destroy(pjmedia_port *this_port) { PJ_UNUSED_ARG(this_port); return PJ_SUCCESS; }
相应的.h文件我字节写的不保证准确性#ifndef __IB_DTMFDET_PORT_H__ #define __IB_DTMFDET_PORT_H__ //#include "pjsua-lib/pjsua.h" #include "pjsua-lib/pjsua.h" #include
PJ_BEGIN_DECL PJ_DEF(pj_status_t) pjmedia_dtmfdet_port_create ( pj_pool_t *pool, unsigned clock_rate, unsigned channel_count, unsigned samples_per_frame, unsigned bits_per_sample, pjsua_dtmfdet_config* cfg, pjmedia_port **p_port ); PJ_END_DECL #endif /* __IB_DTMFDET_PORT_H__ */
最后要记得连接Spandsp动态库,网上这个东西貌似有点难找,可以直接下载Freeswitch源码,里面有spandsp源码和动态库。这样修改完的pjsip 库就能检测in-band DTMF 信息了。还有可以在PJSIP的FAQ找到如何发送in-band DTMF按键的示例代码,这样就可以自己模拟了,不用非得正式的环境了,不过不确保能百分之百识别哦,毕竟是模拟的DTMF按键。http://trac.pjsip.org/repos/wiki/FAQ#inband-dtmf 这个是pjsip发送DTMF的链接地址。
忙前忙后忙了一个多月就这样一个小小的功能实在有点汗颜啊。下面还有我试过的其他方法,不过我都没试出结果列一下等以后看看吧A.参考网址如下: http://article.gmane.org/gmane.comp.voip.pjsip/19289/match=pjsip+inband+dtmf 这位外国友人貌似也实现了inband DTMF的检测,但是我试下来一直在调用我的回调函数最终溢出,谁有兴趣可以在试试看。B.我在查资料的过程中又发现貌似pjsua_media_config media_config; 中设置的media_config.on_aud_prev_play_frame = &play_frame;回调函数可以获取playback方向的frame不过我用Spandsp进行分析时却无法分析出DTMF信息,不知道什么原因。C. 参考网址如下: http://comments.gmane.org/gmane.comp.voip.pjsip/9276 一开始就是试的这个方法,但是我感觉作者应该用 pjmedia_mem_player_create 这套函数来获取playback方向的frame,然而我也没试成功我感觉我应该是在pjmedia_mem_player_create 函数中的buff设置时出现的问题,因为我的回调函数一直在被调用。谁能弄出来麻烦告知一下,我快被他折磨死了。
就这样吧,水平有限多担待点吧,哈哈!