FreeSWITCH添加自定义endpoint之媒体交互

打印 上一主题 下一主题

主题 933|帖子 933|积分 2799

操作系统 :CentOS 7.6_x64FreeSWITCH版本 :1.10.9 之前写过FreeSWITCH添加自定义endpoint的文章:https://www.cnblogs.com/MikeZhang/p/fsAddEndpoint20230528.html今天记录下endpoint媒体交互的过程并提供示例代码及相关资源下载,本文涉及示例代码和资源可从如下渠道获取:关注微信公众号(聊聊博文,文末可扫码)后回复 20230806 获取。一、originate流程 

1、originate命令的使用

originate用于发起呼叫,命令使用的基础模板:
  1. originate ALEG BLEG
复制代码
在fs_cli控制台使用的完整语法如下:
  1. originate <call url> <exten>|&<application_name>(<app_args>) [<dialplan>][<context>] [<cid_name>][<cid_num>] [<timeout_sec>]
复制代码
其中,originate 为命令关键字,为必选字段,用于定义ALEG的呼叫信息,也就是通常说的呼叫字符串,可以通过通道变量定义很多参数;|&()  为必选字段,用于指定BLEG的分机号码或者用于创建BLEG的app(比如echo、bridge等);[][]  可选参数,该参数用于指定dialplan的context,默认值:xml default ;[] 可选参数,该参数用于指定originate超时,默认值:60 ; 这里以分机进行示例呼叫:
  1. originate user/1000 9196 xml default 'user1' 13012345678
复制代码
更多使用方法可参考我之前写的文章:https://www.cnblogs.com/MikeZhang/p/originate20230402.html2、originate功能入口函数

入口函数为originate_function,在 mod_commands_load 中绑定:
  1. SWITCH_ADD_API(commands_api_interface, "originate", "Originate a call", originate_function, ORIGINATE_SYNTAX);
复制代码
具体实现如下:
  1. #define ORIGINATE_SYNTAX "<call url> <exten>|&<application_name>(<app_args>) [<dialplan>] [<context>] [<cid_name>] [<cid_num>] [<timeout_sec>]"
  2. SWITCH_STANDARD_API(originate_function)
  3. {
  4.     switch_channel_t *caller_channel;
  5.     switch_core_session_t *caller_session = NULL;
  6.     char *mycmd = NULL, *argv[10] = { 0 };
  7.     int i = 0, x, argc = 0;
  8.     char *aleg, *exten, *dp, *context, *cid_name, *cid_num;
  9.     uint32_t timeout = 60;
  10.     switch_call_cause_t cause = SWITCH_CAUSE_NORMAL_CLEARING;
  11.     switch_status_t status = SWITCH_STATUS_SUCCESS;
  12.     if (zstr(cmd)) {
  13.         stream->write_function(stream, "-USAGE: %s\n", ORIGINATE_SYNTAX);
  14.         return SWITCH_STATUS_SUCCESS;
  15.     }
  16.     /* log warning if part of ongoing session, as we'll block the session */
  17.     if (session){
  18.         switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_NOTICE, "Originate can take 60 seconds to complete, and blocks the existing session. Do not confuse with a lockup.\n");
  19.     }
  20.     mycmd = strdup(cmd);
  21.     switch_assert(mycmd);
  22.     argc = switch_separate_string(mycmd, ' ', argv, (sizeof(argv) / sizeof(argv[0])));
  23.     if (argc < 2 || argc > 7) {
  24.         stream->write_function(stream, "-USAGE: %s\n", ORIGINATE_SYNTAX);
  25.         goto done;
  26.     }
  27.     for (x = 0; x < argc && argv[x]; x++) {
  28.         if (!strcasecmp(argv[x], "undef")) {
  29.             argv[x] = NULL;
  30.         }
  31.     }
  32.     aleg = argv[i++];
  33.     exten = argv[i++];
  34.     dp = argv[i++];
  35.     context = argv[i++];
  36.     cid_name = argv[i++];
  37.     cid_num = argv[i++];
  38.     switch_assert(exten);
  39.     if (!dp) {
  40.         dp = "XML";
  41.     }
  42.     if (!context) {
  43.         context = "default";
  44.     }
  45.     if (argv[6]) {
  46.         timeout = atoi(argv[6]);
  47.     }
  48.     if (switch_ivr_originate(NULL, &caller_session, &cause, aleg, timeout, NULL, cid_name, cid_num, NULL, NULL, SOF_NONE, NULL, NULL) != SWITCH_STATUS_SUCCESS
  49.         || !caller_session) {
  50.             stream->write_function(stream, "-ERR %s\n", switch_channel_cause2str(cause));
  51.         goto done;
  52.     }
  53.     caller_channel = switch_core_session_get_channel(caller_session);
  54.     if (*exten == '&' && *(exten + 1)) {
  55.         switch_caller_extension_t *extension = NULL;
  56.         char *app_name = switch_core_session_strdup(caller_session, (exten + 1));
  57.         char *arg = NULL, *e;
  58.         if ((e = strchr(app_name, ')'))) {
  59.             *e = '\0';
  60.         }
  61.         if ((arg = strchr(app_name, '('))) {
  62.             *arg++ = '\0';
  63.         }
  64.         if ((extension = switch_caller_extension_new(caller_session, app_name, arg)) == 0) {
  65.             switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_CRIT, "Memory Error!\n");
  66.             abort();
  67.         }
  68.         switch_caller_extension_add_application(caller_session, extension, app_name, arg);
  69.         switch_channel_set_caller_extension(caller_channel, extension);
  70.         switch_channel_set_state(caller_channel, CS_EXECUTE);
  71.     } else {
  72.         switch_ivr_session_transfer(caller_session, exten, dp, context);
  73.     }
  74.     stream->write_function(stream, "+OK %s\n", switch_core_session_get_uuid(caller_session));
  75.     switch_core_session_rwunlock(caller_session);
  76.   done:
  77.     switch_safe_free(mycmd);
  78.     return status;
  79. }
复制代码
调用流程如下:
  1. originate_function
  2.     => switch_ivr_originate
  3.         => switch_core_session_outgoing_channel
  4.             => endpoint_interface->io_routines->outgoing_channel
  5.         => switch_core_session_thread_launch     
复制代码
3、switch_ivr_originate函数

该函数用于发起具体的呼叫。
switch_ivr_originate函数定义:
  1. SWITCH_DECLARE(switch_status_t) switch_ivr_originate(
  2.     switch_core_session_t *session,
  3.     switch_core_session_t **bleg,
  4.     switch_call_cause_t *cause,
  5.     const char *bridgeto,
  6.     uint32_t timelimit_sec,
  7.     const switch_state_handler_table_t *table,
  8.     const char *cid_name_override,
  9.     const char *cid_num_override,
  10.     switch_caller_profile_t *caller_profile_override,
  11.     switch_event_t *ovars, switch_originate_flag_t flags,
  12.     switch_call_cause_t *cancel_cause,
  13.     switch_dial_handle_t *dh)
复制代码
参数解释:
  1. session : 发起originate的channel,即 caller_channel , aleg
  2. bleg : originate所在的leg,会在该函数赋值
  3. cause : 失败原因,会在该函数赋值
  4. bridgeto : bleg的呼叫字符串,只读
  5. timelimit_sec :originate超时时间
  6. table : bleg的状态机回调函数
  7. cid_name_override : origination_caller_id_name,用于设置主叫名称
  8. cid_num_override : origination_caller_id_number,用于设置主叫号码
  9. caller_profile_override :主叫的profile
  10. ovars : originate导出的通道变量(从aleg)
  11. flags : originate flag 参数,一般为 SOF_NONE
  12. cancel_cause :originate取消原因
  13. dh : dial handle,功能类似呼叫字符串,可以设置多条leg同时originate
复制代码
如果outgoing_channel执行成功,会发送SWITCH_EVENT_CHANNEL_OUTGOING事件;并且该channel会设置上CF_ORIGINATING标识位。
  1. if (switch_event_create(&event, SWITCH_EVENT_CHANNEL_OUTGOING) == SWITCH_STATUS_SUCCESS) {
  2.     switch_channel_event_set_data(peer_channel, event);
  3.     switch_event_fire(&event);
  4. }
复制代码
使用 switch_core_session_thread_launch 启动线程创建session :
  1. if (!switch_core_session_running(oglobals.originate_status[i].peer_session)) {
  2.     if (oglobals.originate_status[i].per_channel_delay_start) {
  3.         switch_channel_set_flag(oglobals.originate_status[i].peer_channel, CF_BLOCK_STATE);
  4.     }
  5.     switch_core_session_thread_launch(oglobals.originate_status[i].peer_session);
  6. }
复制代码
二、bridge流程 

1、流程入口

bridge app入口(mod_dptools.c):

函数调用链:
  1. audio_bridge_function
  2.     => switch_ivr_signal_bridge
  3.         => switch_ivr_multi_threaded_bridge
  4.             => audio_bridge_thread
复制代码
uuid_bridge api入口(mod_commands.c):
 函数调用链:
  1. uuid_bridge_function => switch_ivr_uuid_bridge
复制代码
2、bridge机制

注册回调函数:

 状态机里面进行回调, 当channel进入CS_EXCHANGE_MEDIA状态后,回调 audio_bridge_on_exchange_media 函数,触发audio_bridge_thread线程。
三、媒体交互流程 

1、注册编解码类型

通过 switch_core_codec_add_implementation 注册编解码。
添加PCMA编码:

 添加opus编码:
 
2、RTP数据交互及转码

函数调用链:
  1. audio_bridge_on_exchange_media => audio_bridge_thread
复制代码
收发音频数据:
  1. audio_bridge_thread
  2.     => switch_core_session_read_frame
  3.          => need_codec
  4.          => switch_core_codec_decode (调用implement的encode进行转码操作,比如 switch_g711a_decode)
  5.      => session->endpoint_interface->io_routines->read_frame 即: sofia_read_frame
  6.          => switch_core_media_read_frame
  7.         => switch_rtp_zerocopy_read_frame
  8.             => rtp_common_read
  9.             => read_rtp_packet
  10.                   => switch_socket_recvfrom
  11. audio_bridge_thread
  12.     => switch_core_session_write_frame
  13.          => switch_core_session_start_audio_write_thread (ptime不一致时启动线程,有500长度的队列)
  14.           => switch_core_codec_encode (调用implement的encode进行转码操作,比如 switch_g711u_encode)
  15.      => perform_write
  16.         => session->endpoint_interface->io_routines->write_frame 比如: sofia_write_frame
  17.         => switch_core_media_write_frame
  18.             => switch_rtp_write_frame
  19.             => rtp_common_write
  20.                 => switch_socket_sendto
复制代码

 音频数据会转成L16编码(raw格式),然后再编码成目标编码,示意图如下:

 具体可参考各个编码的 encode 和 decode 代码(添加编码时的注释也可参考下):

四、自定义endpoint集成媒体交互示例 

1、产生舒适噪音

产生舒适噪音,避免没有rtp导致的挂机。
1)需要设置 SFF_CNG 标志;
具体可参考 loopback 模块: 

 2)需要设置通道变量 bridge_generate_comfort_noise 为 true:
  1. switch_channel_set_variable(chan_a,"bridge_generate_comfort_noise","true");
复制代码
或者在orginate字符串中设置。
3)audio_bridge_thread函数里面有舒适噪音处理相关逻辑;

 
2、ptime保持一致

需要注意下编码的ptime值,当ptime不一致会触发freeswitch的缓存机制,进而导致运行过程中内存增加。
具体原理可从如下渠道获取:
关注微信公众号(聊聊博文,文末可扫码)后回复 20230806 获取。3、示例代码

这里基于之前写的FreeSWITCH添加自定义endpoint的文章:
https://www.cnblogs.com/MikeZhang/p/fsAddEndpoint20230528.html
以 C 代码为示例,简单实现endpoint收发媒体功能,注意事项如下:
1)设置endpoint编码信息,这里使用L16编码,ptime为20ms;
2)桥接 sip 侧的leg,实现媒体互通;
3)这里用音频文件模拟 endpoint 发送媒体操作,通过 read_frame 函数发送给对端;
4)接收到sip侧的rtp数据(write_frame函数),可写入文件、通过socket发出去或直接丢弃(这里直接丢弃了);
5)不要轻易修改状态机;
6)需要注意数据的初始化和资源回收;
需要对channel进行answer,这里在ctest_on_consume_media函数实现:
完整代码可从如下渠道获取:
关注微信公众号(聊聊博文,文末可扫码)后回复 20230806 获取。4、运行效果

1)编译及安装

 2)呼叫效果
测试命令:
  1. originate user/1000 &bridge(ctest/1001)
复制代码
运行效果:

这里的raw文件采用之前文章里面的示例(test1.raw),如何生成请参考:
https://www.cnblogs.com/MikeZhang/p/pcm20232330.html
endpoint模块集成媒体交互功能的编译及运行效果视频:
关注微信公众号(聊聊博文,文末可扫码)后回复 2023080601 获取。
五、资源下载

本文涉及源码和文件,可从如下途径获取:
关注微信公众号(聊聊博文,文末可扫码)后回复 20230806 获取。

 
 
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

道家人

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表