3

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

 1 year ago
source link: https://www.cnblogs.com/MikeZhang/p/fsAddEndpoint20230806.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
操作系统 :CentOS 7.6_x64
FreeSWITCH版本 :1.10.9
之前写过FreeSWITCH添加自定义endpoint的文章:
今天记录下endpoint媒体交互的过程并提供示例代码及相关资源下载,本文涉及示例代码和资源可从如下渠道获取:
关注微信公众号(聊聊博文,文末可扫码)后回复 20230806 获取。

一、originate流程 

1、originate命令的使用

originate用于发起呼叫,命令使用的基础模板:

originate ALEG BLEG

在fs_cli控制台使用的完整语法如下:

originate <call url> <exten>|&<application_name>(<app_args>) [<dialplan>][<context>] [<cid_name>][<cid_num>] [<timeout_sec>]
originate 为命令关键字,为必选字段,用于定义ALEG的呼叫信息,也就是通常说的呼叫字符串,可以通过通道变量定义很多参数;
|&<application_name>(<app_args>)  为必选字段,用于指定BLEG的分机号码或者用于创建BLEG的app(比如echo、bridge等);
[][<context>]  可选参数,该参数用于指定dialplan的context,默认值:xml default ;
[<timeout_sec>] 可选参数,该参数用于指定originate超时,默认值:60 ;
这里以分机进行示例呼叫:
originate user/1000 9196 xml default 'user1' 13012345678 
更多使用方法可参考我之前写的文章:

2、originate功能入口函数

入口函数为originate_function,在 mod_commands_load 中绑定:

SWITCH_ADD_API(commands_api_interface, "originate", "Originate a call", originate_function, ORIGINATE_SYNTAX);
具体实现如下:
#define ORIGINATE_SYNTAX "<call url> <exten>|&<application_name>(<app_args>) [<dialplan>] [<context>] [<cid_name>] [<cid_num>] [<timeout_sec>]"
SWITCH_STANDARD_API(originate_function)
{
    switch_channel_t *caller_channel;
    switch_core_session_t *caller_session = NULL;
    char *mycmd = NULL, *argv[10] = { 0 };
    int i = 0, x, argc = 0;
    char *aleg, *exten, *dp, *context, *cid_name, *cid_num;
    uint32_t timeout = 60;
    switch_call_cause_t cause = SWITCH_CAUSE_NORMAL_CLEARING;
    switch_status_t status = SWITCH_STATUS_SUCCESS;

    if (zstr(cmd)) {
        stream->write_function(stream, "-USAGE: %s\n", ORIGINATE_SYNTAX);
        return SWITCH_STATUS_SUCCESS;
    }

    /* log warning if part of ongoing session, as we'll block the session */
    if (session){
        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");
    }

    mycmd = strdup(cmd);
    switch_assert(mycmd);
    argc = switch_separate_string(mycmd, ' ', argv, (sizeof(argv) / sizeof(argv[0])));

    if (argc < 2 || argc > 7) {
        stream->write_function(stream, "-USAGE: %s\n", ORIGINATE_SYNTAX);
        goto done;
    }

    for (x = 0; x < argc && argv[x]; x++) {
        if (!strcasecmp(argv[x], "undef")) {
            argv[x] = NULL;
        }
    }

    aleg = argv[i++];
    exten = argv[i++];
    dp = argv[i++];
    context = argv[i++];
    cid_name = argv[i++];
    cid_num = argv[i++];

    switch_assert(exten);

    if (!dp) {
        dp = "XML";
    }

    if (!context) {
        context = "default";
    }

    if (argv[6]) {
        timeout = atoi(argv[6]);
    }

    if (switch_ivr_originate(NULL, &caller_session, &cause, aleg, timeout, NULL, cid_name, cid_num, NULL, NULL, SOF_NONE, NULL, NULL) != SWITCH_STATUS_SUCCESS
        || !caller_session) {
            stream->write_function(stream, "-ERR %s\n", switch_channel_cause2str(cause));
        goto done;
    }

    caller_channel = switch_core_session_get_channel(caller_session);

    if (*exten == '&' && *(exten + 1)) {
        switch_caller_extension_t *extension = NULL;
        char *app_name = switch_core_session_strdup(caller_session, (exten + 1));
        char *arg = NULL, *e;

        if ((e = strchr(app_name, ')'))) {
            *e = '\0';
        }

        if ((arg = strchr(app_name, '('))) {
            *arg++ = '\0';
        }

        if ((extension = switch_caller_extension_new(caller_session, app_name, arg)) == 0) {
            switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_CRIT, "Memory Error!\n");
            abort();
        }
        switch_caller_extension_add_application(caller_session, extension, app_name, arg);
        switch_channel_set_caller_extension(caller_channel, extension);
        switch_channel_set_state(caller_channel, CS_EXECUTE);
    } else {
        switch_ivr_session_transfer(caller_session, exten, dp, context);
    }

    stream->write_function(stream, "+OK %s\n", switch_core_session_get_uuid(caller_session));

    switch_core_session_rwunlock(caller_session);

  done:
    switch_safe_free(mycmd);
    return status;
}
调用流程如下:
originate_function 
    => switch_ivr_originate 
        => switch_core_session_outgoing_channel 
            => endpoint_interface->io_routines->outgoing_channel
        => switch_core_session_thread_launch     

3、switch_ivr_originate函数

该函数用于发起具体的呼叫。

switch_ivr_originate函数定义:

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

二、bridge流程 

1、流程入口

bridge app入口(mod_dptools.c):

300959-20230806212214038-309126875.png

函数调用链:

audio_bridge_function 
    => switch_ivr_signal_bridge
        => switch_ivr_multi_threaded_bridge 
            => audio_bridge_thread
uuid_bridge api入口(mod_commands.c):
300959-20230806212323268-1596503559.png

 函数调用链:

uuid_bridge_function => switch_ivr_uuid_bridge

2、bridge机制

注册回调函数:

300959-20230806212407272-1612575581.png

 状态机里面进行回调, 当channel进入CS_EXCHANGE_MEDIA状态后,回调 audio_bridge_on_exchange_media 函数,触发audio_bridge_thread线程。

三、媒体交互流程 

1、注册编解码类型

通过 switch_core_codec_add_implementation 注册编解码。

添加PCMA编码:

300959-20230806212453787-1856530807.png

 添加opus编码:

300959-20230806212510296-1230403952.png
 

2、RTP数据交互及转码

函数调用链:

audio_bridge_on_exchange_media => audio_bridge_thread

收发音频数据:

audio_bridge_thread 
    => switch_core_session_read_frame
         => need_codec
         => switch_core_codec_decode (调用implement的encode进行转码操作,比如 switch_g711a_decode)
     => session->endpoint_interface->io_routines->read_frame 即: sofia_read_frame
         => switch_core_media_read_frame
        => switch_rtp_zerocopy_read_frame
            => rtp_common_read
            => read_rtp_packet
                  => switch_socket_recvfrom


audio_bridge_thread 
    => switch_core_session_write_frame
         => switch_core_session_start_audio_write_thread (ptime不一致时启动线程,有500长度的队列)
          => switch_core_codec_encode (调用implement的encode进行转码操作,比如 switch_g711u_encode)
     => perform_write
        => session->endpoint_interface->io_routines->write_frame 比如: sofia_write_frame
        => switch_core_media_write_frame
            => switch_rtp_write_frame
            => rtp_common_write 
                => switch_socket_sendto 
300959-20230806212615858-1601548700.png

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

300959-20230806212632058-2098382714.png

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

300959-20230806212706669-2115330700.png
300959-20230806212720061-126909321.png

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

1、产生舒适噪音

产生舒适噪音,避免没有rtp导致的挂机。

1)需要设置 SFF_CNG 标志;
具体可参考 loopback 模块: 

300959-20230806212803682-900112898.png

 2)需要设置通道变量 bridge_generate_comfort_noise 为 true:

switch_channel_set_variable(chan_a,"bridge_generate_comfort_noise","true");

或者在orginate字符串中设置。

3)audio_bridge_thread函数里面有舒适噪音处理相关逻辑;

300959-20230806212859907-1310323183.png

 

300959-20230806212910602-1498730588.png

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函数实现:
300959-20230806213320144-336917119.png

完整代码可从如下渠道获取:

关注微信公众号(聊聊博文,文末可扫码)后回复 20230806 获取。

4、运行效果

1)编译及安装

300959-20230806213526148-574229401.png

 2)呼叫效果

测试命令:

originate user/1000 &bridge(ctest/1001)

运行效果:

300959-20230806213637584-418082929.png

这里的raw文件采用之前文章里面的示例(test1.raw),如何生成请参考:

https://www.cnblogs.com/MikeZhang/p/pcm20232330.html

endpoint模块集成媒体交互功能的编译及运行效果视频:

关注微信公众号(聊聊博文,文末可扫码)后回复 2023080601 获取。

五、资源下载

本文涉及源码和文件,可从如下途径获取:

关注微信公众号(聊聊博文,文末可扫码)后回复 20230806 获取。

300959-20230806213734397-1262499851.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK